이펙티브 타입스크립트 7장 ( Item 53 ~ 57 )
포스트
취소

이펙티브 타입스크립트 7장 ( Item 53 ~ 57 )

해당 포스트는 이펙티브 타입스크립트 7장을 읽고 정리한 포스트입니다.
책의 모든 내용을 작성하는 것이 아닌 주관적인 기준에 따라 필요한 정보만 정리했습니다.

📖 7장 코드를 작성하고 실행하기

초기 JavaScript에 없던 기능들을 TypeScript에서 독립적으로 만들었고, 이후에 JavaScript에서 다른 방식으로 표준이 돼서 호환성에 문제가 생기는 부분들에 대해 알아보는 챕터입니다.
TypeScriptJavaScript의 호환성을 포기했기 때문에 발생하는 혼란스러운 기능들에 대해 알아보겠습니다.

📌 Item 53 ( 열거형(enum) )

  • 특징
    1. 숫자 열거형은 0부터 시작하는 것이 좋음 ( TODO: 이유 알아보기 )
    2. 상수 열거형(const eum)은 런타임에 완전히 제거됨
      ( preserveConstEnumstrue인 경우 상수 열거형의 정보 저장 )
    3. 문자형 열거형은 명목적 타이핑을 사용함

( 근데 이상하게 tsconfig.json에서 preserveConstEnums를 설정하면 안되고 --preserveConstEnums를 사용하면 되네요… 🥲 )
( npx tsc 파일명과 같은 명령어를 실행하면 tsconfig.json을 사용하지 않는다고 합니다… ( npx tsc 사용하기 ) )

0️⃣ 상수 열거형 ( const enum )

일반적으로 상수 열거형은 런타임에 제거됩니다.
단, preserveConstEnums를 적용하면 const enum도 런타임에 제거되지 않습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 상수 열거형
const enum Flavor {
  VANILLA,
  CHOCOLATE,
  STRAWBERRY,
}

let flavor = Flavor.CHOCOLATE;

// 일반 열거형
enum Flavor {
  VANILLA,
  CHOCOLATE,
  STRAWBERRY,
}

let flavor = Flavor.CHOCOLATE;
1
2
3
4
5
6
7
8
9
10
11
// 상수 열거형 ( 컴파일 )
var flavor = 1 /* Flavor.CHOCOLATE */;

// 일반 열거형 ( 컴파일 )
var Flavor;
(function (Flavor) {
  Flavor[Flavor["VANILLA"] = 0] = "VANILLA";
  Flavor[Flavor["CHOCOLATE"] = 1] = "CHOCOLATE";
  Flavor[Flavor["STRAWBERRY"] = 2] = "STRAWBERRY";
})(Flavor || (Flavor = {}));
var flavor = Flavor.CHOCOLATE;

1️⃣ 문자형 열거형과 명목적 타이핑

enum구조적 타이핑이 아닌 명목정 타이핑을 따르기 때문에 (1)에서 에러가 발생하게 됩니다.
따라서 교재에서는 enum을 문자로 사용하지 않는 것을 권장합니다.

1
2
3
4
5
6
7
8
9
10
enum Flavor {
  VANILLA = "VANILLA",
  CHOCOLATE = "CHOCOLATE",
  STRAWBERRY = "STRAWBERRY",
}

const func = (flavor: Flavor) => flavor;

func(Flavor.CHOCOLATE);
func("CHOCOLATE"); // (1) '"CHOCOLATE"' 형식의 인수는 'Flavor' 형식의 매개 변수에 할당될 수 없습니다.

리터럴 타입의 유니온을 이용해서 대신 처리할 수 있습니다.

1
2
3
4
5
type Flavor = "VANILLA" | "CHOCOLATE" | "STRAWBERRY";

const func = (flavor: Flavor) => flavor;

func("CHOCOLATE"); // 타입 추론이 되기 때문에 자동완성도 지원해줌

2️⃣ 매개변수 속성

(2)(3) 모두 같은 방식으로 동작합니다.
( (3)처럼 사용하는 것이 매개변수 속성 방식입니다. )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// (2)
class Person1 {
  name: string;

  constructor(name: string) {
    this.name = name;
  }
}

// (3)
class Person2 {
  constructor(public name: string) {
    this.name = name;
  }
}
  • 매개변수 속성의 문제점
    1. 컴파일 시 코드가 늘어남
    2. 일반 속성과 섞어서 사용하면 혼란이 발생

제가 느끼기엔 생소하고 굳이 사용해야 할 필요성이 느껴지지 않아서 사용하지 않을 것 같습니다.

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

TODO:

4️⃣ 데코레이터

TODO:

🎊 Item 53 결론

  1. 일반적인 TypeScript 코드에서 모든 타입 정보를 제거하면 JavaScript가 되지만, 열거형, 매개변수 속성, 트리플 슬래시 임포트, 데코레이터는 타입 정보를 제거한다고 JavaScript가 되지 않음
  2. 열거형, 매개변수 속성, 트리플 슬래시 임포트, 데코레이터를 사용하지 않는 것이 좋음

📌 Item 54 ( 객체를 순회하는 노하우 )

해당 아이템에서 아래 코드를 모두 사용한다고 가정하고 설명하겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
type Person = {
  name: string;
  age: number;
  gender: boolean;
};

declare const person: Person;
const strangePerson = {
  age: 1,
  gender: true,
  name: "alice",
  birth: new Date(),
};

0️⃣ for ~ in 으로 객체 순회하는 방법

일반적으로 아래와 같이 작성하면 오류가 나는데 처음 만나면 이게 왜 오류인지 이해할 수 없을 수 있습니다.

1
2
3
4
5
6
const func = (person: Person) => {
  let key: keyof typeof person;
  for (key in person) {
    console.log(person[key]); // 'string' 형식의 식을 'Person' 인덱스 형식에 사용할 수 없으므로 요소에 암시적으로 'any' 형식이 있습니다.
  }
};

person이라는 객체는 const이기는 하지만 언제든지 property가 추가될 수 있기 때문에 keystring으로 추론됩니다.
또한 TypeScript구조적 타이핑을 따르기 때문에 Person에 정해진 key만 들어갈 것이라고 확신할 수 없습니다.
( 구조적 타이핑을 따르기 때문에 (1)과 같은 경우도 가능합니다. )

1
2
3
4
5
6
7
8
9
const func = (person: Person) => {
  let key: keyof typeof person;
  for (key in person) {
    console.log(person[key]);
  }
};

func(myPerson);
func(strangePerson); // (1)

(2)처럼 key의 타입을 명시적으로 작성하기 때문에 오류가 나지 않습니다.
혹은 (3)처럼 Object.keys(), Object.values(), Object.entries()를 사용해도 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const func1 = (person: Person) => {
  let key: keyof typeof person; // (2)
  for (key in person) {
    // v: string | number | boolean
    const v = person[key];
  }
};

const func2 = (person: Person) => {
  // (3)
  for (const [key, value] of Object.entries(person)) {
    // key: string
    // value: string | number | boolean
  }
};

func1(myPerson);
func2(strangePerson);

🎊 Item 54 결론

  1. 함수의 매개변수로 쓰이는 객체에는 property가 추가될 수 있음을 인지해야 함
  2. 객체를 순회할 때는 Object.keys(), Object.values(), Object.entries()를 사용하는 것이 일반적임

📌 Item 55 ( DOM 계층 구조 이해하기 )

0️⃣ DOM 계층 타입

  1. EventTarget: window, XMLHttpRequest
  2. Node: document, Text, Comment
  3. Element: HTMLElement, SVGElement
  4. HTMLElement: <i>, <b>
  5. HTML*Element: <button>, <div>

NodeTextComment는 텍스트 노드, 주석 노드를 의미합니다.

1
2
3
4
5
6
7
8
<html>
  <head></head>

  <body>
    <!-- 주석 노드 -->
    <p>🙂 텍스트 노드 🙂</p>
  </body>
</html>

1️⃣ Event 타입

  1. UIEvent: 모든 종류의 사용자 인터페이스 이벤트
  2. MouseEvent: 클릭처럼 마우스로부터 발생되는 이벤트
  3. TouchEvent: 모바일 기기의 터치 이벤트
  4. WheelEvent: 스크롤 휠을 돌려서 발생되는 이벤트
  5. KeyboardEvent: 키 누름 이벤트

이벤트 핸들러에서 리터럴로 선언하면 이벤트 타입을 정의하지 않아도 추론해줍니다.
하지만 따로 빼서 작성하는 경우는 이벤트 타입을 명시적으로 선언해야 합니다.

1
2
3
4
5
6
7
8
9
10
const clickHanlder = (e: MouseEvent) => {
  // 명시적으로 작성
  // e: MouseEvent
};

document.createElement("button").addEventListener("click", (e) => {
  // 알아서 추론
  // e: MouseEvent
});
document.createElement("button").addEventListener("click", clickHanlder);

2️⃣ DOM의 Element 가져오기

DOMElement를 가져오는 경우에는 타입이 명확하지 않은 경우가 더 많습니다.

직접 생성하는 경우는 명확하지만 가져오는 경우에는 TypeScript에서는 타입을 확신할 수 없습니다.

1
2
3
4
5
// $$button: HTMLButtonElement
const $$button = document.createElement("button");

// $button: HTMLButtonElement | null
const $button = document.querySelector("button");

저 같은 경우에는 타입 가드(instanceof)를 이용해서 타입 체커에게 타입을 알려주는 방법을 사용합니다.

1
2
3
4
5
6
7
8
9
(() => {
  const $button = document.querySelector("button");

  // 타입 가드
  if (!($button instanceof HTMLButtonElement))
    return alert("<button>이 존재하지 않습니다.");

  // $button: HTMLButtonElement
})();

🎊 Item 55 결론

  1. DOM의 타입 계층에 대해 공부하고 이해하기
  2. Node, Element, HTMLElement, EventTarget간의 차이점과 Event, MouseEvent 등의 차이점을 알아야 함
  3. DOM의 엘리먼트나 이벤트에 대해 추론할 수 있도록 문맥 정보를 활용해야 함

📌 Item 56 ( 정보를 감추는 목적으로 private 사용하지 않기 )

TypeScript에서는 접근 제어자를 이용해서 멤버 변수의 접근에 제한을 걸 수 있습니다.
( 접근 제어자는 public, protected, private이 있습니다. )

0️⃣ 접근 제어자

TypeScript의 접근 제어자는 컴파일 후에 모두 제거됩니다.
JavaScript에는 접근 제어자라는 문법이 존재하지 않기 때문입니다.

( TypeScript의 문법이 먼저 나오고 JavaScript의 문법이 다른 방법으로 표준으로 나와서 서로 맞지 않음 )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ts
class Person {
  private name: string;
  protected age: number;
  public gender: boolean;

  constructor(name: string, age: number, gender: boolean) {
    this.name = name;
    this.age = age;
    this.gender = gender;
  }
}
const person = new Person("Aatrox");
person.name;

// js
var Person = /** @class */ (function () {
  function Person(name, age, gender) {
    this.name = name;
    this.age = age;
    this.gender = gender;
  }
  return Person;
}());
var person = new Person("Aatrox");
person.name; // 일반적인 멤버이기 때문에 문제 없이 동작

1️⃣ 정보 감추는 방법

아래처럼 private는 타입 체커에 의해서만 즉, 컴파일 타임에서만 체크가 됩니다.
따라서 강제로 타입을 변환하면 접근할 수 있게 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
class Person {
  private name: string;

  constructor(name: string) {
    this.name = name;
  }
}

const person = new Person("Aatrox");

person.name; // Error: 'name' 속성은 private이며 'Person' 클래스 내에서만 액세스할 수 있습니다.
(person as any).name; // 정상 동작

이런 문제를 해결하기 위해 JavaScript의 비공개 필드 기능(#)을 이용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person {
  #name: string;

  constructor(name: string) {
    this.#name = name;
  }

  say() {
    console.log(this.#name);
  }
}

const person = new Person("Aatrox");

person.#name; // Error: '#name' 속성은 프라이빗 식별자를 포함하기 때문에 'Person' 클래스 외부에서 액세스할 수 없습니다.
(person as any).name; // 정상 동작
person.say();

// ts ( ... 나머지 생략 )
// 변환해도 결국 잘못된 접근이라 "JavaScript"에서 오류가 납니다.
person.;
person.name;
person.say();

🎊 Item 56 결론

  1. 접근 제한자는 TypeScript에서만 존재하고 강제적인 조건이 부여될 뿐이고, JavaScript에는 존재하지 않음
  2. 데이터를 감추려면 클로저나 비공개 필드 기능을 이용하기

📌 Item 57 ( 소스맵을 사용하여 타입스크립트 디버깅하기 )

소스맵이란 원본 코드와 변환 코드를 맵핑해주는 역할을 하는 것을 의미합니다.
디버깅을 위해서 사용하며 브라우저와 IDE에서 소스맵을 해석해줍니다.

소스맵 때문에 네트워크 비용이 추가로 발생할 수 있다고 생각할 수 있지만 브라우저의 디버거를 열기 전에는 소스맵을 받지 않기 때문에 문제가 되지 않습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// index.ts
const age: number = 26;

console.log(age);

// index.js ( "sourceMap": true )
"use strict";
const age = 26;
console.log(age);
// + index.js.map 생성됨

// index.js ( "inlineSourceMap": true )
"use strict";
const age = 26;
console.log(age);
//# sourceMappingURL=data:application/json;base64, ... 생략

🎊 Item 57 결론

  1. 변환된 코드 디버깅하지 말고 소스맵 사용하기
  2. 소스맵 공개하지 않기

📮 레퍼런스

  1. « 이펙티브 타입스크립트 7장 » ( 댄 밴더캄 지음, 장원호 옮김, 인사이트, 2021 )
  2. 1-blue - 구조적 타이핑
  3. 1-blue - 타입 가드(instanceof)
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.