본문 바로가기

Frontend/Typescript

enum은 어떻게 써야하는가?

이번에 프로젝트를 진행하면서 상수 값을 저장할 때 계속해서 enum 타입을 사용해 왔다.

그런데, 타입스크립트를 계속 공부하다 보니 enum 타입에 대한 논란이 있는 것을 알게 되었다.

그 논란에 대해서 좀 자세히 알아보고 앞으로 어떻게 enum을 사용할지 생각해 보면 좋을 거 같아서 글을 쓰게 되었다.

 

일단, 나는 아래 글을 보고 나서 해당 문제에 대해서 알아보게 되었다.

https://engineering.linecorp.com/ko/blog/typescript-enum-tree-shaking

 

위 글은 라인 기술 블로그 글인데, 한 줄 요약하자면 enum 타입을 쓰면 tree-shaking이 되지 않으니 const enum이나 union 타입을 쓰라는 것이었다.

 

이렇게만 말하면 무슨 뜻인지 모르겠으니까 좀 자세히 알아보도록 하자.

 

enum vs const enum

enum과 const enum은 각각 아래와 같이 사용할 수 있다.

enum DeliveryStatus1 {
  READY = "READY",
  DELIVERING = "DELIVERING",
  DELIVERED = "DELIVERED",
}

const enum DeliveryStatus2 {
  READY = "READY",
  DELIVERING = "DELIVERING",
  DELIVERED = "DELIVERED",
}

 

코드로 작성해 보면 구조가 거의 유사한데 뭐가 다르다고 하는지 모를 거 같다.

 

다른 점은 바로 트랜스파일된 자바스크립트 코드를 보면 알 수 있다.

// enum 트랜스파일 결과
var DeliveryStatus1;
(function (DeliveryStatus1) {
    DeliveryStatus1["READY"] = "READY";
    DeliveryStatus1["DELIVERING"] = "DELIVERING";
    DeliveryStatus1["DELIVERED"] = "DELIVERED";
})(DeliveryStatus1 || (DeliveryStatus1 = {}));

// const enum 트랜스파일 결과
const READY = "READY";
const DELIVERING = "DELIVERING";
const DELIVERED = "DELIVERED";

 

이제 enum 트랜스파일 결과를 보면 IIFE(즉시 실행 함수)를 포함한 것을 알 수 있다.

몇몇 번들러는 tree-shaking 과정 중에서 즉시 실행 함수로 변환된 값을 사용하지 않는 코드로 인식을 하지 못하게 된다.

그래서 이 코드들이 번들 파일에 추가되어 불필요한 코드가 남아있게 되는 것이다.

즉, enum은 tree-shaking, 불필요한 코드들이 제거되지 않을 수 있는 것이다.

 

하지만 const enum 트랜스파일 결과를 보면 상수 값으로 나오는 것을 알 수 있다.

 

그래서 이런 점을 고려해 봤을 때 enum보다는 const enum을 쓰는 게 더 좋다는 것을 알 수 있다.

하지만, 단순히 enum과 const enum을 남발하지는 않을 테니 정말 크리티컬한 성능적 문제는 없을 것으로 예상된다.

그래도 만약 쓸 일이 있다면 enum보다는 const enum을 사용하자. 정도로 생각하면 될 것 같다.

 

enum 리버스 매핑

숫자형 enum에서는 아래와 같이 리버스 매핑이 가능하다.

typescript의 숫자형 enum이 객체로 변환될 때, 키값뿐만 아니라 값도 키로 변환된다.

enum DeliveryStatus1 {
  READY,
  DELIVERING,
  DELIVERED,
}

const DeliveryStatus1 = {
  READY: 0,
  DELIVERING: 1,
  DELIVERED: 2,
  "0": "READY",
  "1": "DELIVERING",
  "2": "DELIVERED",
};

 

그래서 아래와 같은 접근이 가능해진다.

DliveryStatus1[0]  // READY
Object.keys(DeliveryStatus1);  // ["READY", "DELIVERING", "DELIVERED", "0", "1", "2"]

 

아주 혼란스러운 결과다.

 

하지만 문자형 enum에서는 리버스 매핑을 하지 않기 때문에 같은 문제는 없다.

그리고, const enum으로 아래와 같이 선언해도 트랜스파일 시 상수로 변환되기 때문에 리버스 매핑이 일어나지 않는다.

const enum DeliveryStatus1 {
  READY,
  DELIVERING,
  DELIVERED,
}

// 트랜스파일 결과
const READY = "READY";
const DELIVERING = "DELIVERING";
const DELIVERED = "DELIVERED";

 

enum 순회

enum을 순회할 수 있는 방법에는 두 가지가 있다.

Object.keys, Object.values 메서드를 사용하는 방법이다.

enum DeliveryStatus1 {
  READY = "READY",
  DELIVERING = "DELIVERING",
  DELIVERED = "DELIVERED",
}

Object.keys(DeliveryStatus1).forEach((key) => key)
Object.values(DeliveryStatus1).forEach((value) => value);

 

그런데 위에서 key와 value의 타입이 다른 것을 확인할 수 있었다.

key는 string 타입, value는 DeliveryStatus1 타입이었다.

 

이는 Object.keys와 Object.values 메서드의 호출 시그니처를 보면 해결된다.

/**
     * Returns the names of the enumerable string properties and methods of an object.
     * @param o Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object.
     */
    keys(o: {}): string[];
/**
     * Returns an array of values of the enumerable properties of an object
     * @param o Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object.
     */
    values<T>(o: { [s: string]: T; } | ArrayLike<T>): T[];

 

그래서, Object.keys 메서드는 결국 string 배열을 반환하고, Object.values 메소드는 DeliveryStatus1 배열을 반환하게 되는 것이다.

 

정확한 타입 추론을 하기 위해서는 Object.values 메서드를 사용해서 enum을 순회해야 한다.

 

하지만, 이는 const enum이 아니라 enum으로만 할 수 있는 방법이다.

왜냐하면 const enum은 트랜스파일되면 상수 값으로 인라인화되기 때문에 순회가 불가능하다.

 

그래서 순회가 필요한 경우 아래와 같이 상수 객체를 생성하도록 하자.

const DeliveryStatus1 = {
  READY: "READY",
  DELIVERING: "DELIVERING",
  DELIVERED: "DELIVERED",
} as const;

 

참고 자료

https://velog.io/@vraimentres/typescript-enum

https://kimsangyeon-github-io.vercel.app/blog/2022-03-28-typescript-enum