1장 들어가며
1.1 웹 개발의 역사
Polyfill
- 브라우저가 지원하지 않는 코드를 브라우저에서 사용할 수 있도록 변환한 코드 조각이나 플러그인
- ex)
core.js
,polyfill.io
Transpile
- 최신 버전의 코드를 예전 버전의 코드로 변환하는 과정
- ex)
Babel
CBD(Component Base Development)
- 서비스에서 다루는 데이터를 구붆고 그에 맞는 UI를 표현할 수 있게 컴포넌트 단위로 개발하는 접근 방식
- 재사용할 수 있는 컴포넌트를 개발 또는 조합해서 하나의 애플리케이션을 만드는 개발 방법론
- 컴포넌트
- 모듈과 유사하게 하나의 독립된 기능을 재사용하기 위한 코드 묶음
- 모듈과 달리 런타임 환경에서 독립적으로
배포/실행
될 수 있는 단위(다른 컴포넌트와 의존성을 최소화하거나 없애야 함)
1.2 JS의 한계
동적 타이핑 시스템의 한계
const sumNumber = (a, b) => {
return a + b;
};
sumNumber(100); // NaN
sumNumber('a', 'b'); // ab
- 동적 타입 언어라는 특성 때문에
sumNumber
함수를 호출할 때 사용되는 인수 값에 따라 a와 b의 타입이 결정됨 - 즉, 코드는 기계 입장에서는 정상적이지만 사람 입장에서는 정상적이지 않는 코드 발생
TS 등장(JS의 단점 극복)
-
JS의 슈퍼셋
- 기존 언어에 새로운 기능과 문법을 추가해서 보완하거나 향상하는 것
- 기존 언어와 호환되며 일반적으로 컴파일러 등으로 기존 언어 코드로 변환되어 실행 됨
-
안정성 보장
- 정적 타이핑 제공
- 컴파일 단계에서 타입 검사하기때문에 런타임 에러를 사전에 방지하여 안정성 크게 향상
-
개발 생산성 향상
- IDE에서 자동 타입 완성 기능 제공
- 변수와 함수 타입을 추론 가능
- React 사용할 때, props 타입 확인 가능
-
협업에 유리
interface
,generic
등으로 코들르 더 쉽게 이해할 수 있도록 제공
-
JS 점진적으로 적용 가능
- TS는 JS의 슈퍼셋이기 때문에 일괄 전환이 아닌, 점진적 도입 가능
2장 타입
2.1 타입이란
자료형으로서의 타입
- ECMAScript 표준 데이터 타입:
undefined, null, Boolean, String, Symbol, Number, BigInt, Object
- 데이터 타입은 여러 종류의 데이터를 식별하는 분류 체계로 컴파일러에 값의 형태를 알려줌
- 메모리에 저장된 값을 데이터 타입으로 설명할 수 있으며 모든 데이터를 해석할 때 데이터 타입 체계가 사용됨
- 메모리 관점에서의 데이터 타입은 프로그래밍 언어에서 일반적으로 타입으로 부르는 개념과 같음
컴파일 방식
- 컴파일: 사람이 이해 할 수 있는 방식으로 작성한 코드를 컴퓨터가 이해할 수 있는 기계어로 바꿔주는 과정
- TS 컴파일 결과는 JS 파일: JS의 컴파일 타임에 런타임 에러를 사전에 잡아내기 위함
2.2 TS 타입 시스템
타입 Annotation
-
변수나 상수 혹은 함수의 인자와 반환 값에 타입을 명시적으로 선언해서 어떤 타입 값이 저장 될 것인지를 컴파일러에게 직접 알려주는 문법
let isDone: boolean = false; let decimal: number = 6; let color: string = 'blue'; let list: number[] = [1, 2, 3]; let x: [string, number]; // tuple
구조적 타이핑
-
타입을 사용하는 여러 프로그래밍 언어에서 값이나 객체는 하나의 구체적인 타입을 가지고 있음. 타입은 이름으로 구분되며 컴파일타임 이후에도 남아있음. 이것을 명목적으로 구체화한 타입 시스템이라고 부름
class Animal { String name; int age; }
-
서로 다른 클래스끼리 명확한 상속 관계나 공통으로 가지고 있는 인터페이스가 없다면 타입은 서로 호환되지 않음
interface Developer { faceValue: number; } interface BankNote { faceValue: number; } let develop: Developer = { faceValue: 52 }; let bankNote: BankNote = { faeValue: 1000 }; develop = bankNote; // ok bankNote = develop; // ok
-
TS는 구조로 타입을 구분함(구조적 타이핑)
구조적 서브 타이핑
-
객체가 가지고 있는 속성을 바탕으로 타입을 구분하는 것(이름이 다른 객체라도 가진 속성이 동일하다면 TS는 서로 호환이 가능한 동일한 타입으로 여김)
interface Pet { name: string; } interface Cat { name: string; age: number; } let pet: Pet; let cat: Cat = { name: 'Zag', age: 2 }; pet = cat; // ok
interface Pet { name: string; } function greet(pet: Pet) { console.log('Hello, ' + pet.name); } greet(cat); // ok
- 타입을 명시하지 않은
cat
객체를greet()
함수의 인자로 전달해도 코드는 정상적으로 동작 cat
객체는Pet
인터페이스가 가지고 있는name
속성을 가지고 있어pet.name
의 방식으로name
속성에 접근할 수 있음- 이와 같은 타이핑 방식이 구조적 타이핑
- 타입을 명시하지 않은
-
TS의 서브타이핑, 즉 타입의 상속 역시 구조적 타이핑을 기반으로 하고 있음
class Person { name: string; age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } } class Developer { name: string; age: number; sleepTime: number; constructor(name: string, age: number, sleepTime: number) { this.name = name; this.age = age; this.sleepTime = sleepTime; } } function greet(p: Person) { console.log(`Hello, I'm${p.name}`); } const developer = new Developer('zig', 20, 7); greet(developer); // Hello, I'm zig
Develop
클래스가Person
클래스를 상속받지 않았는데도greet(develop)
는 정상적으로 동작함.Develop
는Person
이 갖고 있는 속성을 가지고 있기 때문
JS를 닮은 TS
- 덕 타이핑: 어떤 함수의 매개변숫값이 올바르게 주어진다면 그 값이 어떻게 만들어졌는지 신경쓰지 않고 사용한다는 개념
- TS는 JS의 슈퍼셋이기 때문에 덕 타이핑 동작을 그대로 모델링 함 그래서 명시적인 이름을 가지고 타입을 구분하는 대신 객체나 함수가 가진 구조적 특징을 기반으로 타이핑하는 방식 택함
- 덕 타이핑은 런타임에 타입을 검사하고 구조적 타이핑은 컴파일타임에 타입체거가 타입을 검사함
- 덕 타이핑과 구조적 타이핑 모두 객체 변수, 메서드 같은 필드를 기반으로 타입을 검사한다는 점에서는 동일하지만, 타입을 검사하는 시점이 다름
- 덕 타이핑은 주로 동적 타이핑에서, 구조적 타이핑은 정적 타이핑에서 사용됨
구조적 타이핑의 결과
-
TS의 구조적 타이핑의 특징 때문에 예기치 못한 결과가 나올수 있음
interface Cube { width: number; height: number; depth: number; } function addLines(c: Cube) { let total = 0; for (const axis of Object.keys(c)) { // ‼️ Element implicitly has an 'any' type // because expression of type 'string' can't be used to index type 'Cube' // ‼️ No index signature with a parameter of type 'string' // was found on type 'Cube' const length = c[axis]; total += length; } }
-
addLines()
함수의 매개변수인c
는Cube
의 타입으로 선언되었고,Cube
인터페이스의 모든 필드는number
타입을 가지기 때문에c[axis]
는 당연히number
타입일 것이라고 예측함 -
그러나
c
에 들어온 객체는Cube
의width, height, depth
외에도 어떤 속성이든 가질 수 있기 때문에c[axis]
의 타입이string
일 수도 있어 에러가 발생함const namedCube = { width: 6, height: 5, depth: 4, name: 'SweetCube', // string 타입의 추가 속성이 정의되었다. };
- TS는
c[axis]
가 어떤 속성을 지닐지 알 수 없으며c[axis]
타입을number
라고 확정할 수 없어서 에러를 발생시킴 - TS 구조적 타이핑의 특징으로
Cube
타입 값이 들어갈 곳에name
같은 추가 속성을 가진 객체도 할당할 수 있기 때문에 발생하는 문제 - 이러한 한계를 극복하고자 TS에는 명목적 타이핑 언어의 특징을 가미한 식별할 수 있는 유니온 같은 방법이 생겨남
- TS는
-
TS의 점진적 타입 확인
- 점진적 타입 검사: 컴파일 타임에 검사하면서 필요에 따라 타입 선언 생략을 허용하는 방식
- 타입을 지정한 변수와 표현식은 정적으로 타입을 검사하지만 타입 선언이 생략되면 동적으로 검사를 수행
- 타입 선언 생략하면 암시적 타입 변환 일어남
주의 사항
-
위 특징때문에 TS의 타입시스템은 정적 타입의 정확성을 100% 보장하지 않음
-
모든 변수와 표현식의 타입을 컴파일에 검사하지 않아도 되기 때문에 타입이 올바르게 정해지지 않으면 런타임에 에러가 발생하기 함
const names = ['zig', 'colin']; // ‼️ TypeError: Cannot read property 'toUpperCase' of undefined console.log(names[2].toUpperCase());
값과 타입
- 값: 프로그램이 처리하기 위해 메모리에 저장하는 모든 데이터
- TS는 값과 타입이 함께 사용됨
값과 타입 공간에 동시에 존재하는 심볼
-
Class
class Developer { name: string; domain: string; constructor(name: string, domain: string) { this.name = name; this.domain = domain; } } const me: Developer = new Developer('kay', 'frontend');
- 변수명
me
뒤에 등장하는:Developer
에서Developer
는 타입에 해당하지만,new
키워드 뒤의Developer
는 클래스의 생성자 함수인 값으로 동작함 - TS에서 클래스는 타입 애너테이션으로 사용할 수 있지만, 런타임에서 객체로 변화되어 JS의 값으로 사용되는 특징을 가지고 있음
enum
-
런타임에 객체로 변환되는 값
-
런타임에 실제 객체로 존재하며, 함수로 표현할 수도 있음
enum Direction { Up, // 0 Down, // 1 Left, // 2 Right, // 3 }
// Enum이 Type으로 사용하는 겨우 enum WeekDays { MON = "Mon", TUES = "Tues", WEDNES = "Wednes", THURS = "Thurs', FRI = "Fri" } // 'MON' | 'TUES' | 'WEDNES' | 'THURS' | 'FRI' type WeekDaysKey = keyof typeof WeekDays; function printDay(key: WeekDaysKey, message: string) { const day = WeekDays[key]; if (day <= WeekDays.WEDNES) { console.log(`It's still ${day}day, ${message}`) } } printDay("TUES", "wanna go home");
// enum이 값 공간에서 사용된 경우 enum MyColors { BLUE = '#0000FF', YELLOW = '#FFFF00', MINT = '#2AC1BC', } function whatMintColor(palette: { MINT: string }) { return palette.MINT; } whatMintColor(MyColors); // ok
enum과 객체 차이
- enum과 객체의 차이
- enum
- 타입 & 값 제공: 선언 시 타입과 런타임 값 둘 다 생성되어 하나의 네임스페이스처럼 사용할 수 있습니다.
- 역매핑: 숫자 enum의 경우, 값과 이름 간에 양방향(정방향/역방향) 매핑이 자동으로 이루어집니다.
- 런타임 객체: 실제 객체로 생성되므로, 트리 쉐이킹 측면에서는 다소 불리할 수 있습니다.
- 객체 (예:
as const
사용)- 불변 & 리터럴 타입:
as const
를 사용하면 각 속성이 고정된 리터럴 타입으로 취급되어 타입 안전성이 높아집니다. - 단순 값 집합: 값만 제공하며, 자동으로 타입(예:
union
타입)이나 역매핑 기능은 지원하지 않습니다. - 최적화: 불필요한 코드 제거(트리 쉐이킹)에 유리합니다.
- 불변 & 리터럴 타입:
- 역매핑(Reverse Mapping)
-
숫자 enum에서 각 멤버의 숫자 값으로 해당 멤버 이름을 찾을 수 있도록, enum 객체가 자동으로 양방향 매핑(값 → 이름, 이름 → 값)을 생성하는 기능입니다.
enum Colors { Red, // 0 Blue, // 1 Green, // 2 } // 내부적으로 아래와 같이 생성됨 { 0: "Red", 1: "Blue", 2: "Green", Red: 0, Blue: 1, Green: 2, }
-
문자열 enum: 문자열 enum은 역매핑 기능을 지원하지 않습니다.
-
enum은 타입과 값의 네임스페이스를 제공하고, 숫자 enum에서는 역매핑 기능이 있어 값을 통해 이름을 찾을 수 있지만, 객체는 단순한 상수 집합으로 트리 쉐이킹 등에 유리한 장점이 있습니다.
우형 enum 어떻게 사용할까?
Q) 팀 내에서 enum
과 유니온 타입을 사용하나요?
-
A팀
- 유니온 타입은 내가 어떤 타입을 가졌는지 전부 기억해야 하고, 변경이 필요하면 사용되는 곳을 모두 찾아서 바꿔야 할 때가 있음. 특히
string
타입의 유니온 타입은 리팩터링하기에 번거로운 점이 많은 것 같음. 또한 유니온 타입은 타입이니까 순회가 안되지만,enum
은 값이기 때문에이터러브 해서 순회 가능한 장점도 있음. 그래서enum
사용 enum
은 정의부를 바꾸면 알아서 사용하는 쪽에서도 변경되서 편함. 그래서 넓은 범위에 확장해서 써야 한다면enum
을 사용 중- 단,
enum
은 트리쉐이킹이 되지 않아 번들 사이즈에 영향을 줄 수 있지만,const enum
을 사용하면 해결할 수 있음.(사실enum
을 사용한다고 해서 전체 파일의 번들 사이즈가 서비스에 영향을 미칠정도로 커지지 않아서 고민하고 있지 않음)
- 유니온 타입은 내가 어떤 타입을 가졌는지 전부 기억해야 하고, 변경이 필요하면 사용되는 곳을 모두 찾아서 바꿔야 할 때가 있음. 특히
-
B팀
- TS에서 타입을 선언하는 용도로
enum
이 있어야 하는지 잘 모르겠음. enum
이 들어갈 수 있는 값을 타입으로 강제해놓고 객체로 만들어야 맞지 않나 하는 생각을 많이 함.enum
은 타입을 위한 문법이라기보다 개발을 위한 문법 같음.enum
의 기능이 TS 컴파일러에 의해 동작하는 것이 이상하게 느껴짐. 예를 들어enum
의 리버스 매핑 기능은 컴파일러에서 처리되면 안되는 동작이라고 생각함. 그래서 사용하지 않음
- TS에서 타입을 선언하는 용도로
-
C팀
enum
사용하기에는 되게 편한데 JS 컴파일될 때 IIFF로 바뀌는게 크진 않지만, 성능에 영향을 줄 수 있다는 것을 알게되어서 사용하고 있지 않음.
Q) enum외에 const enum을 사용하나요?
-
A팀
enumaration(열거)
폴더를 만들어 사용. 이 폴더에 정의한enum
을 외부에서 전역적으로 참조할 때는const enum
을 사용const enum
은 빌드과정에서 참조 값만 남기기 때문에 트리쉐이킹이 된다는 장점있음enum
도 상수를 쓰기 위한 것으로 생각하기 때문에const enum
을 사용하는게 적절하다고 판단함- 물론
isolate
모드를 활성화하고const enum
을 쓰면 안된다는 의견도 있지만, 현재로서는 더 명확하고 정적인 값을 사용할 수 있다는 장점이 더 크다고 생각함
-
B팀
const enum
사용하지 않음.const enum
은enum
과 다르게 직접적인 값으로 치환되기 때문에 전체 네임스페이스에 접근하지 못하고 순회할 수도 없다는 단점을 가지고 있음
트리쉐이킹(tree-shaking)
JS, TS에서 사용하지 않는 코드를 삭제 하는 방식. ES6 이후의 최신 APP 개발 환경에서 웹팩, 롤업 같은 도구로 번들링 작업을 수행할 때 사용하지 않는 코드는 자동으로 삭제 됨.
CommonJS는 트리쉐이킹을 지원하지 않지만, ES6 이후에는 파일 내 특정 모듈만 임포트하면 해당 모듈을 사용하지 않는 파일 코드는 삭제되어 더 작은 크기의 번들링 파일을 생성할 수 있게 되었음
타입을 확인 하는 방법
-
TS에서
typeof
, instanceof` 그리고 타입 단언을 사용해 타입을 확인 할 수 있음 -
typeof
는 연산하기 전에 피연산자의 데이터 타입을 나타내는 문자열을 반환한다. 반환하는 값은 JS의 7가지 기본 데이터 타입(Boolean, null, undefined, Number, BigInt, String, Symbol)과 Function(함수), 호스트 객체 그리고 object 객체
가 될 수 있다.typeof 2022; // "number" typeof 'kay'; // "string" typeof true; // "boolean" typeof {}; // "object"
-
TS에는 값 공간과 타입 공간이 별도로 존재함.
-
TS에서
typeof
연산자도 값에서 쓰일 때와 타입에서 쓰일 때의 역할이 다름.interface Person { first: string; last: string; } const person: Person = { first: 'kay', last: 'ko' }; function email(options: { person: Person; subject: string; body: string }) {}
-
값에 사용된
typeof
는 JS 런타임의typeof
연산자가 됨const v1 = typeof person; // 'object' const v2 = typeof email; // 'string'
-
반면 타입에 사용된
typeof
는 값을 읽고 TS 타입을 반환함type T1 = typeof person; // 타입은 Person type T2 = typeof email; // 타입은 (options: { person: Person; subject: string; body: string; }) => void
-
JS Class 사용시
typeof
연산자 주의 사항class Developer { name: string; sleepingTime: number; constructor(name: string, sleepingTime: number) { this.name = name; this.sleepingTime = sleepingTime; } const d = typeof Developer; // 값이 'function' type T = typeof Developer; // 타입이 typeof Developer }
- JS의 Class는 결국 함수이기 때문에 값 공간에서
typeof Developer
의 값은function
이 됨 - 타입 공간에서
typeof Developer
의 반환 값은 조금 특이한데,type T
에 할당된Developer
는 인스턴스의 타입이 아니라,new
키워드를 사용할 때 볼 수 있는 생성자 함수이기 때문
const zid: Developer = new Developer('zig', 7); type ZigType = typeof zig; // Developer;
-
Developer
는Developer
타입의 인스턴스를 만드는 생성자 함수여서typeof Developer
타입 그 자체인typeof Developer
임 -
typeof Developer
를 풀어서 설명하면new (name: string, sleepingTime: number): Developer
-
JS에서
instanceof
연산자를 사용하면 프로토타입 체이닝 어딘가에 생성자의 프로토타입 속성이 존재하는지 판단할 수 있음 -
typeof
연산자처럼instanceof
연잔사의 필터링으로 타입이 보장된 상태에서 안전하게 값의 타입을 정제하여 사용할 수 있음let error = unknown; if (error instanceof Error) { shwAlertModal(error.message); } else { throw Error(error); }
- JS의 Class는 결국 함수이기 때문에 값 공간에서
-
TS에서 타입 단언을 통해 타입을 강제 할 수 있음(
as
사용) -
컴파일 단계에서는 타입 단언이 형 변환을 강제할 수 있지만, 런타임에서는 효력을 발휘하지 못함
const loaded_text: unknown; // 어딘가에서 unknown 타입 값을 전달받았다고 가정 const validateInputText = (text: string) => { if (text.length < 10) return '최소 10글자 이상 입력해야 합니다.'; return '정상 값 입니다.'; }; validateInputText(loaded_text as string); // as 키워드를 사용해서 string으로 강제 하지 않으면 TS 컴파일러 단계에서 에러 발생
2.3 원시 타입
Boolean
-
오직
true
와false
값만 할당할 수 있는boolean
타입 -
아래 코드에서
errorAction.type
과ERROR_TEXT
가 같은지 비교한 결괏값을boolean
타입으로 변환하는 함수이며 비교식의 결과도boolean
타입을 갖음 -
JS에서는
boolean
원시 값은 아니지만 형 변환을 통해true / false
로 취급되는Truthy / Falsy
값이 존재 함. 이 값은boolean
원시 값이 아니므로 TS에서도boolean
타입에 해당하지 않음const isEmpty: boolean = true; const isLoading: boolean = false; function isTextError(errorCode: ErrorCodeType): boolean { const errorAction = getErrorAction(errorCode); if (errorAction) { return errorAction.type === ERROR_TEXT; } return false; }