이펙티브 타입스크립트 7장

정태호·2023년 9월 3일
0

타입스크립트

목록 보기
12/13
post-thumbnail

코드를 작성하고 실행하기

아이템 53. 타입스크립트 기능보다는 ESMAScript 기능을 사용하기

타입스크립트의 원칙 : 런타임 기능이 아닌 타입 기능만 발전시킨다.

아래의 기능들은 이 원칙이 세워지기 전에 이미 사용되고 있어 타입 공간과 값 공간의 경계를 혼란스럽게 만들기 때문에 사용하지 않는 것이 좋다.

열거형(enum)

enum Flavor { // const enum Flavor -> 상수 열거형
  VANILLA = 0,
  CHOCOLATE = 1,
  STRAWBERRY = 2,
}

단순히 값을 나열하는 것보다 실수가 적고 명확하기 때문에 좋지만 타입스크립트의 열거형은 상황에 따라 다르게 동작한다.

  • 위 예제에서 숫자 열거형에 0, 1, 2 외의 다른 숫자가 할당되면 위험하다.
  • 상수 열거형은 보통의 열거형과 달리 런타임에 완전히 제거된다.
    • 컴파일러는 Flavor.CHOCOLATE을 0으로 바꾼다.
  • preserveConstEnums 플래그를 설정한 상태의 상수 열거형은 보통의 열거형처럼 런타임 코드에 정보를 유지한다.
  • 문자열 열거형은 런타임의 타입 안전성과 투명성을 제공한다.
    • 그러나 다른 타입과 달리 구조적 타이핑이 아닌 명목적 타이핑을 사용한다.

구조적 타이핑은 구조가 같으면 할당이 허용되는 반면 명목적 타이핑은 타입의 이름이 같아야 할당이 허용된다

const enum Flavor {
    VANILLA = 'vanilla',
    CHOCOLATE = 'chocolate',
    STRAWBERRY = 'strawberry',
}

let flavor = Flavor.CHOCOLATE; // 타입이 Flavor
	flavor = 'strawberry';
	// "strawberry' 형식은 'Flavor'형식에 할당될 수 없습니다.

function scoop(flavor: Flavor)

scoop('vanilla'); // 자바스크립트에서는 정상

scoop('vanilla'); // 타입스크립트에서는 에러 'vanilla' 형식은 'Flavor' 형식의 매개변수에 할당될 수 없습니다

자바스크립트와 타입스크립트에서 동작이 다르기때문에 문자열 열거형은 사용하지 않는 것이 좋다.

열거형 대신 리터럴 타입의 유니온을 사용하자.

type Flavor = 'vanilla' | 'chocolate' | 'strawberry';

let flavor: Flavor = 'chocolate'; //정상

function scoop(flavor: Flavor) {
    if (flavor === 'v
    				// 자동완성이 'vanilla'를 추천합니다.
    }

매개변수 속성

// 자바스크립트
class Person {
    name: string;
    constructor(name: string){
    	this.name = name;
    }
}

// 타입스크립트 (매개변수 속성)
class Person {
	constructor(public name: string){}
}
  • 타입스크립트 다른 패턴들과 이질적이다.
  • 일반 속성과 같이 사용하면 설계가 혼란스러워지기 때문에 한 가지만 사용하자.

네임스페이스와 트리플 슬래시 임포트

  • 타입스크립트 초창기 시절 모듈 시스템(임포트 하기 위한)
namespace foo {
function bar() {}
}
/// <reference path="other.ts"/>
foo.bar

트리플 슬래시 임포트와 module 키워드는 호환성을 위해 남아있을 뿐이니 ES6 스타일의 모듈 import, export를 사용하자.

데코레이터

  • 클래스, 메소드, 속성등에 애너테이션을 붙이거나 기능을 추가하는데 사용할 수 있다.

현재까지도 표준화가 완료되지 않았기 때문에, 앵귤러 등 애너테이션이 필요한 프레임워크를 사용 중이 아니라면 표준이 되기 전까지는 사용하지 않는 것이 좋다.

요약

  • 일반적으로 타입스크립트 코드에서 모든 타입 정보를 제거하면 자바스크립트가 된다.
    • 열거형, 매개변수 속성, 트리플 슬래시 임포트, 데코레이터는 타입 정보를 제거한다고 자바스크립트가 되지 않는다.
  • 타입스크립트 역할을 명확하게 하기 위해 사용하지 말자!

아이템 54. 객체를 순회하는 노하우

const obj= {
    one: 'uno',
    two: 'dos',
    three: 'tres',
}

for(const k in obj) {
	const v = obj[k];  // error -> k의 타입이 string 
  // obj에 인덱스 시그니처가 없기 때문에 엘리먼트는 암시적으로 'any' 타입입니다.
}

k의 타입은 string 이지만, obj 객체는 one,two,three 세 개의 키만 존재하기 때문에 타입이 서로 다르게 추론되어 오류가 발생한다.

오류 발생 이유?

interface ABC {
  a: string;
  b: string;
  c: number;
}

function foo(abc: ABC){
  for(const k in ABC){ // k는 string
    const v = abc[k] // 'ABC" 타입에 인덱스 시그니처가 없기 때문에 엘리먼트는 암시적 'any'가 됩니다.
  }
}

const x = {a: 'a', b: 'b', c: 2, d: new Date()};
foo(x)

foo 함수는 x 객체로 호출이 가능하다. ABC 타입에 할당 가능한 어떠한 값이든 매개변수로 허용하기 때문이다. (구조적 타이핑)

그렇기 때문에 타입스크립트는 ABC 타입의 키를 string 타입으로 선택한다.

해결방법

k의 타입을 구체적으로 명시하기

function foo(abc: ABC) {
  let k: keyof ABC;
  for (k in abc) {
    const v = abc[k]; // 타입이 string | number
  }
}

이전 예제처럼, d 속성은 Date 타입뿐만 아니라 어떠한 타입이든 될 수 있기 때문에 v가 string | number 타입으로 추론된 것은 잘못된 것이다.

Object.entries

  • 단지 객체의 키와 값을 순회하고 싶다면 Object.entries를 사용하자
function foo(abc: ABC) {
  for(const [k, v] of Object.entries(abc)){
    k // string
    v // any
  }
}

keyof 선언은 상수이거나 추가적인 키 없이 정확한 타입을 원하는 경우, Object.entries는 더욱 일반적으로 쓰이지만 키와 값의 타입을 다루기 어렵다.

요약

  • 함수의 매개변수로 쓰이는 객체에는 추가적인 키가 존재할 수 있다는 점을 명심하자.

아이템 55. DOM 계층 구조 이해하기

DOM 계층은 웹브라우저에서 자바스크립트를 실행할 때 어디에서나 존재한다.

타입스크립트에서는 DOM 엘리먼트의 계층 구조를 파악하기 용이하다. Element와, EventTarget에 달려 있는 Node의 구체적인 타입을 안다면 타입 오류를 디버깅 하기 쉽고 언제 타입 단얼을 사용해야 할지 알수 있다.

제목1제목2
EventTargetwindow, XMLHttpRequest
Nodedocument, Text, Comment
ElementHTMLElement, SVGElement 포함
HTMLElement<i>, <b> ...
HTML(---)Element<button>, <p> ...

EventTarget

  • DOM 타입 중 가장 추상화된 타입
  • 이벤트 리스너 추가 및 제거, 이벤트 보내기만 가능하다.

Node

  • Element 의 상위 타입. text와 주석도 Node이다.

Element

  • HTMLElement 와 SVGElement 로 구성되어있다.

HTML---Element

  • 이 타입의 특정 엘리먼트들은 자신만의 고유 속성을 가지고 있다.
  • ex) ImageElement의 src 속성, HTMLInputElement 의 value 속성 등
  • 각 엘리먼트의 고유 속성에 접근하기 위해서 구체적으로 타입을 지정해야 한다.
document.getElementsByTagName('p')[0]; //HTMLParagraphElement
document.createElement('button') //HTMLButtonElement
document.querySelector('div') //HTMLDivElement | null

그러나 항상 정확한 타입을 얻을 수 있는 것은 아니다. 특히 document.getElementById에서 문제가 발생한다.

document.getElementById('my-div'); // HTMLElement 

타입 단언문은 지양해야 하지만 DOM 관련해서는 타입스크립트 보다 우리가 더 정확히 알고 있는 경우이므로 단언문을 사용해도 좋다.

document.getElementById('my-div') as HTMLDivElement;

// null 체크 -> if문을 사용해도 됨
const div = document.getElementById('my-div')!;

Event

Event 타입에도 별도의 계층 구조가 있고 각 타입마다 고유의 속성을 가지고 있다.

  • UIEvent: 모든 종류의 사용자 인터페이스 이벤트
  • MouseEvent: 마우스로부터 발생되는 이벤트
  • TouchEvent: 모바일 기기의 터치 이벤트
  • WheelEvent: 스크롤 휠로부터 발생되는 이벤트
  • KeyboardEvent: 키 누름 이벤트

요약

  • DOM 타입은 타입스크립에서 중요한 정보이다.
  • DOM 엘리먼트와 이벤트에는 구체적인 타입 정보를 사용하거나, 타입스크립트가 추론할 수 있도록 문맥 정보를 활용하자.

아이템 56. 정보를 감추는 목적으로 private 사용하지 않기

속성에 언더스코어(_)를 붙이는 것은 비공개라고 표시한 것뿐 클래스 외부로 공개되어 있다는 점을 주의하자!

타입스크립트 public, protected, private 접근 제어자 사용

  • 컴파일 시점에만 오류를 표시해 줄 뿐, 런타임에는 아무런 효력이 없다.
  • 단언문을 사용하면 타입스크립트 상태에서도 private 속성에 접근할도 수 있다.
class Foo {
	_private = 'secret123';
}
class Diary {
	private secret = 'cheated on my English test';
}

const diary = new Diary();
(diary as any).secret // 정상

즉, 정보를 감추기 위해 private를 사용하면 안된다.

정보를 은닉하기위해선 클로저를 사용해야 한다.

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'); // 결과는 true
  • 장점) Passwordchecker 생성자 외부에서 passwordHash 변수에 접근할 수 없다 -> 정보 은닉 성공
  • 단점) passwordHash에 접근하는 메서드 역시 내부에 작성되어야하고, 인스턴스가 생성될 때마다 메서드의 복사본이 생성됨 -> 메모리 낭비 발생

현재 표준화 진행중인 정보 은닉 방법
비공개 필드 기능은 접두사로 #을 붙여 타입 체크와 런타임 모두에서 비공개로 만드는 역할을 한다.

class Passwordchecker {
    #passwordHash: number;
    constructor(passwordHash: number) {
    	this.#passwordHash = passwordHash;
    }
    checkPassword(password: string) {
    	return hash(password) === this.#passwordHash;
    }
}
const checker = new Passwordchecker(hash('s3cret1'));
checker.checkPassword('secret'); // 결과는 false
checker.checkPassword('s3cret'); // 결과는 true

#passwordHash 속성은 클래스 외부에서 접근할 수 없으며, 클로저와 다르게 클래스 메서드나 동일한 클래스의 개별 인스턴스끼리는 접근이 가능하다.

요약

  • 접근 제어자는 타입 시스템에서만 강제될 뿐이며 런타임에는 소용이 없다. 접근 제어자로 데이터를 감추려고 하지 말자!
  • 확실히 데이터를 감추고 싶으면 클로저를 사용하자.

아이템 57. 소스맵을 사용하여 타입스크립트 디버깅하기

타입스크립트 코드를 실행한다는 것은, 엄밀히 말하자면 타입스크립트 컴파일러가 생성한 자바스크립트 코드를 실행한다는 것이다.

변환된 자바스크립트코드는 복잡해서 디버깅하기 어렵다. -> 이를 해결하기위해 Source Map 이 만들어졌다.

타입스크립트에서의 소스맵 생성 설정

// tsconfig.json
{
  "compilerOptions": {
	"sourceMap": true
  }
}

이제 컴파일을 실행하면 각 .ts 파일에 대해 .js.js.map 두 개의 파일을 생성한다. .js.map 파일이 소스맵이다.

Source Map

  • 변환된 코드의 위치와 코드를 매핑함
  • 원본코드가 그대로 유출되지 않도록 주의해야함

요약

  • 원본 코드가 아닌 변환된 자바스크립트 코드를 디버깅하지 말자. 소스맵을 사용해 런타임에 타입스크립트 코드를 디버깅 하자.
  • 소스맵이 최종적으로 변환된 코드에 완전히 매핑되었는지 확인하자.
  • 소스맵에 원본 코드가 그대로 포함되도록 설정되어 있을 수도 있다. 공개되지 않도록 설정을 확인해야 한다.

타입 체커가 디버거를 대체할 수는 없다. 소스맵을 사용해서 제대로 된 타입스크립트 디버깅 환경을 구축하자!

profile
주니어 프론트엔드 개발자가 되고 싶습니다!

0개의 댓글