1.1TypeScript Basics
타입스크립트 기본 문법
TL;DR
추억의 쪽지 시험
TypeScript?
동적 타이핑(Dynamic Typing) 언어인 자바스크립트는 변수의 타입이 런타임에서 결정된다. 다른 언어들(Java, C, ...)에선 변수를 선언할 때 무슨 타입인지까지도 지정하는데 비해, 자바스크립트는 값이 할당되는 순간 타입이 정해진다.
이런 특성은 프로젝트가 작고 빠르게 뚝딱 만들때는 편리해보일 수 있어도, 프로젝트가 진행되고 커짐에 따라 런타임에서 예상치 못한 버그가 나올 가능성이 무척 늘어나게 한다. 예를 들어, 숫자와 실수로 문자열을 더했는데 전체를 문자열로 바꿔버리는 자바스크립트의 작동 방식은 의도적이었으면 물론 문제없고 오히려 간편하게 문제를 푼 것이 되겠지만 대부분의 경우 의도를 떠나 '그렇게 될 줄도 모르고' 지나가게 되어 이후에 더 큰 문제를 마주하면서 발견하게 된다.
타입스크립트는 이런 자바스크립트의 슈퍼셋 언어로, 컴파일 단계에서 미리 타입 오류를 잡아주기 때문에 런타임에 가서야 예상치 못한 문제를 발견하게 되는 경우를 줄여준다. 또, 타입이 정해져있으면 자동 완성 등의 코드 힌트 또한 보다 정확해진다. 타입이 정해져있기 때문에 중간에 의도치 않게 타입을 바꾸거나 해당 타입에 맞지 않는 데이터를 넣는다거나 하는 일을 사전에 방지할 수 있다.
TypeScript 기초 및 기본 타입들
1. Type Inference(타입 추론)와 Type Assignment(타입 할당)
타입스크립트에서 변수에 타입을 지정하는 방법은 크게 2가지, 타입 추론과 타입 할당이 있다. 타입 추론은 주로 초기값을 변수에 할당할 때 자동으로 해당 값의 타입을 변수의 타입으로 지정하는 식으로 이루어진다.
반면, 타입 할당은 명시적으로 해당 변수가 어떤 타입인지 지정한다. 할당은 변수 이름 다음에 :를 쓰는식으로 이루어진다.
typescriptlet userAge = 33; // 타입 추론 let userName: string; // 타입 할당 // ... userName = 'Sungyup'
함수 파라미터도 같은 방식이다.
typescriptfunction add(a : number, b = 5){// 초기값을 설정한 b는 타입이 자동으로 number로 추론된다.return a + b;}add(10, 6); // 16add('10', 6); // 에러!
2. 여러가지 타입을 가질 수 있는 변수 처리
일단 답이 아닌 것부터 알아보면, 타입스크립트에는 any라는 타입이 있다. 이 타입은 동적으로 타입을 정하겠다는 것을 의미하므로, 타입스크립트를 쓰는 근본 원인을 무시하는 것이다.
typescriptlet age: any = 33;age = '34'; // 가능하긴 한데...
여러가지 타입을 가질 수 있는 변수는 타입들을 Union하는 것이 일반적으로, |로 여러 타입을 이어 붙이면 된다.
typescriptlet age: string | number = 33;age = '34'; // okay
3. 배열
배열의 구성요소들도 타입 지정을 할 수 있다. 아래처럼 타입 할당을 해도 되지만, 초기값을 넣어두면 해당 타입으로 추론한다.
typescriptlet hobbies: string[]; hobbies.push('piano'); // ['piano'] hobbies.push('writing'); // ['piano', 'writing'] hobbies.push(10); // 에러!
여러개의 타입이 들어가는 배열은 아래와 같은 식으로 Union(|) 연산자를 통해 지정할 수 있다.
typescriptlet users: (string | number)[]; users = [1, 'Sungyup'];
나중에 배울 Generic Type를 미리 예고하자면, 아래와 같은 방식으로도 타입을 지정할 수 있다.
typescriptlet users: Array<string | number>; // let users: (string | number)[]과 동일
이 방식은
Array<T>라는 Generic Type이 타입스크립트에 이미 정의된 상태인데,T자리에string | number라는 구체적 타입 인자를 넣어서 정의한 것이다.
4. 튜플
튜플은 유한개의 원소로 이루어진 집합이다. 만약 숫자만 들어갈 수 있는, 정확히 3개의 요소가 있는 튜플을 정의하려면 어떻게 할 수 있을까? 앞에서 배운 방법으로만 하면 number[]를 생각할 수 있지만, 이 경우 3개로 요소의 수를 제한할 수 없다.
이런 경우 직접 배열의 각 순서에 무슨 타입이 들어가는지 아래와 같이 정의할 수 있다.
typescriptlet possibleResults: [number, number, number];
5. 객체
JSON을 자주 쓰고, 이 때문에 타입 에러가 많이 발생하곤 하는 자바스크립트에서 객체 타입 정의는 아주 중요하다. 타입스크립트에선 객체에 어떤 속성이 있는지, 그리고 각 속성은 어떤 타입의 값을 가지는지 정의해서 제한할 수 있다.
개인적으로 타입스크립트로 개발을 하면서 외부 라이브러리를 쓰면 객체 관련된 에러를 가장 많이 마주했는데(특정 속성이 필수인데 빠졌다거나), 그말인즉슨 자바스크립트였으면 꼼짝없이 모르고 런타임에서 뭔가 잘못된걸 겨우 발견해서 어디서부터 고쳐야하는지 한참 헤맸을 문제를 개발 단계에서 이미 쉽게 확인하고 고칠 수 있었단 의미일 것이다.
typescriptlet user: { name: string; age: number; hobbies: string[]; role: { description: string; id: number; } } = { name: 'Sungyup', age: 33, hobbies: ['piano', 'writing'], role: { description: 'admin', id: 1 } }
6. "Must not be null" 타입
타입스크립트에는 약간 이상하게 생긴 타입도 있다.
typescriptlet val: {} = 'some text'; // 에러 아님!
이 {}타입은 null이나 undefined가 아닌 값을 의미하는 타입이다.
7. 그렇다면 실제 키-값을 알기 전의 빈 객체는?
그렇다면 객체긴 한데 키-값 타입은 아직 잘 모르겠는 객체는 어떻게 정의할 수 있을까?
타입스크립트에는 특수한 Record라는 Generic Type이 있다. Record<Keys, Type>라는 Generic Type을 이용하면 빈 객체의 타입을 설정할 수 있다.
typescriptlet data: Record<string, number | string>;
8. Enum
특정 옵션들만 허용해야 할 때는 enum 타입을 정의한다. 예를 들어, Admin, Editor, Guest의 3가지 옵션만 있어야한다면 string 타입만으로는 제한할 수 없다.
typescriptenum Role { Admin, Editor, Guest } let userRole: Role = Role.Admin; // Role.을 입력하면 자동완성이 뜬다 userRole = 2; // Role안의 인덱스에 접근하듯이 접근할 수도 있다!
다만, 아래와 같이 Union 연산자로 가능한 옵션들을 제한할 수도 있다. 실제로, 몇가지 이유로 인해(이번 포스팅 맨 아래에서 더 자세히 살펴보자!) 아래와 같은 union literal + type alias방식이 enum 키워드보다 자주 쓰이곤 하는 편이다.
typescriptlet userRole: 'admin' | 'editor' | 'guest' = 'admin'; userRole = 'guest'; // 역시 자동완성된다.
9. Type Aliases(custom types)
방금 전 Union 키워드로 만든 userRole과 같은 타입들을 여러 군데에서 쓰려고 할 때, 복사를 하기보단 해당 타입을 하나의 커스텀 타입으로 정의하고 재활용할 수 있다.
typescripttype Role = 'admin' | 'editor' | 'guest' | 'reader'; let userRole: Role = 'admin'; function access(role: Role){ // ... }
앞서 본 객체의 경우도 마찬가지로 타입을 정의하고, 객체를 생성할 때 해당 타입을 할당하는 식으로 쓰는 것이 편하다.
typescripttype User = { name: string; age: number; hobbies: string[]; role: { description: string; id: number; } }; const me: User = { name: 'Sungyup', age: 33, hobbies: ['piano', 'writing'], role: { description: 'admin', id: 1 } };
10. 함수의 반환값에 타입 지정하기
타입스크립트의 타입 추론 기능 덕분에 함수의 반환값에 타입을 지정할 필요는 없는 경우가 많지만, 가끔 필요할 때가 있다. 이런 경우 아래와 같이 결과값에 대한 타입도 지정한다.
typescriptfunction add(a: number, b: number): number { return a + b; }
10-1. void 타입
함수가 반환값이 없으면 void 타입을 반환한다.
typescriptfunction log(message: string): void { console.log(message); // 아무것도 반환하지 않는다! }
10-2. never 타입
해당 함수가 아무것도 반환하지 않는것은 물론, 다른 곳에서 반환값을 쓰지 못하게 막을 경우라면 never 타입을 지정한다. 다시 말해, 끝까지 실행될 수 없는 함수임을 알리는 것이다.
typescriptfunction logAndThrow(errorMessage: string): never { console.log(errorMessage); throw new Error(errorMessage); }
10-3. 타입이 함수인 함수
함수를 파라미터로 받는 경우, 해당 파라미터에 대한 타입을 어떻게 지정해야할까? => 키워드를 활용한 arrow function으로 함수인것을 알린다. 이를 통해 해당 함수의 파라미터 및 반환값에 대한 타입도 지정할 수 있다.
typescriptfunction performJob(cb: () => void) { // ... cb(); }; // 파라미터가 어떤게 들어가는지는 이름으로, 어떤 타입인지는 타입 지정으로 할 수 있다. function complexJob(cb: (msg: string) => string) { // ... cb('message'); };
11. null과 undefined
null과 undefined는 자바스크립트의 해당 데이터 타입들과 같은 의미를 가진다.  이전에도 정리한적 있었지만, undefined는 아직 값이 할당되지 않은 상태이고, null은 비어있는 값이다.
html 문서에 form 엘리먼트를 만들고 거기에 연결된 타입스크립트 코드를 작성하는 경우, 해당 form 엘리먼트의 값을 어떤 변수에 할당한다고 하자.
typescriptconst inputEl = document.getElementById('user-name');
이러면 inputEl의 타입은 HTMLElement | null로 타입스크립트에서 자동으로 추론할 것이다.
undefined는 DOM API 반환값에선 쓰이지 않고, user-name이라는 ID가 없다면 null이 반환되고 만약 있다면 HTMLElement가 반환된다. 여기서 .id나 .className, .style등의 속성으로 접근한다. 값은 HTMLInputElement의 .value로 접근하는데, 이에 대해선 12번 Type Casting(as)에서 알아보자.다만 이렇게 null도 가능하게 된다면 이후 inputEl의 속성에 접근하려고 하면 에러가 발생하게 된다. 따라서 null이 아닐때 접근하라거나 null이 아닐 것이라고 타입을 narrow-down 해주는 작업이 필요하다. 여기에는 여러가지 방법이 있다.
11-1. if 조건문으로 null값일때 대처하기
if(!inputEl)과 같은 조건문으로 null값일때 빠른 반환을 해버리거나 오류를 발생시킨다면, 이후의 코드에선 null인 경우는 없으므로 속성에 접근할 수 있다.
typescriptconst inputEl = document.getElementById('user-name');if(!inputEl){throw new Error('Element not found!');}// null인 경우는 이미 제거됨console.log(inputEl.id);
11-2. !로 값이 반드시 존재한다고 알리기
!를 쓰면 해당 값은 반드시 존재한다는 의미다. 다만, 이 경우는 신중하게 쓰지 않는다면 예상치 못한 에러를 방지할 수 없다.
typescriptconst inputEl = document.getElementById('user-name')!;console.log(inputEl.id);
11-3. ?로 값이 존재할때만 접근하기
?를 쓰면 해당 값이 존재할때 속성에 접근하라는 의미가 된다. 다만, 이 경우도 역시 실제로 null인 경우 fallback이 없다는 한계가 있다.
typescriptconst inputEl = document.getElementById('user-name')!;console.log(inputEl?.id);
12. Type Casting과 as
getElementById로 HTMLElement 타입을 받게 되면 값에 접근할 수 없다. 이는 id로만 엘리먼트에 접근했기 때문에 해당 엘리먼트가 값이 있는 인풋임을 알 수 없기 때문이다. 따라서, 이 경우 id로 엘리먼트를 받겠다면 as 키워드로 해당 엘리먼트가 HTMLInputElement라는 사실을 알려줘야 한다.
typescriptconst inputEl = document.getElementById('user-name') as HTMLInputElement | null;console.log(inputEl?.value);
13. unknown
코드를 작성할 때, 특정 변수나 파라미터가 무슨 타입일지 아직은 알 수 없는 단계일 때가 있다. 이럴때 unknown 타입을 설정하고, if로 해당 변수나 파라미터의 타입에 따른 조건문을 작성한다.
아래와 같은 조건문은 다소 복잡하지만, any를 쓰고 val.log()를 실행하는 것보다 더욱 안전한 개발이 가능하다. unknown은 타입이 체크되면 안전하게 사용할 수 있다는 의미가 있기 때문이다.
typescriptfunction process(val: unknown){ if( typeof val === 'object' && !!val && 'log' in val && typeof val.log === 'function' ){ val.log(); } }
14. Optional Values
함수에서 파라미터가 옵션이라면(없어도 된다면) ?로 표시한다.
typescriptfunction generateError(msg?: string){throw new Error(msg);}generateError();
객체 등의 타입을 설정할때도 옵션이라면 ?로 표시할 수 있다.
typescripttype User = {id: string;name: string;role?: 'admin' | 'guest'};
15. Nullish Coalescing
널 병합 연산자(??)는 연산자 왼쪽이 null 또는 undefined일 때만 오른쪽을 반환하고, 그 외는 왼쪽을 반환하는 연산자다.falsy value일 때가 아니다! 타입스크립트에선 이 널 병합 연산자로 인풋이 들어왔는지 등에 따른 분기를 쉽게 처리할 수 있다.
typescriptlet input = ''; // 아래와 같이 쓰면 0, ''과 같은 falsy value도 `false`가 된다! // const didProvideInput = input || false; // 아래와 같이 널 병합 연산자를 써야 한다. const didProvideInput = input ?? false;
🥸 보론
1. 자바스크립트의 기본 타입들
자바스크립트는 ECMAScript 사양에 따라 7개의 원시 타입(Primitive Types)을 가진다.
- string
 - number
 - bigint: 숫자뒤에 n을 붙인다.
 - boolean
 - symbol: 유일한 식별자를 생성한다. 주로 객체 키에 사용된다.
 - null: 의도적으로 빈 값.
 - undefined: 아직 할당되지 않은 값.
 
이 타입들은 타입스크립트의 기본 타입 시스템과 대응되며, any, unknown, void, never는 타입스크립트에만 있는 타입이다.
2. Enum이 덜 쓰이는 이유
타입스크립트는 런타임이 아니라 개발용 언어이기 때문에, 컴파일러가 코드를 읽으며 타입 규칙이 맞는지 검사한다. 타입스크립트로 적은 타입과 관련된 코드들 중 상당수는 자바스크립트로 트랜스파일링 할 때 없어지는데, enum은 다소 예외적으로 자바스크립트 객체를 실제로 생성한다.
typescriptenum Role { Admin, Editor, Guest }
위 코드를 컴파일하면 아래와 같이 객체가 생성되는 것을 확인할 수 있다:
javascriptvar Role; (function (Role){ Role[Role["Admin"] = 0] = "Admin"; Role[Role["Editor"] = 1] = "Editor"; Role[Role["Guest"] = 2] = "AGuest; })(Role || (Role = {}));
즉, enum은 단순 타입 정의가 아니라 실행 시점에 객체를 만드는 코드를 추가하기 때문에 코드 크기가 늘고, 런타임에도 부하를 줄 수 있다. 또, 트리셰이킹 때도 런타임 객체다보니 남아서 번들 최적화에도 지장을 준다.
만약 양방향 매핑, 즉 숫자와 문자열을 매핑해서 Role.Admin === 0, Role[0] === 'Admin'과 같은 특성을 많이 썼으면 enum은 나름의 가치를 가졌겠지만 Union Literal + Type Alias, 즉 아래의 방식으로도 충분히 타입 체크를 할 수 있고 자동완성도 편하게 할 수 있다.
typescripttype Role = 'admin' | 'editor' | 'guest'; let userRole: Role = 'admin';
이 방식은 런타임 코드가 전혀 추가되지 않기에 enum보다 가볍고 직관적이다. 따라서 enum보다 자주 쓰이게 되었다.