[7장] 코드를 작성하고 실행하기
이펙티브 타입스크립트 7장을 정리한 내용입니다.
작성일 2024.02.14
페이지가 생성된 시간 2024.10.01 21:01:05

0

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은 호환성을 위해 남겨져 있다. importexport를 사용하자

데코레이터

앵귤러 프레임워크나 애너테이션이 필요한 프레임워크를 사용하는 것이 아니라면 데코레이터는 사용하지 말자

요약

  • 열거형, 매개변수 속성, 트리플 슬래시 임포트, 데코레이터는 타입 정보를 제거한다고 자바스크립트가 되지는 않는다.
  • 열거형, 매개변수 속성, 트리플 슬래시 임포트, 데코레이터는 사용하지 말자

아이템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 코드를 디버깅하지 말자.
  • 소스맵을 사용해 런타임에 타입스크립트 코드를 디버깅하자.
  • 소스맵이 변환된 코드에 완전히 매핑되는지 확인하자.
  • 소스맵에 원본 코드가 그대로 포함되지는 않는지 확인하자. 공개되면 안 된다.
©2024 dlwl98
github
PostsAbout