네 Enum! 누가 Typescript에서 Enum을 쓰냐!

허민(허브)·2022년 7월 10일
45
post-thumbnail

Enum을 Tree-shaking관점으로 Typescirpt에서 더 효과적으로 사용하기 위해 Union Type으로 Migration 할 때 공부하였던 내용과 간단한 사용방법 및 검증을 다룬다.


글쓴이가 올리는 당부의 말

추가적으로 강현구 개발자님께서 아래와 같은 제보를 직접 해주셨습니다. (메일로 해당 내용을 인용할 수 있다는 걸 허락해주셔서 감사합니다!)

나는 일단 글을 작성하면서 1번에 대한 오류를 범했다. 평소 webpack과 babel 환경에서만 개발을 하다보니 최근 시작한 React Native 프로젝트에서 metro가 tree shaking을 지원할 것이라는 안일한 생각에 리팩토링을 진행했다는 점이다.

그리고 2번에 대하여 생산성에 대한 측면을 고려하지 않고 무조건 이게 enum 보단 as const 타입을 통한 객체 순환이 더 좋은거야! 라고 생각하여 리팩토링을 진행했다는 점에서 슈퍼울트라초보주니어의 실수를 범했다는 점이다.


이 글 하단에 작성할 수 도 있지만 상단에 작성하는 이유는 아직 부족한 실력의 개발자의 글이 타인의 코드에 유사한 오류를 범할 수 있다는 무시무시한 영향을 끼칠 수 있다고 생각되어 상단에 작성하였습니다. 하단 글에서 React 실습 코드는 React Native 환경에서 작성했지만 문법과 취지는 읽으시는데 문제 없습니다. 만약 사용하신다면 저와 같은 과오를 범하지 마시고 Tree Shaking을 지원하는 번들러에서 활용하시기 바랍니다.

또한 저는 Tree Shaking 관점에서 Enum에 대해 다루었지만 Enum 객체를 modern Typescirpt 에서는 필요에 따라 as const와 같은 형태로 사용할 수 있고 굳이 enum을 활용할 필요는 없다고 핸드북에서 다루고 있습니다. 이런 방향으로도 객체를 다뤄 볼 수 있구나! 라는 관점으로 글을 읽어주셨으면 감사하겠습니다 ㅎㅎ !

Enum이란?

열거형으로 이름이 있는 상수들의 집합을 정의할 수 있습니다. 열거형을 사용하면 의도를 문서화 하거나 구분되는 사례 집합을 더 쉽게 만들수 있습니다. TypeScript는 숫자와 문자열-기반 열거형을 제공합니다.


//숫자를 열거할 수 있다.
export enum Number {
	one = 1,
    two, // 2
  	three, // 3
}

// 임의의 문자열을 할당할 수 있다. 
export enum Status {
	TODO = "todo",
    DOING = "doing",
    DONE = "done",
}
    
//실제 코드에선 import 하여 아래와 같이 사용가능
const TodoStatus : Status = Status.DOING

Enum은 Typescript가 자체적으로 구현한 코드입니다. 그렇기 때문에 Javascript 에서는 객체로 선언을 하여 사용한다.

export const Status = {
	TODO = "todo",
    DOING = "doing",
    DONE = "done",
}

const TodoStatus = Status.DOING;
console.log(TodoStatus) // doing

하지만 Typescript는 슈퍼셋(SuperSet)언어기 때문에 이를 트랜스파일하게 되면 아래와 같은 자바스크립트 코드가 된다.


//타입스크립트 컴파일러로 트랜스파일링
//숫자를 열거할 수 있다.
export var Number;
(function (Number) {
    Number[Number["one"] = 1] = "one";
    Number[Number["two"] = 2] = "two";
    Number[Number["three"] = 3] = "three";
})(Number || (Number = {}));
// 임의의 문자열을 할당할 수 있다. 
export var Status;
(function (Status) {
    Status["TODO"] = "todo";
    Status["DOING"] = "doing";
    Status["DONE"] = "done";
})(Status || (Status = {}));

//babel을 활용한 트랜스 파일링
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.Status = exports.Number = void 0;
//숫자를 열거할 수 있다.
let Number; 

exports.Number = Number;

(function (Number) {
  Number[Number["one"] = 1] = "one";
  Number[Number["two"] = 2] = "two";
  Number[Number["three"] = 3] = "three";
})(Number || (exports.Number = Number = {}));

// 임의의 문자열을 할당할 수 있다. 
let Status;
exports.Status = Status;

(function (Status) {
  Status["TODO"] = "todo";
  Status["DOING"] = "doing";
  Status["DONE"] = "done";
})(Status || (exports.Status = Status = {}));

typescirpt은 v4.7.4 활용, babel은 v7.18.8을 활용하였다.

Typescirpt 컴파일러는 이를 IIFE(즉시실행 함수)로 구현을 해냈다. 그런데 Rollup과 같은 번들러는 IIFE를 사용하지 않는 코드라고 판단할 수 없어서 Tree-shaking이 되지 않는다. 결국 Number와 Status를 실제로는 사용하지 않더라도 최종 번들에는 포함시키는 것이다.

즉 typescript에서 enum을 활용하면 Tree-shaking이 되지 않는 것이다.

그렇다면 Tree-shaking이란 무엇일까??

Tree-shaking이란?

Tree shaking is a term commonly used in the JavaScript context for dead-code elimination. It relies on the static structure of ES2015 module syntax, i.e. import and export. The name and concept have been popularized by the ES2015 module bundler rollup.

트리 쉐이킹은 사용하지 않는 코드를 제거하는 방식이다. (나무라는 프로젝트를 마구마구 흔들어서 죽은 잎사귀나 열매를 떨어트리자)이 용어는 Rollup에 의해 인기를 얻게 되었으나, 사용하지 않는 코드 제거에 대한 개념은 이미 존재했었다. 또한 이 개념은 webpack에서도 찾아볼 수 있다.

즉 Tree-shaking을 통해서 빌드 시스템을 구성하면 export 했지만 아무데서도 import 하지 않은 모듈이나 사용하지 않는 코드를 삭제해서 번들 크기를 줄여서 렌더링 시간을 줄일 수 있다.

그러면 enum 대신 어떤걸 쓰면 좋나요?

In modern TypeScript, you may not need an enum when an object with as const could suffice:

타입스크립트 핸드북에 의하면 enum 대신 as const 문법을 통하여 사용할 수 있다.
실제 현재 개발하고 있는 코드에서는 아래와 같이 사용하고 있다.

export const orderStatusKeys = {
  주문대기중: 'received',
  주문완료: 'accepted',
  제작중: 'making',
  픽업완료: 'completed',
  취소된주문: 'canceled',
} as const;

export type OrderStatusUnion =
  typeof orderStatusKeys[keyof typeof orderStatusKeys];

이 코드는 아래와 같이 컴파일된다. Typescript 코드에서는 orderStatusKeys의 타입을 정의한 이점을 그대로 누리면서 Javascript로 트랜스파일해도 IIFE가 생성되지 않으므로 Tree-shaking을 할 수가 있다.

export const orderStatusKeys = {
    주문대기중: 'received',
    주문완료: 'accepted',
    제작중: 'making',
    픽업완료: 'completed',
    취소된주문: 'canceled',
};

그렇다면 const enum은 어떨까? Typescript에서 const enum을 사용해보면 거의 비슷하지만 enum의 내용이 트랜스파일할때 인라인으로 확장된다는 점이 다르다.

//typescirpt
export const enum orderStatusKeys {
  주문대기중 ='received',
  주문완료 = 'accepted',
  제작중 = 'making',
  픽업완료 = 'completed',
  취소된주문 = 'canceled',
};

const state = orderStatusKeys.제작중;

//javascript
const state = "making" /* orderStatusKeys.제작중 */;
export {};

하지만 만약 영어가 아니라 한글로 저장한다면 어떨까??

//typescript
export const enum orderStatusKeys {
  주문대기중 ='주문대기중입니다.',
  주문완료 = '주문완료입니다.',
  제작중 = '제작중입니다.',
  픽업완료 = '픽업완료입니다.',
  취소된주문 = '취소된 주문입니다.',
}

const state = orderStatusKeys.제작중

//javascript
const state = "\uC81C\uC791\uC911\uC785\uB2C8\uB2E4." /* orderStatusKeys.제작중 */;
export {};

이렇게 unicode escaped로 바뀌게 되는데 실행 자체는 잘되지만 한글로 볼려면, 이에 대해서 불필요한 escape를 지워줘야 한다는 번거로움이 있다.

리액트에서 실제 활용

현재 프로젝트에서는 주문 상태에 따라서 아래 버튼이 다르게 보여야한다. 그래서 해당 주문 상태를 위에 선언해준 것과 같이 union type으로 지정을 해주어서 사용하고 있다.

테오의 프론트엔드 톡방에서 곰젤리님이 힘찬 스윙을 해주셔서 덕분에 이렇게 이 글을 쓸 수 있었다.

export const orderStatusKeys = {
  주문대기중: 'received',
  주문완료: 'accepted',
  제작중: 'making',
  픽업완료: 'completed',
  취소된주문: 'canceled',
} as const;

export type OrderStatusUnion =
  typeof orderStatusKeys[keyof typeof orderStatusKeys];

react 에서는 jsx문법에서 object/array 자료형을 응용해서 아래와같이 사용해볼 수 있다. orderStatusKeys의 요소를 선언해줄 수도 있지만 ['received'.toString()] 과 같이고 사용할 수 있다. 하지만 나는 영어보단 좀 더 직관적으로 코드를 작성하고 싶은 마음이 있어서 아래와 같이 선언해서 사용해주었다.

아래 코드는 이번 글에서 컨셉을 이해하기 위해 View를 간략화 해서 표현하였지만 이제 해당 코드에 하단 버튼이 있는 Footer Component가 들어간다.

export type OrderStatusViewProps {
	status : OrderStatusUnion
}
export const OrderStatusView:FC<OrderStatusViewProps> = ({status}) => {
  return ({
    {
    <View>
      
          [orderStatusKeys.주문대기중]: <View>주문 대기중 입니다.</View>,
          [orderStatusKeys.주문완료]: <View>주문완료 입니다.</View>,
          [orderStatusKeys.제작중]: <View>제작중 입니다.</View>,
          [orderStatusKeys.픽업완료]: <View>픽업완료 입니다.</View>,
          [orderStatusKeys.취소된주문]: <View>취소된주문입니다.</View>,
      }[status]
  		}
      </View>
      );
}

많이 어려운 주제고 타입스크립트를 더욱 잘쓸려고 노력하고자 섰던 글이다. 직접 타입스크립트가 어떻게 트랜스파일링 되는지 검증하면서 글을 쓴게 큰 도움이 되었다. 이후에는 직접 웹팩 설정을 하여 빌드 시간이 어떻게 줄어드는지 간단한 예제같은걸 만들어서 해봐야겠다.

참고 문서

typescript 핸드북 Enum
TypeScript enum을 사용하지 않는 게 좋은 이유를 Tree-shaking 관점에서 소개합니다.
Tree-shaking
Tree Shaking | Webpack

profile
Adventure, Challenge, Consistency

6개의 댓글

comment-user-thumbnail
2022년 7월 10일

스윙하고 계시군요 👍

1개의 답글
comment-user-thumbnail
2022년 7월 11일

단순히 Typescript 에서 enum 사용을 지양하는거보다, 아래의 두가지 관점을 더 추가해서 판단하면 더 좋지 않을까 해서 의견을 남겨봅니다 ㅎㅎ

  1. 번들러에서 tree shaking 을 지원하는지에 대한 고려도 필요합니다.
    (fyi, React Native 의 Metro bundler 는 tree shaking 을 지원하지 않습니다.)

  2. tree shaking 은 기본적으로 사용되지 않은 코드를 제거하는데 의미가 있습니다. 즉, 작성한 코드가 사용되지 않을수도 있다는 전제조건이 깔려야 합니다.
    다른 사람이 import 해서 사용하는 서드 파티 라이브러리를 개발한다면 그럴 가능성이 높겠지만, 일반적인 어플리케이션이라면 우리가 enum 을 선언하고 사용하지 않을 확률이 얼마나 될까? 라는 관점에서도 고려해볼 수 있습니다.
    (enum 이 가져다주는 생산성 vs 사용될 가능성이 높은 코드에 트리 쉐이킹을 고려해서 얻는 이득)

1개의 답글
comment-user-thumbnail
2022년 7월 12일

당장 코드는 조금 길어질지 모르겠지만 다음과 같이 status의 key 와 value를 분리해서 작성하는 것은 어떤가요

type OrderStatusKey =
    | "주문대기중"
    | "주문완료"
    | "제작중"
    | "픽업완료"
    | "취소된주문";

type OrderStatusValue =
    | "received"
    | "accepted"
    | "making"
    | "completed"
    | "canceled";

type OrderStatusTypes = {
  [key in OrderStatusKey]: OrderStatusValue;
};

type OrderStatusViewProps = {
  status: OrderStatusValue;
};

const orderStatus: OrderStatusTypes = {
  주문대기중: 'received',
  주문완료: 'accepted',
  제작중: 'making',
  픽업완료: 'completed',
  취소된주문: 'canceled',
};
1개의 답글