Skip to content

typescript enum과 tree shaking에 대해

Youngho Kim edited this page Nov 27, 2022 · 1 revision
  • 작성자: J112_양성훈

최근 프로젝트 개발 도중 열거형을 사용할일이 생겼다. 사용자가 선택할 수 있는 mode type을 정의하는 부분이었는데 처음에는 typescript의 enum을 사용했다. 몇몇 포스팅에서 최적화 이슈로 enum을 사용하지 말라는 글을 읽었고 관련해서 tree-shaking이라는 개념을 알게 되었다. 이 포스팅에서는 왜 enum을 사용하면 안되는지와 개선방안 그리고 tree-shaking에 대해서 알아보고자 한다.

Enum의 문제점

Enum?

enum은 ts에서 만든 열거형 타입으로 명명된 상수 집합을 정의할 수 있게 한다. enum은 ts의 다른 타입들 처럼 a type-level extension of JavaScript가 아니라 ts에서 자체 제공하는 몇 안되는 기능 중 하나다. 뭔말이냐면 일반 타입은 트랜스파일링하면 js 코드로는 남아있지 않는데 enum은 js로 변환된다는 말이다.

enum은 언제 사용될까? 필자는 3가지 이유로 enum을 사용한다.

  1. 개발 용의성: 정의 내용이 한곳에 정의되어 있어 파악하기 편하고, 상수와 다르게 자동완성 기능 사용 가능
  2. 유지보수성: 한곳에 모여 정의되어 있기 때문에 이 집합만 관리하면 된다.
  3. 안정성: 상수 표현에서 발생할 수 있는 휴먼 에러 방지

필자는 프로젝트에서 아래와 같이 enum 타입을 사용했다.

enum CanvasType {
	postit = 'postit',
	section = 'section',
	move = 'move',
	select = 'select',
	draw = 'draw',
}

enum의 문제점

편리한 점이 많은 enum이 뭐가 문제길래 사용하지 말라는걸까? 먼저 ts 빌드 과정을 알아보자. 일반적으로 ts는 tsc 혹은 바벨 등으로 js코드로 트랜스파일링된 후 webpack, rollup과 같은 번들러를 통해 하나의 파일로 묶이게 된다. 번들러에 의해 하나의 파일로 묶이는 과정에서 여러가지 최적화를 진행하게 되는데 그 중 하나가 tree-shaking이다.

tree-shaking은 직역하면 나무를 턴다. 즉 불필요한 코드를 삭제하는 과정을 말한다. export 했지만 아무곳에서도 import 되지 않은 모듈이나 코드를 삭제하여 번들의 크기를 줄이는 과정이다.

문제는 enum이 tree-shaking이 일어나지 않는다는 것이다. 왜인지 테스트를 통해 알아보자

테스트

환경:

먼저 enum을 tsc로 트랜스 파일링해보자

export enum CanvasType {
	postit = 'postit',
	section = 'section',
	move = 'move',
	select = 'select',
	draw = 'draw',
}

// 결과
export var CanvasType;
(function (CanvasType) {
    CanvasType["postit"] = "postit";
    CanvasType["section"] = "section";
    CanvasType["move"] = "move";
    CanvasType["select"] = "select";
    CanvasType["draw"] = "draw";
})(CanvasType || (CanvasType = {}));

이를 rollup으로 번들링해보자

image

CanvasType을 어디서도 쓰지 않지만 tree-shaking이 일어나지 않았다.

enum을 트랜스파일링하면 즉시실행함수(IIFE)가 만들어지는데 번들러가 ‘사용하지 않는 코드’로 판단할 수 없어 tree-shaking이 일어나지 않는다고 한다. 정확한 이유는 검색해도 안나오지만 아마 sideEffect 때문이지 않을까 생각한다.

webpack5에서도 직접 테스트 해보진 않았지만 tree-shaking 작동 이슈가 있는 것 같다. (참고:https://stackoverflow.com/questions/68720866/why-does-webpack-5-include-my-unused-typescript-enum-exports-even-when-tree-sha)

개선방안

  1. const enum 사용
export const enum CanvasType {
	postit = 'postit',
	section = 'section',
	move = 'move',
	select = 'select',
	draw = 'draw',
}

export const test = CanvasType.postit

//트랜스파일링
export const test = "postit" /* CanvasType.postit */;

걍 앞에 const만 붙이면 된다. 근데 트랜스파일링 결과가 이상하다라고 생각할 수 있는데 공식문서에서 const enum은 “Const enum members are inlined at use sites”라고 표현한다. 즉 쓰이는 부분에 inline으로 직접 박히는 것 그래서 자연스럽게 tree-shaking이 일어날 수 밖에 없다. 근데 번들러가 해주는게 아니라 ts 컴파일 단계에서 일어난다. 개념적으로는 tree-shaking이라 볼 수 없지만 논리적으로는 같은 결과가 일어나는 것 즉 본질은 같다.

근데 const enum은 단점이 존재한다.

  1. 긴 문자열을 할당하는 경우

    const enum CanvasType {
    	postit = 'postit',
    	section = 'section',
    	move = 'move',
    	select = 'select',
    	draw = 'draw',
    	random = '"u5BFFu9650u7121u5BFFu9650u7121u4E94u52ABu306Eu64E6u308Au5207u308Cu6D77u7802u5229u6C34u9B5Au306Eu2026'
    }
    const test = CanvasType.random
    
    console.log(test === CanvasType.random)
    
    // 트랜스파일링
    const test = "\"u5BFFu9650u7121u5BFFu9650u7121u4E94u52ABu306Eu64E6u308Au5207u308Cu6D77u7802u5229u6C34u9B5Au306Eu2026" /* CanvasType.random */;
    console.log(test === "\"u5BFFu9650u7121u5BFFu9650u7121u4E94u52ABu306Eu64E6u308Au5207u308Cu6D77u7802u5229u6C34u9B5Au306Eu2026" /* CanvasType.random */);

    이렇게 번들 크기가 커질 수 있다

  2. -isolatedModules 옵션 사용시 다른파일의 const enum이 작동하지 않는 이슈가 있다고 한다. - isolatedModules는 “operate on a single file at a time” 이기 때문에 다른 파일의 const enum을 참조해서 inline에 넣어야하는 과정에서 오류가 있는 듯 하다(추측임) (참고: https://www.typescriptlang.org/tsconfig#isolatedModules)

  3. babel로 트랜스파일링이 안된다. → 대표적인 문제였는데 babel 7.16.0 부터 지원이 된다고 한다. (참고: https://www.cloudless.blog/post/babel-vs-tsc)

  4. union Type 사용

const enum이 여러 단점이 있다보니 union Type을 응용한 방법을 권장한다. (참고: https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums)

const CanvasType = {
	postit: 'postit',
	section: 'section',
	move: 'move',
	select: 'select',
	draw: 'draw',
} as const;

type CanvasType = typeof CanvasType[keyof typeof CanvasType];

//트랜스 파일링
const CanvasType = {
    postit: 'postit',
    section: 'section',
    move: 'move',
    select: 'select',
    draw: 'draw',
};

image

tree-shaking도 잘 일어나고 형태의 안정성도 보장할 수 있다.

따라서 필자도 후자의 방법으로 열거형을 표현하고 있다.(js에서 열거형 쓰기 참 힘들다)

tree-shaking

tree-shaking에 대해 좀 더 알아보자

tree-shaking 하는 법

  1. es6 모듈내보내기 import/export 사용 tree-shaking은 es6의 모듈 방법으로만 작동한다. es6로 작성해야하는건 물론이며 babel이 commonJS로 변환하고 있지 않은지 체크해야한다.

  2. 사이드 이펙트 고려하기 각 모듈이 사이드 이펙트를 발생시키는지는 tree-shaking에 중요한 요소다. 사이드 이펙트가 없이 모듈을 분리하여 코드를 짜는게 우선 중요하겠다. 사이드 이펙트가 없이 코드를 짜도 번들러가 이 사실을 모를 수 있다. 따라서 번들러에게 알려주는 방법이 필요하다 package.json의 sideEffects 속성을 건드릴 수도 있고 /*#__PURE__*/ 이노테이션을 통해서도 가능하다. 정확한 방법은 각 번들러 공식문서를 참고하자 https://webpack.kr/guides/tree-shaking/

  3. 원하는 것만 가져오기

    // bad
    import * as utils from "../../utils/utils";
    
    // good
    import { simpleSort } from "../../utils/utils";

    필요한 모듈만 가져와서 쓰면 tree-shaking에 유리하다. 아니 유리했다. 예전에는 namespace 방식이나 전체 모듈을 불러오는게 tree-shaking을 지원하지 않는 듯 했으나 최근 버전의 번들러들은 네임스페이스도 지원하는 듯 하다. 그래도 아직 지원하지 않는 번들러가 있을 수도 있고 가독성 측면에서도 원하는 것만 가져오는게 좋다.

re-exporting

export * from './socket.types';
export * from './workspace-object.types';
export * from './workspace-member.types';
export * from './workspace.types';

최근에 프로젝트에서 re-exporting 패턴으로 묶어서 refactoring을 진행했다. tree-shaking을 작성하다보니 이렇게되면 사용하지 않는 것도 사용했다고 번들러가 오해하지 않을까?

다행이도 webpack4부터 tree-shaking을 지원한다고 한다. 아래와 같이 설정해주면 된다 (참고: https://huns.me/development/2265)

module.exports = {
  optimization: {
    providedExports: true
  }
};

참고문헌

https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums

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

https://ui.toast.com/weekly-pick/ko_20181220

https://www.cloudless.blog/post/babel-vs-tsc

https://webpack.kr/guides/tree-shaking/

https://ui.toast.com/weekly-pick/ko_20180716

https://huns.me/development/2265

https://xpectation.tistory.com/218

https://velog.io/@sensecodevalue/Typescript-Enum-왜-쓰지-말아야하죠

📚 그라운드 룰

✏️ 컨벤션

🧑‍🏫 멘토링

📁 애자일 프로세스

기획
데일리 스크럼
스프린트 리뷰
스프린트 회고
트러블 슈팅
기타 산출물

📖 기술문서

Week2
Week3
Week4
Week5

🗂 참고문서

Clone this wiki locally