-
Notifications
You must be signed in to change notification settings - Fork 1
typescript enum과 tree shaking에 대해
- 작성자: J112_양성훈
최근 프로젝트 개발 도중 열거형을 사용할일이 생겼다. 사용자가 선택할 수 있는 mode type을 정의하는 부분이었는데 처음에는 typescript의 enum을 사용했다. 몇몇 포스팅에서 최적화 이슈로 enum을 사용하지 말라는 글을 읽었고 관련해서 tree-shaking이라는 개념을 알게 되었다. 이 포스팅에서는 왜 enum을 사용하면 안되는지와 개선방안 그리고 tree-shaking에 대해서 알아보고자 한다.
enum은 ts에서 만든 열거형 타입으로 명명된 상수 집합을 정의할 수 있게 한다. enum은 ts의 다른 타입들 처럼 a type-level extension of JavaScript가 아니라 ts에서 자체 제공하는 몇 안되는 기능 중 하나다. 뭔말이냐면 일반 타입은 트랜스파일링하면 js 코드로는 남아있지 않는데 enum은 js로 변환된다는 말이다.
enum은 언제 사용될까? 필자는 3가지 이유로 enum을 사용한다.
- 개발 용의성: 정의 내용이 한곳에 정의되어 있어 파악하기 편하고, 상수와 다르게 자동완성 기능 사용 가능
- 유지보수성: 한곳에 모여 정의되어 있기 때문에 이 집합만 관리하면 된다.
- 안정성: 상수 표현에서 발생할 수 있는 휴먼 에러 방지
필자는 프로젝트에서 아래와 같이 enum 타입을 사용했다.
enum CanvasType {
postit = 'postit',
section = 'section',
move = 'move',
select = 'select',
draw = 'draw',
}
편리한 점이 많은 enum이 뭐가 문제길래 사용하지 말라는걸까? 먼저 ts 빌드 과정을 알아보자. 일반적으로 ts는 tsc 혹은 바벨 등으로 js코드로 트랜스파일링된 후 webpack, rollup과 같은 번들러를 통해 하나의 파일로 묶이게 된다. 번들러에 의해 하나의 파일로 묶이는 과정에서 여러가지 최적화를 진행하게 되는데 그 중 하나가 tree-shaking이다.
tree-shaking은 직역하면 나무를 턴다. 즉 불필요한 코드를 삭제하는 과정을 말한다. export 했지만 아무곳에서도 import 되지 않은 모듈이나 코드를 삭제하여 번들의 크기를 줄이는 과정이다.
문제는 enum이 tree-shaking이 일어나지 않는다는 것이다. 왜인지 테스트를 통해 알아보자
환경:
- tsc 트랜스파일링: v4.8.4, module: nsnext, target: es5 https://www.typescriptlang.org/play?target=2#code/FBA 에서 진행 (프로젝트 환경과 동일)
- 번들링: https://rollupjs.org/repl/ 에서 진행
먼저 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으로 번들링해보자
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)
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은 단점이 존재한다.
-
긴 문자열을 할당하는 경우
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 */);
이렇게 번들 크기가 커질 수 있다
-
-isolatedModules 옵션 사용시 다른파일의 const enum이 작동하지 않는 이슈가 있다고 한다. - isolatedModules는 “operate on a single file at a time” 이기 때문에 다른 파일의 const enum을 참조해서 inline에 넣어야하는 과정에서 오류가 있는 듯 하다(추측임) (참고: https://www.typescriptlang.org/tsconfig#isolatedModules)
-
babel로 트랜스파일링이 안된다. → 대표적인 문제였는데 babel 7.16.0 부터 지원이 된다고 한다. (참고: https://www.cloudless.blog/post/babel-vs-tsc)
-
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',
};
tree-shaking도 잘 일어나고 형태의 안정성도 보장할 수 있다.
따라서 필자도 후자의 방법으로 열거형을 표현하고 있다.(js에서 열거형 쓰기 참 힘들다)
tree-shaking에 대해 좀 더 알아보자
-
es6 모듈내보내기 import/export 사용 tree-shaking은 es6의 모듈 방법으로만 작동한다. es6로 작성해야하는건 물론이며 babel이 commonJS로 변환하고 있지 않은지 체크해야한다.
-
사이드 이펙트 고려하기 각 모듈이 사이드 이펙트를 발생시키는지는 tree-shaking에 중요한 요소다. 사이드 이펙트가 없이 모듈을 분리하여 코드를 짜는게 우선 중요하겠다. 사이드 이펙트가 없이 코드를 짜도 번들러가 이 사실을 모를 수 있다. 따라서 번들러에게 알려주는 방법이 필요하다 package.json의 sideEffects 속성을 건드릴 수도 있고
/*#__PURE__*/
이노테이션을 통해서도 가능하다. 정확한 방법은 각 번들러 공식문서를 참고하자 https://webpack.kr/guides/tree-shaking/ -
원하는 것만 가져오기
// bad import * as utils from "../../utils/utils"; // good import { simpleSort } from "../../utils/utils";
필요한 모듈만 가져와서 쓰면 tree-shaking에 유리하다. 아니 유리했다. 예전에는 namespace 방식이나 전체 모듈을 불러오는게 tree-shaking을 지원하지 않는 듯 했으나 최근 버전의 번들러들은 네임스페이스도 지원하는 듯 하다. 그래도 아직 지원하지 않는 번들러가 있을 수도 있고 가독성 측면에서도 원하는 것만 가져오는게 좋다.
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
데일리 스크럼
- [Week1-Day1] 팀 빌딩
- [Week1-Day2] 데일리 스크럼
- [Week1-Day3] 데일리 스크럼
- [Week1-Day4] 데일리 스크럼
- [Week1-Day5] 데일리 스크럼
- [Week2-Day1] 스프린트 계획 회의
- [Week2-Day2] 데일리 스크럼
- [Week2-Day3] 데일리 스크럼
- [Week2-Day4] 데일리 스크럼
- [Week3-Day1] 스프린트 계획 회의
- [Week3-Day2] 데일리 스크럼
- [Week3-Day3] 데일리 스크럼
- [Week3-Day4] 데일리 스크럼
- [Week4-Day1] 스프린트 계획 회의
- [Week4-Day2] 데일리 스크럼
- [Week4-Day3] 데일리 스크럼
- [Week4-Day4] 데일리 스크럼
- [Week5-Day1] 스프린트 계획 회의
- [Week5-Day2] 데일리 스크럼
- [Week5-Day3] 데일리 스크럼
- [Week5-Day4] 데일리 스크럼
- [Week6-Day1] 스프린트 계획 회의
- [Week6-Day2] 데일리 스크럼
- [Week6 Day3] 데일리 스크럼
- [Week6 Day4] 데일리 스크럼