03/26
Enum 은 보통, 자주 사용해야 할 관련된 상수들을 정의해서 모아놓을 수 있기 때문에, 편리하게 종종 사용하게 된다. 다시 말해, 어떤 변수가 가질 수 있는 값의 후보군을 미리 정해두고 그 안에서만 선택하게 만들기 때문에, 관리가 편하고 휴먼에러를 줄일 수 있는 방법으로서 유용하다.
그런데 이런 Enum 도, TypeScript 에서 사용할 때는 문제가 생길 수 있다는데...
Tree - shaking 불가능
TS 는 enum 을 컴파일할 때 즉시 실행 함수(IIFE) 형태의 JavaScript 코드를 생성한다. 문제는, 이런 코드는 정적 분석이 어려워서 번들러(Webpack, Rollup 등) 가 사용하지 않는 코드를 제거하지 못하고 최종 번들에 포함시킨다는 것.
너무 본론만 설명해서 (두괄식의 장점이자 단점이라고 할 수 있다), 추가 설명이 필요해 보이는 부분들을 마저 설명해보자.
IIFE (Immediately Invoked Function Expression)
즉시 호출 함수 표현식은, 함수를 정의하자마자 곧바로 실행하는 JS 디자인 패턴이다.
(function () {
console.log("정의되자마자 바로 실행됩니다!");
})();
// 화살표 함수 버전
(() => {
console.log("이것도 즉시 실행됩니다.");
})();
익명함수라고 알고 사용하고 있던 그것. 이를 사용하는 이유는, 외부 스코프에서 접근할 수 없기에, 캡슐화와 전역 변수 오염 방지가 가능하다. 그렇기 때문에, 단 한 번만 실행되어야 하는, 설정이나 초기화 로직에 적합한 방식.
Tree - shaking
트리쉐이킹은 JS 번들링 과정에서 사용하지 않는 코드를 제거하여 최종 파일 크기를 줄이는 최적화 기술이다. 이를 마치 나무를 흔들어 죽은 잎사귀를 떨어뜨리는 행동에 비유한 것.
ES6 의 정적 모듈 구조(import/export) 에서는, 번들러가 코드를 실행하지 않고도 어떤 함수나 변수가 실제 사용되는지 분석하고, import 로 불러왔지만 코드 내에서 한 번도 참조되지 않은 항목은 최종 번들 파일에서 제외해 트리쉐이킹을 실행한다.
문제가 발생하는 부분은, TS 의 enum 은 컴파일 시 단순한 상수가 아니라 IIFE 를 포함한 복잡한 객체로 변환된다는 것.
컴파일 전의 TS 에 이런 enum 이 있다고 하면,
// TypeScript
enum Direction { Up, Down }
컴파일 하는 경우 JS 로는 이렇게 변환된다.
// JavaScript
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 0] = "Up";
Direction[Direction["Down"] = 1] = "Down";
})(Direction || (Direction = {}));
번들러는 위 코드가 Side Effect 를 가질 수 있다고 판단한다. 다시 말해서, 함수 내부에서 외부 변수를 조작할 가능성이 있기 때문에, Direction 을 실제로 사용하지 않더라도, 위험해서 삭제하지 못하는 코드로 분류한다는 것.
Reverse Mapping 의 혼란
역방향 매핑은, TS 의 숫자형 Enum 에서만 발생하는 특징으로, key >> value 를 찾을 수 있을 뿐만 아니라 value >> key 를 찾을 수 있는 기능을 말한다. 하지만, 이 과정에서 코드의 복잡성과 예측 불가능함을 발생시키는데,
단순한 enum 예시를 하나 들어보면,
// TypeScript
enum Status {
Success = 200,
}
컴파일된 JS 코드를 보면 그 이유를 알 수 있다.
// 컴파일된 JavaScript 결과 (이중 할당)
var Status;
(function (Status) {
Status[Status["Success"] = 200] = "Success";
})(Status || (Status = {}));
이 코드가 실행되면, 예시로 든 객체는 형태가 이상해지는데...
{
"Success": 200, // 정방향 (Key -> Value)
"200": "Success" // 역방향 (Value -> Key)
}
무려 양쪽을 모두 포함한 이상한(?) 객체가 된다????
이로 발생할 수 있는 문제는,
Key 를 순회하는 경우, enum 의 모든 항목을 반복문으로 돌릴 때, 숫자 값까지 키로 취급되어 출력된다. 이에 더해, 역방향 매핑 구조 때문에 선언되지 않은 임의의 숫자를 할당해도 TS 가 에러를 잡지 못하는 경우도 발생한다.
또한, 문자열을 값으로 사용하는 Enum 은 역방향 매핑을 생성하지 않으므로, 동일한 Enum 이더라도 타입에 따라 객체의 구조가 달라진다.
해결 방안
Const Assertion
as const 를 사용하면, 별도의 런타임 객체 생성 없이 Type 시스템을 활용할 수 있으며, 트리쉐이킹도 완벽하게 지원된다.
const Direction = {
Up: 0,
Down: 1,
} as const;
// 타입 추출
type Direction = typeof Direction[keyof typeof Direction];
Union Types
또는, 단순한 값의 집합만이 필요하다면 유니온 타입을 사용하는 것이 가장 가볍고 직관적이다.
type Status = 'Pending' | 'Success' | 'Failed';
정리하자면, 런타임 성능과 번들 크기 최적화를 위해, 또한 혼란을 방지하기 위해, enum 대신 const assertion 이나 Union Types 를 사용하는 것이 현대 TypeScript 에서 권장하는 관례. 하지만, 현실적으로는 진행중인 프로젝트에서 이미 사용하고 있는 방식이 있다면 그와 Convention 을 맞추는 것이 나을 수 있다.
'언어 > TypeScript' 카테고리의 다른 글
| 옵셔널 체이닝(?.) (0) | 2025.12.19 |
|---|---|
| declare (0) | 2025.12.02 |
| TypeScript import 오류들 (0) | 2025.11.25 |
| Union / Intersection Type (0) | 2025.01.08 |
| extends / implements (0) | 2024.09.23 |