회사에서 enum을 정의할 때 아래와 같이 as const
를 붙여서 정의하고 있었는데 이유가 궁금해졌다.
const Status = {
TODO: 'TODO',
DOING: 'DOING',
DONE: 'DONE'
} as const;
요즘 애용하고 있는 chatGPT에게 물어보며 대화를 나눴더니 이해가 쏙쏙 되었다. 어떻게 이렇게 똑똑한지.. 괜히 개발자들의 미래를 위협하는 게 아니다🤓 나눴던 대화는 아래와 같다.
타입스크립트의 기능인 as const
는 타입스크립트 컴파일러에게 해당 변수의 값이 read-only여야 한다고 알려주는 역할을 합니다. 이것은 컴파일러가 타입 checking을 더 잘할 수 있도록 도와주고 에러를 방지할 수 있게 합니다.
Object.freeze()
는 성능에 영향을 미칠 수 있습니다. 런타임에 어떤 변화도 일어나지 않도록 끝까지 추적하게 만들기 때문입니다.
반면, as const
는 개발 단계에서 코드가 우리가 의도한대로 동작하게 한 뒤, 코드가 자바스크립트로 트랜스파일된 후에 사라집니다.
enum은 enumeration의 약자로 key(문자)와 value(미리 정의된 상수)로 이루어진 객체입니다.
당신의 예시는 enumeration의 간단한 구현체라 볼 수 있습니다. 그것이 미리 정의된 string 속성값을 가지고 있고 각각이 뚜렷하게 다른 값을 나타내기 때문입니다.
하지만 자바스크립트는 다른 언어들과 다르게 내장 enum 데이터 타입을 가지고 있지 않아 object로 대신할 수 있습니다. 타입스크립트에서는 enum 키워드를 사용해 enumeration을 정의할 수 있고 그건 좀 더 elegant(😱)한 방법입니다.
enum Status {
TODO = 'TODO',
DOING = 'DOING',
DONE = 'DONE'
};
위와 같이 enum으로 정의하면 코드를 더 읽기 쉽고 이해하기 쉽게 만들 수 있습니다. 또한 enum을 사용하면 스위치문을 사용하듯이 숫자에 기반한 표현식을 사용할 수 있고 reverse mapping을 생성할 수 있습니다.
그건 프로젝트의 요구사항에 따라 다릅니다. 만약 자바스크립트로 된 프로젝트에서 타입스크립트를 작성하고 싶다면 as const를 사용할 수 있습니다.
더 복잡하고 큰 프로젝트에서 더 일관성 있게 enumeration을 작성하고 싶다면 enum이 더 공식적이고 가독성, 유지보수 측면에서 좋습니다.
여기까지 대화를 나누고 나니 다른 자료도 궁금해졌다. 그러다 이 logqwerty.log 블로그를 보게 되었고 더 깊이 이해하게 되었다. 타입스크립트 공식문서의 내용에 따르면 enum
과 as const
는 탄생한 목적 자체가 다르다. as const
는 type assertion을 위해 만들어졌다. 아래 코드를 살펴보자
let x = "hello" // string으로 추론
let x = "hello" as const; // 'hello'로 추론
typescript는 let으로 선언할 경우에 'hello'라는 리터럴로 추론되는 게 아니라 상위 타입인 string으로 추론이 된다. as const
를 사용하면 리터럴 자체로 타입을 한정시킬 수 있다.
그렇다면 처음부터 const로 선언하면 될 것 같지만 object
나 array
는 const로 선언하더라도 내부 프로퍼티의 추론 범위가 한정되지 않는다.
// 블로그에서 본 예시 참고
const language = {
korean: "ko", // string으로 추론
english: "en", // string으로 추론
};
language.korean = "kkk"; // kkk로 korean프로퍼티가 변경
language.english = "eee"; // eee로 english프로퍼티가 변경
이때 as const
를 사용하여 리터럴 값으로 추론되게 만들고 에러를 방지할 수 있다.
const language = {
korean: "ko", // "ko"로 추론, readonly 프로퍼티로 변경
english: "en", // "en"로 추론, readonly 프로퍼티로 변경
} as const;
// Error: Cannot assign to 'korean' because it is a read-only property
language.korean = "kkk";
// Error: Cannot assign to 'english' because it is a read-only property.
language.english = "eee";
as const
와 enum
모두 사용하면 좋겠지만 enumeration을 생성할 목적으로 만들어진 enum
을 사용하는 것이 나아보인다.
위의 블로그를 더 보다보니 enum
과 const enum
에 관한 내용도 있었다. 이 두개는 모두 enumeration을 위해 사용할 수 있지만 트랜스파일된 결과물이 다르기 때문에 상황에 맞게 판단해야 한다고 한다. 판단의 기준은 reverse mapping에 있다.
reverse mapping은 위에서 chatGPT가 잠시 언급하기도 했다.
위와 같이 enum으로 정의하면 코드를 더 읽기 쉽고 이해하기 쉽게 만들 수 있습니다. 또한 enum을 사용하면 스위치문을 사용하듯이 숫자에 기반한 표현식을 사용할 수 있고 reverse mapping을 생성할 수 있습니다.
블로그에서 좋은 예시를 들어주었다. 아래 코드를 살펴보자
// transpile 이전
enum COLOR {
red,
blue,
green
}
// transpile 이후
var COLOR;
(function (COLOR) {
COLOR[COLOR["red"] = 0] = "red";
COLOR[COLOR["blue"] = 1] = "blue";
COLOR[COLOR["green"] = 2] = "green";
})(COLOR || (COLOR = {}));
이것은 쉽게 풀어보자면 아래와 같은 형태의 객체로 바뀌는 것이라 볼 수 있다.
var COLOR = {
red: 0,
blue: 1,
green: 2,
'0': 'red',
'1': 'blue',
'2': 'green'
}
(name
->value
)와 (value
->name
) 이렇게 양방향으로 저장이 되는 것이다.
반면 const enum은 역방향으로 매핑되지 않는다.
// transpile 이전
const enum Direction {
Up,
Down,
Left,
Right,
}
let directions = [
Direction.Up,
Direction.Down,
Direction.Left,
Direction.Right,
];
// transpile 이후
"use strict";
let directions = [
0 /* Direction.Up */,
1 /* Direction.Down */,
2 /* Direction.Left */,
3 /* Direction.Right */,
];
reverse mapping이 필요한 경우가 아니라면 불필요한 코드를 줄이기 위해 const enum
을 사용하는 것이 좋을 것 같다. 애초에 가독성이 장점인 enum에서 Color[0]과 같은 코드를 작성할 일은 적기 때문이다.
아래 예시는 에러 로깅시에 중요도를 숫자로 함께 나타낸 코드이다. 이처럼 index와 string 모두가 필요한 경우에 enum을 사용하는 게 더 낫다고한다.
enum LogLevel {
ERROR,
WARN,
INFO,
DEBUG,
}
function logImportant(level: LogLevel, message: string): void {
if (level > LogLevel.WARN) return;
const logLevel = LogLevel[level];
console.log(`[${logLevel}] ${message}`);
}
// logImportant(0, "This is a error level message.");
logImportant(LogLevel.ERROR, "This is a error level message.");
// logImportant(1, "This is a warn level message.");
logImportant(LogLevel.WARN, "This is a warn level message.");
// logImportant(2, "This is a info level message.");
logImportant(LogLevel.INFO, "This is a info level message.");
// logImportant(3, "This is a debug level message.");
logImportant(LogLevel.DEBUG, "This is a debug level message.");
// 출력
// [ERROR] This is a error level message.
// [WARN] This is a warn level message.