스터디/이펙티브 타입스크립트

[이펙티브 타입스크립트] 7장 코드를 작성하게 실행하기 아이템53~55

paran21 2024. 6. 22. 23:50

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

타입스크립트 개발 초기에는 자바스크립트에 클래스, 데코레이터, 모듈 시스템 같은 기능이 없어 독립적으로 이러한 기능들을 개발하였다.

자바스크립트에서 이러한 기능들이 새로 추가되면서 타입스크립트의 초기 버전에서 독립적으로 개발했던 기능과 호환성 문제를 발생시켰다.

 

타입스크립트는 초기 버전과의 호환성을 포기하고 자바스크립트의 신규 기능을 그대로 채택하였다.

자바스크립트는 런타임 기능을, 타입스크립트는 타입 기능만 발전시킨다는 명확한 원칙을 세웠다.

 

다음의 언급되는 기능들은 타입스크립트 초기에 개발된 기능들로 타입 공간과 값 공간의 경계를 혼란스럽게 만들기 때문에 가급적 사용하지 않는 것이 좋고, 불가피하게 사용한다면 유의해야 한다.

 

열거형(enum)

숫자 열거형

enum Flavor {
  VANILLA = 0, 
  CHOCOLATE = 1,  
  STRAWBERRY = 2,
}

 

- 0, 1, 2 외의 다른 숫자가 할당되면 매우 위험하다.

- 런타임에 완전히 제거된다(문자열 열거형과 다른 동작이다)

- preserveConstEnums를 사용하면 상수 열거형은 보통의 열거형처럼 런타임 코드에 상수 열거형 정보를 유지한다.

const enum Direction {
  Up = 1,
  Down,
  Left,
  Right
}

function move(direction: Direction) {
  console.log(direction);
}

move(Direction.Up); // 1 출력
move(Direction.Left); // 3 출력

 

아무 설정을 하지 않는다면 자바스크립트는 다음과 같이 컴파일된다.

function move(direction) {
    console.log(direction);
}

move(1); // 1 출력
move(3); // 3 출력

 

그러나 다음과 같이 preserveConstEnums를 설정하면 런타임에서도 열거형이 유지된다. 

{
  "compilerOptions": {
    "preserveConstEnums": true
  }
}
var Direction;
(function (Direction) {
    Direction[Direction["Up"] = 1] = "Up";
    Direction[Direction["Down"] = 2] = "Down";
    Direction[Direction["Left"] = 3] = "Left";
    Direction[Direction["Right"] = 4] = "Right";
})(Direction || (Direction = {}));

function move(direction) {
    console.log(direction);
}

move(Direction.Up); // 1 출력
move(Direction.Left); // 3 출력

 

문자열 열거형

- 런타임의 타입 안전성과 투명성을 제공한다.

- 구조적 타이핑이 아니라 타입의 이름이 같아야 할당이 허용되는 명목적 타이핑을 사용한다.

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

// 타입이 Flavor
let flavor = Flavor.CHOCOLATE;   

❌'"strawberry"' 형식은 'Flavor' 형식에 할당될 수 없습니다.
flavor = 'strawberry';

 

Flavor는 런타임 시점에는 문자열이기 때문에 자바스크립트와 타입스크립트의 동작이 다르다.

function scoop(flavor: Flavor) { ... }

//자바스크립트 🟢
//타입스크립트 ❌
//'"vanilla"' 형식은 'Flavor' 형식의 매개변수에 할당될 수 없습니다.
scoop('vanilla');

 

리터럴 타입의 유니온

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

 

열거형보다는 리터럴 타입의 유니온을 권장한다

열거형만큼 안전하며 자바스크립트와 호환되고 편집기에서 자동완성 기능을 사용할 수 있다.

 

 

매개변수 속성

타입스크립트에서는 클래스를 초기화할 때 매개변수 대신 간결한 속성을 제공한다.

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

//타입스크립트 매개변수 속성
class Person {
  constructor(public name: string) {}
}

//구조적 타이핑 때문에 가능하다.
const p: Person = {name: 'Jed Bartlet'};

 

- 일반적으로 타입스크립트 컴파일은 타입 제거가 이루어지므로 코드가 줄어들지만, 매개변수 속성은 코드가 늘어나는 문법이다.

- 매개변수 속성은 런타임에는 실제로 사용되지만, 타입스크립트 관점에서는 사용되지 않는 것처럼 보입니다.

- 매개변수 속성과 일반 속성을 섞어서 사용하면 클래스의 설계가 혼란스러워진다.

 

클래스에 매개변수 속성만 존재한다면 클래스 대신 인터페이스로 만들고 객체 리터럴을 사용하는 것이 좋다.

interface Person {
  name: string;
}

const person: Person = {
  name: 'Jeb Bartlet'
};

 

매개변수 속성은 타입스크립트의 다른 패턴들과 이질적이고, 초보자에게 생소한 문법이다.

매개변수 속성을 사용한다면 한 가지만 사용하는 것이 좋다.

 

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

ECMAScript 2015 이전에 자바스크립트에 공식적인 모듈 시스템이 없었기 때문에 타입스크립트는 자체적으로 module 키워드와 트리플 슬래시 임포트를 사용했다.

//ECMAScript 2015에서 모듈시스템을 도입한 이후 타입스크립트는 충돌을 피하기 위해 namespace 키워드를 추가했다.
//https://www.typescriptlang.org/ko/docs/handbook/namespaces.html
namespace foo {
  functionn bar() {}
}

//트리플 슬래시 임포트
/// <reference path="shapes.ts" />

 

트리플 슬래시 임포트와 module 키워드는 호환성을 위해 남아 있을 뿐이며, ECMAScript 2015 스타일의 모듈(import와 export)를 사용해야 한다.

 

데코레이터

데코레이터는 클래스, 메서드, 속성에 어노테이션을 붙이거나 기능을 추가하는 데 사용할 수 있다.

{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}
// 메서드 데코레이터 정의
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    console.log(`Method ${propertyKey} called with args: ${JSON.stringify(args)}`);
    return originalMethod.apply(this, args);
  };

  return descriptor;
}

// 클래스에 메서드 데코레이터 적용
class Calculator {
  @log
  add(a: number, b: number): number {
    return a + b;
  }
}

 

데코레이터는 앵귤러 프레임워크를 지원하기 위해 추가되었다.

앵귤러를 사용하거나 애너테이션이 필요한 프레임워크를 사용하고 있는게 아니라면, 데코레이터가 표준이 되기 전에는 타입스크립트에서 데코레이터를 사용하지 않는 게 좋다.

 

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

interface ABC {
  a: string;
  b: string;
  c: number;
}
function foo(abc: ABC) {
  // const k: string
  for (const k in abc) {
    // ❌ obj에 인덱스 시그니처가 없기 때문에 엘리먼트는 암시적으로 'any' 타입입니다.
    const v = abc[k];
  }
}

 

객체를 순회할 때 for문 안에 const k는 string이고, ABC 타입에는 'a', 'b', 'c' 세 개의 키만 존재하기 때문에 타입이 달라 오류가 발생한다.

// 🟢 구체적으로 key 타입을 명시하면 오류가 사라진다.
let k: keyof typeof obj;
for (k in obj) {
  const v = obj[k];       
}

 

ABC 타입에 할당 가능한 객체에는 a, b, c 외에 다른 속성이 존재할 수 있기 때문에 타입스크립트는 ABC 타입의 키를 string으로 선택한다.

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

 

keyof 선언은 상수이거나 추가적인 키 없이 정확한 타입을 원하는 경우에 적절하다.

 

keyof를 사용하면 k와 v가 너무 좁은 타입으로 할당된다.

d: new Date()와 같이 다른 타입도 할당될 수 있기 때문에 이는 잘못된 타입 추론이다.

 

단지 객체의 키와 값을 순회하고 싶다면 Object.entries를 사용하면 된다.

대신 키와 값의 타입을 다루기 까다롭다.

function foo(abc: ABC) {
  for (const [k, v] of Object.entries(abc)) {
  k  // string 타입
  v  // any 타입  
  }
}

 

객체를 다룰 때는 프로토타입 오염의 가능성을 염두에 두어야 한다.

for-in 구문을 사용하면, 객체의 정의에 없는 속성이 갑자기 등장할 수 있다.

❌ Object.prototype에 순회 가능한 속성을 절대 추가하면 안된다.
Object.prototype.z = 3; 
const obj = {x: 1, y: 2};
for (const k in obj) { console.log(k); }

 

만약 for-in 루프에서 k가 string 키를 가지게 된다면 프로토타입 오염 가능성을 의심해봐야 한다.

 

 

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

타입스크립트에서는 DOM 엘리먼트의 계층 구조를 파악하기 용이하다.

 

EventTarget은 DOM 타입 중 가장 추상화된 타입으로, 이벤트 리스너를 추가하거나 제거하고, 이벤트를 보내는 것밖에 할 수 없다.

Node 타입에는 텍스트 조각과 주석이 있다.

Element에는 SVG 태그의 전체 계층 구조를 포함하면서 HTML이 아닌 엘리먼트인 SVGElement가 포함된다.

HTMLxxxElement는 자신만의 고유한 속성을 가지고 있다.

HTMLImageElement에는 src가, HTMLInputElement에는 value 속성이 있다.

이런 속성에 접근하려면 상당히 구체적으로 타입을 지정해야 한다.

 

일반적으로 타입 단언문은 지양해야 하지만, DOM과 관련해서는 타입스크립트보다 우리가 더 정확히 알고 있는 경우라면 단언문을 사용해도 된다.

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

 

Event는 가장 추상화된 이벤트이다.

function handleDrag(eDown: Event) {
  // ...  
  const dragStart = [    
    // ❌ 'Event'에 'clientX' 속성이 없습니다.
    // ❌ 'Event'에 'clientY' 속성이 없습니다.
    eDown.clientX, eDown.clientY
  ];  
  // ...
}

 

실제 사용할 때는 MouseEvent와 같이 구체적인 타입으로 선언

mousedown 이벤트 핸들러를 인라인 함수로 만들면 타입스크립트는 더 많은 문맥 정보를 사용할 수 있기 때문에 대부분의 오류를 제거할 수 있다.

function addDragHandler(el: HTMLElement) {
  //'mousedown' 이벤트 핸들러를 통해 문맥 정보를 제공한다.
  el.addEventListener('mousedown', eDown => {
    const dragStart = [eDown.clientX, eDown.clientY];
    // MouseEvent로 타입을 구체적으로 선언한다.
    const handleUp = (eUp: MouseEvent) => {
      el.classList.remove('dragging');
      el.removeEventListener('mouseup', handleUp);
      const dragEnd = [eUp.clientX, eUp.clientY]; 
      console.log('dx, dy = ', [0, 1].map(i => dragEnd[i] - dragStart[i]));
    }   
  el.addEventListener('mouseup', handleUp);  
  });
}