위 코드에서 AnyObject와 App을 import 했다. AnyObject는 시맨틱처럼 웹앱에서 사용되는 어느 오브젝트이건 해당하는 오브젝트를 포함하는 타입을 지정한 것이다. App은 index.ts 파일에서의 작업을 거쳐서 app이라는 인스턴스를 생성하고 render 메소드를 실행한다. 여기서 app 인스턴스는 생성자함수로 root라는 문자열과 title이라는 key와 문자열 value를 가진 오브젝트를 파라미터로 전달한다.
여기서 root는 index.html에 root라는 아이디를 가진 태그에 사용될 것, title에 "Javascript & TypeScript Essential Chapter 5 - Sign up" 문자열을 반환 한다는 것을 예상할 수 있다. 또 app 인스턴스의 메소드로 render 함수를 실행하는데, 렌더링 한다는 것을 알수는 있지만 어떤 작업을 하는지 어느 부분을 렌더링하는지 등의 상세한 작업을 알 필요는 없다. 다만 index.ts 파일에서는 app 인스턴스를 생성하고 render 함수를 실행하는 작업을 한다.
src 하위의 폴더 구조는 이렇다. index.html에서 최상위의 index.ts 파일을 기준으로 둔다. index.ts 파일에선 전역적으로 사용하는 부분과 app.ts에 있는 사항들을 렌더링한다. app.ts는 회원가입 웹앱에 초기화하거나 사용되는 밸리데이션 등을 정의하고 입력한 사항들이 전송됐을 시에 발생하는 이벤트 등이 정의되어 있다.
views 폴더에 있는 각 파일등은 입력한 사항들의 화면과 구조 등이 있으며, types 폴더에는 각 기능들의 타입이 정의 되어있다. utils 폴더에는 유틸리티성 파일, 여러 방면에 사용될 수 있는 파일을 정의한다.
스코프는 하나의 그룹핑 되어있는 영역을 말하며 라이프 사이클은 어떠한 영역 안에서 실행되고 그 영역을 벗어나게 되면 삭제되는 형태를 말한다. 스코프는 전역 스코프, 블록 스코프, 함수 등이 있으며 함수 또한 fucntion(){} 형태의 스코프 안에서 동작하고 함수 실행이 끝나고 다른 영역으로 넘어갔을 때 사라지게 된다.
스코프, 예를 들어, 함수가 여러개 있다고해서 메모리에 모든 함수가 등록되는 것이 아니다. 선언되어 있는 모든 스코프가 메모리에 올라갈 수는 없다. 메모리는 한정 되어있는 자원이기 때문에 효율적으로 사용할 수 있어야 한다. 스코프는 그러한 것들을 유동적으로 이용하고 있는 셈이다.
let myname = 'kim'; // 전역 스코프
function foo() {
let x = 10;
console.log(myname);
console.log(x);
bar();
zoo();
function bar(){
let y = 10;
console.log(x);
console.log(myname);
} // foo 함수 스코프
const zoo = function(){
}
if(x ===10){ // foo 스코프의 변수 x
let x = 100; // foo 스코프 내의 if문 블록 안의 변수 x
console.log(x);
} // foo 함수 스코프
bar();
} // 전역 스코프
foo();
console.log(x);
스코프는 처음 진입될 때 생성된다. foo 함수로 진입 할 때, myname이라는 변수는 전역 스코프에 있으며, foo함수 내의 x라는 변수는 foo 함수의 스코프안에서 저장된다. 따라서 foo 함수에서는 myname 변수를 사용할 수 있지만 foo 함수가 실행되는 부분(전역 스코프)에서는 foo함수 안의 x를 사용할 수 없다. 스코프에서는 중첩이 일어나기 때문에 foo 함수 안에 bar 함수를 선언하면 bar 함수는 전역스코프와 foo함수 스코프 안에 있는 변수들을 사용할 수 있다.
또한 함수 선언문(function xxx(){})으로 작성된 함수는 호이스팅으로 인해 실행보다 선언이 코드 아래에 있어도 사용할 수 있지만 함수 표현식(var xxx = function(){})으로 작성된 함수는 꼭 함수가 선언되고 나서 실행되어야 한다.
delay라는 함수를 선언하고 이 함수는 Promise를 리턴한다. Promise 패턴의 함수에는 2가지의 파라미터를 받아올 수 있는데 성공, 실패이며, 여느 callback 함수와 다른건 이 부분이다. 처리가 성공했을 시 작업과 실패했을 시의 처리를 다르게 할 수 있다.
위 코드에서 then()~catch()패턴에서는 성공했을 시(resolve)에 'success' 문자열을 파라미터로 받아서 사용할 수 있으며 실패했을 시(reject)에 'failure' 문자열을 파라미터로 받아서 사용할 수 있다. 꼭 문자열 뿐만이 아니더라도 콜백함수로 성공과 실패의 반환값을 넘긴다면 해당하는 패턴에서 사용할 수 있다. 성공시에 then()을 리턴하고 then()에서 또 다음의 처리를 진행하고 싶다면 return을 통해 다음 작업을 같은 방식으로 진행한다. 그렇다면 이전의 then()에서 넘긴 return 값을 then()의 파라미터로 받아서 사용할 수 있다. 이 이유는 then()에서 반환한 값은 또 Promise를 리턴하기 때문이다.
또 Promise를 반환하는 패턴이라면 async & await으로 사용할 수 있는데 then()~catch()와 다른 점은 해당하는 코드의 진행을 동기적으로 처리할 수 있다는 점이다. then()~catch()의 경우는 then()의 처리가 완료되고 다음의 처리를 하기위해 then()을 연속적으로 붙여서 사용해야 하는데 async & await의 경우는 async로 선언된 함수 안에 await이 명시되어 있는 함수의 다음 코드의 진행은 동기적으로 작성해도 비동기로 처리된다.
지금까지의 코드에서 비동기 처리를 해야하는 부분(서버에서 데이터를 가져오는)은 XHR을 통해 콜백함수로 받아오거나, fetch를 사용해서 Promise를 리턴하는 방법이 있다.
Promise 패턴을 사용하게되면 then(), catch(), finally 등으로 코드를 단계적으로 확인할 수 있는데, 이 방법 말고도 보기에는 동기적인 코드로 작성하지만 작동은 비동기적으로 사용할 수 있는 패턴이있다. 마찬가지로 이것은 Promise를 반환할 때 사용하며, 내부적으로 비동기처리에 대한 사이클이 달라지는 것은 아니지만 다른 프로그래밍 언어의 코드 진행처럼 비동기처리를 작성할 수 있다.
async & await
async
export default class Api {
...
async request<AjaxResponse>(): Promise<AjaxResponse> {
const response = await fetch(this.url);
return await response.json() as AjaxResponse;
}
}
export class NewsFeedApi extends Api {
...
async getData(): Promise<NewsFeed[]> {
return this.request<NewsFeed[]>();
}
}
export class NewsDetailApi extends Api {
...
async getData(): Promise<NewsDetail> {
return this.request<NewsDetail>();
}
}
사용하는 함수나 메소드에 async 키워드를 사용하고 해당 함수나 메소드는 await으로 받아서 사용할 수 있다.
지금까지 작성한 모든 소스코드는 동기적 방식으로 작성했다. 현재 해커뉴스 클라이언트의 API를 통해서 json형태의 데이터를 받아온다.
export default class Api {
...
getRequest<AjaxResponse>(): AjaxResponse {
this.ajax.open('GET', this.url, false);
this.ajax.send();
return JSON.parse(this.ajax.response) as AjaxResponse;
}
...
}
Api 클래스의 getRequest 메서드는 XHR 형태로 데이터를 받아오지만 동기적으로 받아온다. 그렇다는 것은 데이터의 양이 많거나 서버의 통신이 조금 느릴 때, 해당 작업을 처리하는 시간동안 만들어놓은 시스템이 잠시 멈추는 듯한 느낌을 받을 수 있고 실제로 그 처리 작업을 하는 동안 그 뒤의 작업은 수행하지 못함으로 화면의 처리가 유동적이지 못하게 느낄 수 있다.
위 방식은 기존의 동기적인 XHR 형식에서 비동기적인 XHR 형식으로 전환한 것과 Promise를 반환하는 패턴인 fetch Api를 사용했다. XHR 형식은 비동기적 처리를 할 때, return을 반환할 수 없으므로 ( return이 반환 되었을 시에 이미 send를 통해 작업이 끝남, 처리 순서가 뒤바뀜) callback 함수를 통하여 해당 작업이 끝났을 시에 작동하도록 처리 결과를 넘겨주었다.
fetch 형식은 Promise를 반환하며, Promise는 하나의 디자인패턴으로, 비동기처리를 위한 콜백 함수를 사용할때, 콜백 함수의 가시적인 면에서 이점이 있으며, json의 데이터를 object 형태로 변환하는 과정 등의 처리에 있어 이점이 있다.
기존의 app.ts로 만든 하나의 파일에 페이지의 모든 기능을 작성했다. 이번에는 하나로 작성된 ts 파일을 기능 단위로 여러개의 ts파일로 구분할 것이다. 페이지의 역할을 하는 파일, 타입을 지정해주는 파일, 공통적으로 사용되는 파일, 라우팅 기능을 하는 파일 등으로 구분하는 폴더 구조를 갖는다.
구조를 정하여 관리하게 되면 어느 부분에서 에러, 오류 등이 발생 했을 때, 해당하는 파일에서 사용된 부분만을 수정하면 되고, 기능을 사용하는 부분은 사용 되어지는 파일의 세부적인 부분까지 몰라도 기능을 사용할 수 있지만 너무 세세하게 나눠진 구조는 오히려 구조를 더 복잡하게 할 수도 있다.
// (src/app.ts)
import Router from "./core/router";
import { NewsFeedView, NewsDetailView } from "./page";
import Store from "./store";
const store = new Store();
const router: Router = new Router();
const newsFeedView = new NewsFeedView('root', store);
const newsDetailView = new NewsDetailView('root', store);
router.setDefaultPage(newsFeedView);
router.addRoutePath('/page/', newsFeedView);
router.addRoutePath('/show/', newsDetailView);
router.route();
기존에 함수(function)형태로 작성한 코드들을 ES6의 클래스(class)형태로 바꾼다. function을 class로 바꾸면서 기존의 각 기능 등을 정의하는 함수만으로 정의되어 있는데, 각각의 함수들을 공통적인 부분으로 인스턴스화 할 수 있고, 공통적인 부분 들은 공통적인 클래스로 정의하고 상속을 받아서 사용 할 수 있는 구조를 만들 수 있다.
또한 클래스의 특징으로 구조와 기능은 같지만 값만 다른 여러개의 인스턴스를 복제하듯이 만들어 낼 수 있다. (화면의 동일한 컴포넌트들을 값만 다르게 생성할 수 있음)
이번 장에선 지금까지 구현한 코드들의 구조를 개선했다. 함수로 구현했던 부분을 전부 es6의 클래스 형태로 바꿨고 목록이나 내용을 보여주는 클래스는 공통적으로 처리하는 부분을 View 클래스를 통해 상속을 받아 구현 했고 ajax를 통해 데이터를 불러오는 부분은 Api 클래스를 상속 받아 만듦으로써 구조화 시켰으며, 라우팅 처리해주는 부분 역시 클래스화 했다.