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; // okinterface 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 zigDevelop클래스가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는 값과 타입이 함께 사용됨
값과 타입 공간에 동시에 존재하는 심볼
-
Classclass 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; }