[7장] 코드를 작성하고 실행하기이펙티브 타입스크립트 7장을 정리한 내용입니다.작성일 2024.02.14페이지가 생성된 시간 2024.11.16 01:32:32
7장 코드를 작성하고 실행하기
목표: 코드를 작성하고 실행하면서 실제로 겪을 수 있는 문제를 알아보자
아이템53
타입스크립트의 기능보다는 ECMAScript 기능을 사용하기
과거 자바스크립트의 부족한 점을 타입스크립트는 독립적으로 개발해 보완했다.
시간이 흐르며 자바스크립트는 내장 기능을 추가했고, 이는 타입 공간과 값 공간의 경계를 혼랍스럽게 한다.
어떤 점을 유의해야 호환성 문제를 일으키지 않는지 알아보자.
열거형(enum)
타입스크립트의 enum
enum Flavor {
VANILLA = 0,
CHOCOLATE = 1,
STRAWBERRY = 2,
}
타입스크립트 enum의 문제
- 앞 예제에서 0,1,2 이외의 숫자가 할당되면 위험하다
- 상수 열거형은 런타임에서 완전히 제거된다(앞 예제에서 const enum Flavor로 작성하면 Flavor.CHOCOLATE은 0이 된다?)
- preserveConstEnums 플래그를 설정하면 런타임에서 열거형 정보를 유지한다
- 문자열 열거형은 명목적 타이핑을 사용한다(구조적 타이핑이 아니라)
문자열 열거형의 명목적 타이핑 알아보기
자바스크립트와 타입스크립트의 동작이 다르다
enum Flavor {
VANILLA = 'vanilla',
CHOCOLATE = 'chocolate',
STRAWBERRY = 'strawberry',
}
let flavor = Flavor.CHOCOLATE; // 타입이 Flavor
flavor = 'strawberry';
// ~~~~~~ Type '"strawberry"' is not assignable to type 'Flavor'
// 명목적 타이핑으로 인해 할당이 되지 않는다?
function scoop(flavor: Flavor) {
/* ... */
}
scoop('vanilla'); // 'vanilla' 형식은 'Flavor' 형식의 매개변수에 할당될 수 없습니다.
scoop(Flavor.VANILLA); // 정상
따라서 문자열 열거형 대신 리터럴 유니온 타입을 사용하자
type Flavor = 'vanilla' | 'chocolate' | 'strawberry';
let flavor: Flavor = 'chocolate'; // OK
클래스 매개변수 속성
아래 두 코드는 동일한 기능을 한다
// 생성자 매개변수
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
}
// 매개변수 속성
class Person {
constructor(public name: string) {}
}
일관성 없이 쓰면 혼란스럽다
// BAD
class Person {
first: string;
last: string;
constructor(public name: string) {
[this.first, this.last] = name.split(' ');
}
}
클래스에 매개변수 속성만 존재한다면 클래스 대신 인터페이스로 만들고 객체 리터럴을 사용하는 것이 좋습니다.
class Person {
constructor(public name: string) {}
}
const p: Person = { name: 'Jed Bartlet' }; // OK
// 위처럼 매개변수 속성만 존재하는 경우에는 인터페이스로 만들자는 뜻 같습니다?
매개변수 속성과 일반 속성을 같이 쓰지 말고 한 가지만 사용하자!
네임스페이스와 트리플 슬래시 임포트
타입스크립트는 자체적인 모듈 시스템으로 module
키워드와 트리플 슬래시
임포트를 사용했다.
ECMAScript 2015가 공식적으로 모듈 시스템을 도입한 이후에는 module
과 같은 기능을 하는 namespace
키워드를 추가했다.
트리플 슬래시와 module
은 호환성을 위해 남겨져 있다. import
와 export
를 사용하자
데코레이터
앵귤러 프레임워크나 애너테이션이 필요한 프레임워크를 사용하는 것이 아니라면 데코레이터는 사용하지 말자
요약
- 열거형, 매개변수 속성, 트리플 슬래시 임포트, 데코레이터는 타입 정보를 제거한다고 자바스크립트가 되지는 않는다.
- 열거형, 매개변수 속성, 트리플 슬래시 임포트, 데코레이터는 사용하지 말자
아이템54
객체를 순회하는 노하우
함수는 할당 가능한 어떠한 값이든 매개변수로 허용하기 때문에 다음과 같은 일이 벌어진다
- 프로토타입 오염 가능성을 염두에 둔 것도 있다
interface ABC {
a: string;
b: string;
c: number;
}
function foo(abc: ABC) {
for (const k in abc) {
const v = abc[k];
// ~~~~~~ 'ABC' 타입에 인덱스 시그니처가 없기 때문에
// 엘리먼트는 암시적으로 'any'가 됩니다.
}
}
foo({ a: 'a', b: 'b', c: 2, d: new Date() }); // OK
// keyof 키워드를 사용하면 문제점
function foo(abc: ABC) {
let k: keyof ABC
for (k in abc) {
// let k: "a" | "b" | "c"
const v = abc[k] // Type is string | number
// '할당 가능한' 모든 값에 비해 타입이 너무 좁게 추론된다.
}
// 아래처럼 순회하자!
function foo(abc: ABC) {
for (const [k, v] of Object.entries(abc)) {
k // Type is string
v // Type is any
}
}
요약
- 객체를 순회할 때 키가 어떤 값인지 정확하게 파악하고 있다면 keyof를 쓰자
- 객체를 순회하는 일반적인 방법은
Object.entries
를 사용하는 것이다
아이템55
DOM 계층 구조 이해하기
DOM 계층구조 예시
HTMLParagraphElement < HTMLElement < Element < Node < EventTarget
계층구조에 따른 예시
- EventTarget : window, XMLHttpRequest (DOM 타입 중 가장 추상화된 타입)
- Node : document, Text, Comment
- Element: HTMLElement, SVGElement 포함
- HTMLElement:
<i>
,<b>
- HTMLButtonElement:
<button>
구체적으로 DOM 관련 타입을 지정해야 오류 없이 속성에 접근할 수 있다.
리터럴 값을 사용하면 정확한 타입을 얻을 수 있다.
document.getElementsByTagName('p')[0]; // HTMLParagraphElement
document.createElement('button'); // HTMLButtonElement
document.querySelector('div'); // HTMLDivElement
우리가 타입스크립트보다 더 정확히 알고있는 경우 단언문을 사용해도 좋다
document.getElementById('my-div') as HTMLDivElement;
Event 타입도 별도의 계층구조가 있으면 Event는 가장 추상화된 이벤트이다.
// clientX, clientY는 MouseEvent 타입에 있다..
function handleDrag(eDown: Event) {
// ...
const dragStart = [eDown.clientX, eDown.clientY];
// ~~~~~~~ Property 'clientX' does not exist on 'Event'
// ~~~~~~~ Property 'clientY' does not exist on 'Event'
// ...
}
요약
- DOM 엘리먼트와 이벤트에는 충분히 구체적인 타입 정보를 사용하자
- 타입스크립트가 추론할 수 있도록 문맥 정보를 활용하자
아이템56
정보를 감추는 목적으로 private 사용하지 않기
타입스크립트의 private 필드
런타임에서 동작하지 않으므로 우회해 접근할 수 있다 => 데이터가 감춰지지 않는다
class Diary {
private secret = 'cheated on my English test';
}
const diary = new Diary();
diary.secret; // 접근 불가능
(diary as any).secret; // 단언을 통해 우회하여 접근이 가능
클로저를 사용해 정보를 숨기기
외부에서 passwordHash에 접근이 불가능하다
declare function hash(text: string): number;
class PasswordChecker_ {
checkPassword: (password: string) => boolean;
constructor(passwordHash: number) {
this.checkPassword = (password: string) => {
return hash(password) === passwordHash;
};
}
}
const checker = new PasswordChecker(hash('s3cret'));
checker.checkPassword('s3cret'); // Returns true
생성자 내부에서 메서드를 정의할 경우 인스턴스를 생성할 때마다 메서드의 복사본이 생성되므로 메모리가 낭비된다.
또한 동일한 클래스 인스턴스끼리 비공개 데이터에 접근이 불가능하다.
접두사 '#'
메모리 낭비 X
동일한 클래스 인스턴스끼리 비공개 데이터에 접근 O
declare function hash(text: string): number;
class PasswordChecker {
#passwordHash: number;
constructor(passwordHash: number) {
this.#passwordHash = passwordHash;
}
checkPassword(password: string) {
return hash(password) === this.#passwordHash;
}
}
요약
- public, protected, private 접근 제어자는 런타임에는 소용이 없다. 접근 제어자로 데이터를 감추려고 하지 말자.
- 확실히 데이터를 감추고 싶다면 클로저를 사용하자.
아이템57
소스맵을 사용하여 타입스크립트 디버깅하기
디버깅은 런타임에 동작하며, 디버깅하면 보게 되는 코드는 전처리기, 컴파일러, 압축기를 거친 자바스크립트 코드일 것이다.
이 코드는 복잡해 디버깅하기 어렵다.
디버깅 문제를 해결하기 위해 브라우저 제조사들은 서로 협력해 소스맵이라는 해결책을 내놓았다.
소스맵은 변환된 코드의 위치와 심벌들을 원본 코드의 원래 위치와 심벌들로 매핑한다.
타입스크립트가 소스맵을 생성할 수 있게 하려면 tsconfig.json에서 sourceMap 옵션을 설정하면 된다.
.js.map 파일이 소스맵이다.
요약
- 원본 코드가 아닌 변환된 js 코드를 디버깅하지 말자.
- 소스맵을 사용해 런타임에 타입스크립트 코드를 디버깅하자.
- 소스맵이 변환된 코드에 완전히 매핑되는지 확인하자.
- 소스맵에 원본 코드가 그대로 포함되지는 않는지 확인하자. 공개되면 안 된다.