By kimcoder
2022.01.21

Enum에 대한 고찰

enum은 자바스크립트의 타입 레벨에서는 존재하지 않지만 타입스크립트에서는 존재하는 몇 안되는 기능 중 하나이다. 흔히 프로그래밍 중 연관된 상수들을 묶을때 열거형이라고 불리우는 enum을 통해 표현하고 한다.

1enum Direction {
2  Up,
3  Down,
4  Left,
5  Right,
6}
7
8function moveTo(direction: Direciton) {
9  if (direction === Direciton.UP) {
10    // Do something..
11  }
12
13  // Do something..
14}

타입스크립트로 개발을 하다보면 위와 같은 코드를 흔히 작성하게 된다.
앞서 말한것 처럼, enum은 자바스크립트에서는 존재하지 않기 때문에 타입스크립트로 enum을 사용했을 때, 이 코드가 컴파일이 된 후 자바스크립트에서는 어떤식으로 표현이 되는지 고찰을 해보고 사용하는 것이 좋겠다.

Enum의 특징과 분류

  • Enum은 값으로 아래와 같은 값들을 가질 수 있다.
    • 문자열 리터럴 ( e.g. 'hello', 'a', 'bye' )
    • 숫자 리터럴 ( e.g. 0, 1, 0.2, 1000 )
    • 부호가 있는 숫자( 음수 ) 리터럴 ( e.g. -1, -0.2, -1000 )
  • 이 값들은 또한, 모두 계산되어진 값들이어야 한다.

Numeric enums

  • numeric enum의 멤버는 초기값을 할당해 주지 않으면, 기본적으로 그 전의 멤버로부터 1씩 증가되는 숫자가 값으로 할당된다.
  • 첫 멤버가 값을 할당받지 않았다면 0을 값으로 가지게 된다.

아래 코드를 보면 확실하게 이해할 수 있다.

1enum Direction {
2  Up,    // 0
3  Down,  // 1
4  Left,  // 2
5  Right, // 3
6}
7
8enum Speed {
9  VeryLow = -1,     // -1
10  Low,              // 0
11  Normal = -0.2,    // -0.2
12  Fast,             // 0.8
13  VeryFast          // 1.8
14  SuperFast = 100,  // 100
15  UltraFast         // 101
16}

numeric enum은 컴파일 시, reverse mapping이 된다.

after compile

1// before compile
2enum Direction {
3  Up,
4  Down,
5  Left,
6  Right,
7}
8
9let up = Direction.Up; // 0
10let nameOfUp = Direction[up]; // 'Up'
11
12// after compile
13var Direction;
14(function (Direction) {
15  Direction[(Direction['Up'] = 0)] = 'Up';
16  Direction[(Direction['Down'] = 1)] = 'Down';
17  Direction[(Direction['Left'] = 2)] = 'Left';
18  Direction[(Direction['Right'] = 3)] = 'Right';
19})(Direction || (Direction = {}));
1console.log(Direciton);
2
3{0: 'Up', 1: 'Down', 2: 'Left', 3: 'Right', Up: 0, Down: 1, Left: 2, Right: 3}
  • reverse mapping이 된 후의 실제 Direction은 위와 같은 형태를 이룬다.
  • 숫자를 값으로 가지는 enum의 멤버들은 모두 reverse mapping이 된다.

String enums

  • string enum의 멤버들은 모두 초기 값을 할당 받아야 한다.
  • numeric enum처럼 자동 증가되는 값을 가지지 않지만, 직렬화가 잘 된다는 장점이 있다.
    • 멤버의 이름에 관계 없이, 코드 동작시 의미 있고 읽을 수 있는 값을 제공할 수 있다.
1enum Direction {
2  Up = 'UP',
3  Down = 'Down',
4  Left = 'Left',
5  Right = 'Right',
6}

after compile

1var Direction;
2(function (Direction) {
3  Direction['Up'] = 'UP';
4  Direction['Down'] = 'Down';
5  Direction['Left'] = 'Left';
6  Direction['Right'] = 'Right';
7})(Direction || (Direction = {}));

Heterogeneous enums

  • 기술적으로 enumstring enumnumeric enum의 멤버가 혼합이 될 수 있지만, 목적이 불분명한 코드가 될 수 있다.
  • 자바스크립트 런타임의 동작을 영리한 방법으로 활용하는 이유가 아니라면 사용하지 않는 것을 권장한다.
  • 혼합하여 사용할때도 마찬가지로 numeric enum의 자동 증가되는 값 할당, reverse mapping 등의 특징은 동일하다.
1enum Direction {
2  Up,
3  Down = 'Down',
4  Left = 1,
5  Right,
6}

after compile

1var Direction;
2(function (Direction) {
3  Direction[(Direction['Up'] = 0)] = 'Up';
4  Direction['Down'] = 'Down';
5  Direction[(Direction['Left'] = 1)] = 'Left';
6  Direction[(Direction['Right'] = 2)] = 'Right';
7})(Direction || (Direction = {}));

Declaration Merging

  • enumdeclaration merging(선언 병합)의 대상이 된다.
  • numeric enum의 선언 병합시, 2번째 enum은 초기 값을 항상 가져야 한다.
1enum Direction {
2  Up,
3  Down,
4  Left,
5}
6
7enum Direciton {
8  Right = 4,
9}

after compile

1var Direction;
2(function (Direction) {
3  Direction[(Direction['Up'] = 0)] = 'Up';
4  Direction[(Direction['Down'] = 1)] = 'Down';
5  Direction[(Direction['Left'] = 2)] = 'Left';
6})(Direction || (Direction = {}));
7(function (Direction) {
8  Direction[(Direction['Right'] = 4)] = 'Right';
9})(Direction || (Direction = {}));
10
11console.log(Direciton);
12
13{0: 'Up', 1: 'Down', 2: 'Left', 3: 'Right', Up: 0, Down: 1, Left: 2, Right: 3}

Immediately invoked function expression

  • 모든 enum은 컴파일 후 즉시실행 함수 표현식(IIFE)의 형식으로 코드가 변환된다.
  • rollup과 같은 번들러는 IIFE의 사용 여부를 판단할 수 없기 때문에, Tree-shaking 되지 않는다.
    • enum이 죽은 코드가 되더라도 컴파일된 코드의 번들에는 존재하게 된다.
    • 여기서 직접 확인할 수 있다.

const enum

  • enum의 추가 코드 생성과 간접 참조 코드를 피하고 싶으면 const enum을 사용하는게 방안이 될 수 있다.
  • const enum은 컴파일 중에 완전히 제거되고, 참조하는 코드영역에서 값만 인라인되어진다.
1const enum Direction {
2  Up,
3  Down,
4  Left,
5  Right,
6}
7
8console.log(Direction.Up);

위와 같은 코드가 컴파일이 되면, 아래와 같이 인라인된 코드만 남게 된다.

1console.log(0 /* Up */);

기본 enum과 비교했을 때, 컴파일 후에는 많은 차이가 생기는 것으로 볼 수 있다.
불필요한 코드들이 생성되지 않아, 메모리와 번들링 된 코드의 사이즈가 조금 더 줄어든다는 이점이 있다.
하지만 d.ts 파일을 생성해야하거나, 라이브러리로써 제공해야 될 경우에는 타입추론이 불가하다는 단점도 있다.
이럴 경우엔, tsconfigpreserveConstEnums 옵션을 true로 설정하여 해결할 수 있다.

또, tsconfigisolatedModules 옵션을 true로 설정하고 cosnt enum을 사용하면 컴파일시 오류가 나게 되니, 사용 전에 이러한 부분을 꼭 숙지하여야 한다.

Object Literal const assertions

타입스크립트 3.4 버전에 나온 const assertions을 사용하면 enum을 사용하지 않아도 동일하게 상수들을 열거하는 표현을 쉽게 할 수 있다.

1const Direction = {
2  Up: 'Up',
3  Down: 'Down',
4  Left: 'Left',
5  Right: 'Right',
6} as const;
7
8type EnumerateDirection = typeof Direction[keyof typeof Direction];
9// EnumerateDirection = 'Up' | 'Down' | 'Left' | 'Right'
10
11function moveTo(direction: EnumerateDirection) {
12  if (direction === 'Up') {
13    // Do something..
14  }
15}

위의 코드와 같이 타입을 추론하기 위해 type aliasing을 한 번 해주어야하는 번거로움이 있지만, enum으로부터 얻을 수 없는 몇 가지의 장점이 존재한다.

  1. 컴파일을 하면 타입스크립트에서 작성한 형식이 유지된 채로 자바스크립트로 변환된다.
  2. Tree-shaking이 적용된다.
  3. (의도하지 않은)선언 병합의 대상이 되지 않는다.

after compile

1const Direction = {
2  Up: 'Up',
3  Down: 'Down',
4  Left: 'Left',
5  Right: 'Right',
6};
7
8// EnumerateDirection = 'Up' | 'Down' | 'Left' | 'Right'
9function moveTo(direction) {
10  if (direction === 'Up') {
11    // Do something..
12  }
13}

마치며

위의 내용을 요약하자면,

  1. enum 멤버들의 값은 계산된 문자열 혹은 숫자만 할당될 수 있다.
  2. enum도 선언 병합이 가능하다.
  3. numeric enum은 컴파일 후 reverse mapping이 된다.
  4. enum은 IIFE로 변환되기 때문에 Tree-shaking이 되지 않는다.
  5. const enum은 해당 값을 참조하는 부분의 코드만 인라인된다.
  6. TS 3.4 이상의 버전은 객체 리터럴을 const assertions 해주는 것으로 상수의 열거를 표현할 수 있다.

References