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

History

TS가 태동하던 2010년경, JS는 결함이 많고 개선할 부분이 많은 언어였다. 그리고 클래스, 데코레이터, 모듈 시스템 같은 기능이 없어 프레임워크나 트랜스파일러로 보완하는 것이 일반적이었다. 따라서 TS도 초기엔 독립적으로 개발한 클래스, 열거형(enum), 모듈 시스템을 포함시킬 수 밖에 없었다.

시간이 흘러 TC39(JS를 관장하는 표준 기구)는 부족했던 점들을 대부분 내장 기능으로 추가했다. 그러나 JS에 새로 추가된 기능은 TS 초기에 독립적으로 개발된 기능과 호환성 문제를 발생시켰다. TS 진영은 다음 전략 중 하나를 선택해야 했다.

  • TS 초기 버전의 형태를 유지하기 위해 JS의 신규 기능을 끼워 맞추는 것

  • JS의 신규 기능을 그대로 채택해 TS 초기 버전과의 호환성을 포기하는 것 (대부분 두 번째 전략을 선택)

결국 TC39는 런타임 기능을, TS 팀은 타입 기능만 발전시킨다는 명확한 원칙을 세워 현재까지 지켜오고 있다.

그러나 이 원칙이 세워지기 이전에 이미 사용되던 몇 가지 기능이 있는데, 이 기능들은 타입 공간(TS)과 값 공간(JS)의 경계를 혼란스럽게 하므로 사용하지 않는게 좋다.

열거형(enum)

TS도 다른 언어처럼 enum이 존재한다.

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

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

Flavor     // 자동 완성 추천: VANILLA, CHOCOLATE, STRAWBERRY
Flavor[0]  // 값이 VANILLA

하지만 상황에 따라 다르게 동작한다.

  • 숫자 열거형(e.g., 앞의 예제 Flavor)은 0, 1, 2 외에 다른 숫자가 할당되면 매우 위험하다.

  • 상수 열거형은 보통의 열거형과 달리 런타임에서 완전히 제거된다. 앞의 예제를 const enum Flavor로 바꾸면, 컴파일러는 Flavor.CHOCOLATE을 0으로 만들어 버린다. 이런 결과는 기대하지도 않았으며, 문자열 열거형과 숫자 열거형과 전혀 다른 동작이다.

  • preserveConstEnums 플래그를 설정한 상태의 상수 열거형은 보통 열거형처럼 런타임 코드에 상수 열거형 정보를 유지한다.

  • 문자열 열거형은 런타임의 타입 안전성과 투명성을 제공하지만, TS의 다른 타입과 달리 구조적 타이핑이 아닌 명목적 타이핑을 사용한다.

TS의 일반적인 타입이 할당 가능성을 체크하기 위해 구조적 타이핑(Item 4)을 사용하는 반면, 문자열 열거형은 명목적 타이핑(nominally typing)을 사용한다.

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

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

명목적 타이핑은 라이브러리를 공개할 때 필요하다. 하지만 TS에서는 열거형을 임포트해서 문자열 대신 사용해야 한다.

해결책은 열거형 대신 리터럴 타입의 유니온을 사용하는 것이다.

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

let flavor: Flavor = 'chocolate' // 정상
   flavor = 'mint chip'
// ~~~~~~ '"mint chip" 유형은 'Flavor' 형식에 할당될 수 없다.

리터럴 타입의 유니온은 열거형만큼 안전하며, JS와 호환이 된다는 장점이 있다. 편집기에서도 열거형처럼 자동완성 기능을 사용할 수 있다.

자세한 내용은 Item 33에서 다뤘다.

매개변수 속성

일반적으로 클래스를 초기화할 때 속성을 할당하기 위해 생성자의 매개변수를 사용한다.

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

TS는 더 간결한 문법을 제공한다.

class Person {
  constructor(public name: string) {}
}

이러한 public name을 '매개변수 속성'이라고 부르며, 그 위의 예제와 동일하게 동작한다. 하지만 문제점이 존재한다.

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

  • 매개변수 속성이 런타임에는 실제로 사용되지만, TS 관점에서는 사용되지 않는 것처럼 보인다.

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

클래스에 매개변수 속성만 존재한다면 클래스 대신 인터페이스를 만들고 객체 리터럴을 사용하는 게 좋다. 구조적 타이핑 특성 때문에 다음처럼 할당할 수 있다는 것을 주의해야 한다.(Item 4)

class Person {
  constructor(public name: string) {}
}
const p: Person = { name: 'Jed Bartlet' } // 정상

매개변수 속성에 대해서는 찬반 논란이 있다. 어떤 이들은 코드 양이 줄어들어 선호하지만, 이 문법은 다른 TS 패턴과 이질적이고 초급자에게 생소한 문법이라는 것을 기억하자. 또한 매개변수 속성와 일반 속성을 섞어서 사용하면 설계가 혼란스러워지기 때문에 한가지만 사용하는 게 좋다.

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

ECMAScript 2015 이전에는 JS에 공식적인 모듈 시스템이 없어서 각 환경마다 자신만의 방식으로 모듈 시스템을 마련했다.

  • Node.js는 requiremodule.exports를 사용한 반면, AMD는 define 함수와 콜백을 사용했다.

TS 역시 자체적 모듈 시스템을 구축했으며, module 키워드와 '트리플 슬래시' 임포트를 사용했다. ECMAScript 2015가 공식적인 모듈 시스템을 도입한 이후, TS는 충돌을 피하기 위해 module과 같은 기능을 하는 namespace 키워드를 추가했다.

namespace foo {
  function bar() {}
}

/// <reference path="other.ts" />
foo.bar()

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

데코레이터

데코레이터는 클래스, 메서드, 속성에 애너테이션(annotation)을 붙이거나 기능을 추가하는 데 사용할 수 있다. 예를 들어 클래스의 메서드를 호출할 때마다 로그를 남기려면 logged 애너테이션을 정의할 수 있다.

데코레이터는 처음에 앵귤러 프레임워크를 지원하기 위해 추가되었으며, tsconfig.jsonexperimentalDecorators 속성을 설정하고 사용해야 한다. 현재도 표준화가 완료되지 않았으므로, 사용 중인 데코레이터가 비표준으로 바뀌거나 호환성이 꺠질 수 있다.

앵귤러를 사용하거나 애너테이션이 필요한 프레임워크(e.g., Nest.js)를 사용하고 있는 게 아니라면 표준이 되기 전까지 TS에서 데코레이터를 사용하지 않는 것이 좋다.

Summary

  • 일반적으로 TS 코드에서 모든 타입 정보를 제거하면 JS가 되지만, 열거형, 매개변수 속성, 트리플 슬래시 임포트, 데코레이터는 타입 정보를 제거해도 JS가 되지 않는다.

  • 타입스크립트의 역할을 명확하게 하려면, 열거형, 매개변수 속성, 트리플 슬래시 임포트, 데코레이터는 사용하지 않는 것이 좋다.

Last updated