- Published on
enum, as const, Object.freeze() 그리고 const enum
- Authors
- Name
- 윤영서
원티드 프리온보딩 인턴쉽 당시, 다른 분께서 routing path를 Object.freeze() 를 이용해 Path를 설정해주는 방법을 제시하셨습니다. 사실 Router에서 ‘home’, ‘detail’과 같은 상수들을 하드코딩하는 기분이 썩 좋지는 않았는데, 그런 상수들을 하나의 객체로 묶어 처리해줄 수 있음에 굉장히 신선한 충격을 받았던 기억이 납니다.
그래도 어디선가 들은게 있었던 저는, 작성하신 분께 여쭤봅니다. ‘혹시 as const
를 사용하지 않고 Object.freeze()
를 사용하신 이유가 있으실까요?’ 그 당시에 Object.freeze() 를 사용하셨던 분은 그냥 습관처럼 작성하셨다고 대답하셨으나, 시간이 지난 지금 const enum, as const, enum, Object.freeze()
가 어떤 측면에서 다른 것인지 궁금해져 학습해봅니다.
아래는 Chart의 색상을 지정해주기 위해 객체를 사용하여 상수 값을 정의하는 방법들입니다. 모두 IDE의 자동 완성 기능을 사용할 수 있게 됩니다.
//as const
const CHART_COLOR = {
AREA: '#EC8090',
BAR: '#9EA1FF',
FILTERED_BAR: '#4548a2',
} as const;
//Object.freeze
const FREEZED_CHART_COLOR = Object.freeze({
AREA: '#EC8090',
BAR: '#9EA1FF',
FILTERED_BAR: '#4548a2',
});
//enum
enum ENUM_CHART_COLOR {
AREA = '#EC8090',
BAR = '#9EA1FF',
FILTERED_BAR = '#4548a2',
}
//const enum
const enum CONST_ENUM_CHART_COLOR {
AREA = '#EC8090',
BAR = '#9EA1FF',
FILTERED_BAR = '#4548a2',
}
각각은 다음과 같은 타입을 가집니다.
//as const
const CHART_COLOR: {
readonly AREA: "#EC8090";
readonly BAR: "#9EA1FF";
readonly FILTERED_BAR: "#4548a2";
}
//Object.freeze
const FREEZED_CHART_COLOR: Readonly<{
AREA: "#EC8090";
BAR: "#9EA1FF";
FILTERED_BAR: "#4548a2";
}>
//enum
enum ENUM_CHART_COLOR
//const enum
const enum CONST_ENUM_CHART_COLOR
Object.freeze()와 as const
우선, 타입을 봤을 때 닮은꼴인 Object.freeze()
와 as const
에 대해 알아보겠습니다.
//as const
const CHART_COLOR: {
readonly AREA: '#EC8090';
readonly BAR: '#9EA1FF';
readonly FILTERED_BAR: '#4548a2';
};
//Object.freeze
const FREEZED_CHART_COLOR: Readonly<{
AREA: '#EC8090';
BAR: '#9EA1FF';
FILTERED_BAR: '#4548a2';
}>;
두 방식 모두 readonly
라는 키워드를 볼 수 있는데, 어떤 부분이 다른 것일까요?
우선, as const
는 각 객체의 속성 값을 고유한 리터럴 타입
으로 assertion 합니다. (여기서 AREA는 “#EC8090”, BAR는 “#9EA1FF”등). 그리고 readonly
키워드가 중첩된 모든 프로퍼티에 재귀적으로 붙습니다. 또한, 컴파일
때 작용하고, 런타임때는 작용하지 않습니다.
Object.freeze
는 자바스크립트에서 객체를 불변(immutable)
하게 만드는 방식입니다. as const 는 각 객체의 속성에 readonly 키워드가 붙는 반면, Object.freeze()는 유틸리티 타입인 Readonly<Type>
를 타입으로 가집니다. 4.7.4버전 이전에는 객체의 속성들을 primitive type으로 assertion했으나, 이후 버전부터는 as const 와 마찬가지로 1 depth
까지는 리터럴 타입으로 assertion합니다.
//typescript 4.7.4 이전 버전
const FREEZED_CHART_COLOR: Readonly<{
AREA: string;
BAR: string;
FILTERED_BAR: {
DARK: string;
};
}>;
//typescript 4.7.4 이후 버전
const FREEZED_CHART_COLOR: Readonly<{
AREA: '#EC8090';
BAR: '#9EA1FF';
FILTERED_BAR: {
DARK: string;
};
}>;
게다가 Object.freeze()
로 동결된 객체를 변경하려 하면, 런타임
때 오류가 발생합니다.
FREEZED_CHART_COLOR.BAR = '다크'; //런타임때 오류
CHART_COLOR.BAR = '다크'; //컴파일시만 오류
console.log(FREEZED_CHART_COLOR.BAR); //런타임 오류로 변경 안됨
console.log(CHART_COLOR.BAR); //컴파일시만 오류, 런타임때는 정상 진행되어 "다크" 출력
중첩된 객체를 통해 살펴보면, 둘의 차이는 더욱더 분명해집니다.
const CHART_COLOR = {
AREA: '#EC8090',
BAR: '#9EA1FF',
FILTERED_BAR: {
DARK: '#4548a2',
},
} as const;
//as const의 타입
const CHART_COLOR: {
readonly AREA: '#EC8090';
readonly BAR: '#9EA1FF';
readonly FILTERED_BAR: {
readonly DARK: '#4548a2';
};
};
const FREEZED_CHART_COLOR = Object.freeze({
AREA: '#EC8090',
BAR: '#9EA1FF',
FILTERED_BAR: {
DARK: '#4548a2',
},
});
//Object.freeze의 타입
const FREEZED_CHART_COLOR: Readonly<{
AREA: '#EC8090';
BAR: '#9EA1FF';
FILTERED_BAR: {
DARK: string;
};
}>;
위와 같이, 중첩된 객체의 타입 추론
적인 부분에서 생각을 하게 되면, Object.freeze()
보다는 as const
가 더 적절해 보입니다. 리터럴 타입으로 타입 좁히기가 가능해지기 때문입니다.
enum과 const enum
enum
은 열거형으로, 이름이 있는 상수들의 집합을 정의할 수 있습니다. 그리고 열거형 값에 접근할 때, 추가로 생성된 코드 및 추가적인 간접 참조에 대한 비용을 피하기 위해 const enum
을 사용할 수 있습니다.
특이한 점은, enum은 숫자형일때 정방향 (name -> value) 매핑과 역방향 (value -> name) 매핑 모두가 가능하다는 점입니다. (문자형일때는 불가능)
enum Enum {
A,
}
let a = Enum.A;
let nameOfA = Enum[a];
console.log(a); //0
console.log(nameOfA); // "A"
다음과 같은 enum과 const enum 코드가 있습니다.
//enum
enum ENUM_CHART_COLOR {
AREA = '#EC8090',
BAR = '#9EA1FF',
FILTERED_BAR = '#4548a2',
}
//const enum
const enum CONST_ENUM_CHART_COLOR {
AREA = '#EC8090',
BAR = '#9EA1FF',
FILTERED_BAR = '#4548a2',
}
이를 트랜스파일하면 다음과 같은 자바스크립트 코드가 생성됩니다.
var ENUM_CHART_COLOR;
(function (ENUM_CHART_COLOR) {
ENUM_CHART_COLOR['AREA'] = '#EC8090';
ENUM_CHART_COLOR['BAR'] = '#9EA1FF';
ENUM_CHART_COLOR['FILTERED_BAR'] = '#4548a2';
})(ENUM_CHART_COLOR || (ENUM_CHART_COLOR = {}));
enum
은, ECMAScript에서 지원하는 표준 문법이 아닙니다. 타입스크립트에서 자체적으로 구현한 문법입니다. 자바스크립트에서 존재하지 않는 것을 구현하기 위해 타입스크립트 컴파일러는 즉시 실행 함수
를 포함한 코드를 생성합니다. 그런데 Rollup과 같은 번들러는 즉시 실행 함수를 '사용하지 않는 코드'라고 판단할 수 없어서 Tree-shaking
이 되지 않습니다.
✏️Tree-Shaking이란?
코드 최적화 과정에서 적용되는 사용하지 않은 코드 제거를 의미합니다. Tree-shaking은 번들(bundle)의 시작 지점에서 시작하여 실행될 수 있는 함수만 포함하고 나머지 미사용 함수를 제거함으로써 번들 전체에서 사용되지 않는 함수를 제거합니다.
간단히 말해, Tree-shaking은 번들에 포함된 코드 중에서 실제로 사용되지 않는 코드를 식별하고 제거하여 번들의 크기를 줄이고 실행 속도를 향상시키는 과정입니다. 이것은 웹 개발에서 자바스크립트 번들링과 같은 상황에서 자주 사용되며, 불필요한 코드를 제거하여 최적화된 번들을 생성하는 데 도움이 됩니다.
Tree-shaking이 되지 않는 문제 이외에도, enum은 타입스크립트의 “a typed superset of JavaScript”라는 컨셉에 맞지 않습니다. const가 아닌 enum은 자바스크립트와 호환되지 않는 구문으로 런타임에 존재하는 자바스크립트 객체를 생성하여 이 개념을 위반합니다.
enum의 코드가 자바스크립트로 트랜스파일 시 상당히 길어진 것과 대조되게, const enum
의 코드는 찾아볼 수도 없이 사라진 것을 볼 수 있습니다. 이는 const enum의 코드가 컴파일 될 시에 값이 상수로 치환되어 인라인되기 때문입니다. 그렇기 때문에 이를 다른 말로 생각해보면 런타임 시 값을 계산하는 것이 허용되지 않음
을 의미한다고 생각할 수 있습니다.
Tree-shaking의 관점에서 살펴보면, 코드의 번들 사이즈는 Union Types < const enum < const object(as const) < enum
순으로 사이즈가 작습니다. 그럼 여기서 의문이 생깁니다. ‘용량으로 보면 const enum이 더 좋은거 아닌가?’
Kabuku의 블로그에서는 const enum을 사용하지 말아야 하는 이유에 대해 다음과 같이 이야기하고 있습니다:
Kabuku의 블로그 : const enum을 사용하지 말아야 하는 이유
※ 이 글이 쓰여진 시점은 타입스크립트의 버전이 3.8.2일 당시임을 미리 작성합니다.
1. const enum은 Babel로 전송할 수 없습니다.
이 문제에 대해서는 현재 두 개의 workaround가 있습니다. 하나는 const enum을 일반 enum으로 다시 쓰는 것, 다른 하나는 babel-plugin-const-enum플러그인을 사용하여 전송 과정에서 자동으로 수행하는 것입니다.
2. 앰비언트 컨텍스트에서 const enum을 사용하면 문제가 될 수 있습니다.
앰비언트 컨텍스트란 타입스크립트에서 선언만 하고 실제 구현은 다른 곳에서 제공되는 코드 환경을 가리킵니다. 일반적으로 외부 라이브러리나 프레임워크의 유형 정의를 선언할 때 사용됩니다( _.d.ts파일 안이나 declare구문처럼).--isolatedModules 컴파일 옵션을 활성화하면 앰비언트 컨텍스트에서 선언된 const enum에 다른 모듈(다른 파일)에서 액세스하면 컴파일 오류가 발생합니다. const enum을 내부적으로 밖에 이용하지 않고, 그 코드가 컴파일 될 때의 옵션을 100% 제어할 수 있으면 좋지만, 그렇지 않으면 문제가 됩니다.
npm에 공개하는 것으로 const enum를 export 하는 것에 관해서는 명확하게 잘못이라고 말할 수 있습니다 . 타입스크립트팀의 멤버도 이를 바탕으로 “const enum on DT really does not make sense” (DT는 DefinitelyTyped)이며, 앰비언트 컨텍스트에서는 “You should use a union type of literals (string or number) instead.” 라고 말합니다.
3. --isolatedModules이 유효한 경우의 const enum의 동작은 앰비언트 컨텍스트 밖에서도 이상합니다.
GitHub의 코멘트를 읽고 알았는데, --isolatedModules을 활성화하고 const enum을 컴파일하면 다음과 같이 나타납니다.
컴파일 전:
export const enum A { B, } export const enum A { C = 2, } /// b.ts import { A } from './a'; console.log(A.B, A.C);
컴파일 후:
/// a.js export var A; (function (A) { A[(A['B'] = 0)] = 'B'; })(A || (A = {})); (function (A) { A[(A['C'] = 2)] = 'C'; })(A || (A = {})); //# sourceMappingURL=a.js.map /// b.js console.log(A.B, A.C); //# sourceMappingURL=b.js.map
--isolatedModules는 컴파일을 1모듈(1파일)씩 실시할 수 있도록 하는 옵션이므로 const enum의 출력 결과가 통상의 enum과 같이 되는 것은 범위내라는 가정하에 있습니다. 다른 모듈(다른 파일)에 존재하는 const enum 정의 원래 정보를 참조할 수 없는 상황에서는 const enum의 원래 동작인 인라인화를 실현할 수 없습니다.
추가적으로, const enum으로 작성된 코드는 자바스크립트로 트랜스파일 시 문자열을 유니코드로 변환하기 때문에, 빌드 코드가 길어질 수 있다는 단점이 있습니다.
// TypeScript
const enum NAME {
JUGEM = '寿限無寿限無五劫の擦り切れ海砂利水魚の…', // 일본에서 '김수한무 거북이와 두루미 삼천갑자 동방삭...'과 비슷한 용도로 사용하는 긴 이름입니다.
TARO = '다로',
JIRO = '지로',
}
const isJugem = name === NAME.JUGEM;
// JavaScript 트랜스파일 후
const isJugem =
name ===
'u5BFFu9650u7121u5BFFu9650u7121u4E94u52ABu306Eu64E6u308Au5207u308Cu6D77u7802u5229u6C34u9B5Au306Eu2026'; /* JUGEM */
그렇다면, Enum의 대체제는 없을까요?
as const와 union type
typescript 공식문서에서는 다음과 같이 이야기 하고 있습니다.
In modern TypeScript, you may not need an enum when an object with as const could suffice:
The biggest argument in favour of this format over is that it keeps your codebase aligned with the state of JavaScript, and when/if enums are added to JavaScript then you can move to the additional syntax.
현대의 TypeScript에서는 객체에 as const를 사용하여 상수를 정의할 때 열거형(enum)이 필요하지 않을 수 있습니다:
이 방식을 선호하는 가장 큰 이유는 JavaScript의 현재 상태와 코드베이스를 일치시키는 데 있습니다. 그리고 만약 JavaScript에 나중에 열거형이 추가된다면, 그 때 추가적인 구문으로 전환할 수 있을 것입니다.
자바스크립트의 superset이라는 타입스크립트의 컨셉에서도 벗어나지 않고, Tree-shaking관점에서도 벗어나지 않는 as const와 keyof키워드를 사용해 코드를 작성하면, 다음처럼 가능합니다.
const CHART_COLOR = {
AREA: '#EC8090',
BAR: '#9EA1FF',
FILTERED_BAR: '#4548a2',
} as const;
type CHART_COLOR_TYPE = (typeof CHART_COLOR)[keyof typeof CHART_COLOR];
//CHART_COLOR_TYPE의 타입
type CHART_COLOR_TYPE = '#EC8090' | '#9EA1FF' | '#4548a2';
이와 같이, as const와 keyof키워드를 통해 union type을 생성하게 되면, enum과 비슷한 효과를 내고, 타입스크립트의 컨셉에 충실한 코드의 작성이 가능해집니다.
✏️ 출처
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze
https://learntypescript.dev/10/l5-deep-immutable https://www.typescriptlang.org/ko/docs/handbook/utility-types.html#readonlytype https://www.typescriptlang.org/ko/docs/handbook/enums.html https://en.wikipedia.org/wiki/Tree_shaking https://engineering.linecorp.com/ko/blog/typescript-enum-tree-shaking https://handhand.tistory.com/277#2️⃣-const-enum https://www.kabuku.co.jp/developers/good-bye-typescript-enum