as const란

권현경·2023년 1월 11일
1

회사에서 enum을 정의할 때 아래와 같이 as const를 붙여서 정의하고 있었는데 이유가 궁금해졌다.

const Status = {
    TODO: 'TODO',
    DOING: 'DOING',
    DONE: 'DONE'
} as const;

요즘 애용하고 있는 chatGPT에게 물어보며 대화를 나눴더니 이해가 쏙쏙 되었다. 어떻게 이렇게 똑똑한지.. 괜히 개발자들의 미래를 위협하는 게 아니다🤓 나눴던 대화는 아래와 같다.

나: as const가 뭐야?

타입스크립트의 기능인 as const는 타입스크립트 컴파일러에게 해당 변수의 값이 read-only여야 한다고 알려주는 역할을 합니다. 이것은 컴파일러가 타입 checking을 더 잘할 수 있도록 도와주고 에러를 방지할 수 있게 합니다.


나: read-only로 불변성을 보장하는거라면 Object.freeze를 사용하는 방법도 있지 않나?

Object.freeze()는 성능에 영향을 미칠 수 있습니다. 런타임에 어떤 변화도 일어나지 않도록 끝까지 추적하게 만들기 때문입니다.

반면, as const는 개발 단계에서 코드가 우리가 의도한대로 동작하게 한 뒤, 코드가 자바스크립트로 트랜스파일된 후에 사라집니다.


나: 내가 위에서 정의한 Status는 enum이 맞지?

enum은 enumeration의 약자로 key(문자)와 value(미리 정의된 상수)로 이루어진 객체입니다.
당신의 예시는 enumeration의 간단한 구현체라 볼 수 있습니다. 그것이 미리 정의된 string 속성값을 가지고 있고 각각이 뚜렷하게 다른 값을 나타내기 때문입니다.

하지만 자바스크립트는 다른 언어들과 다르게 내장 enum 데이터 타입을 가지고 있지 않아 object로 대신할 수 있습니다. 타입스크립트에서는 enum 키워드를 사용해 enumeration을 정의할 수 있고 그건 좀 더 elegant(😱)한 방법입니다.

enum Status {
    TODO = 'TODO',
    DOING = 'DOING',
    DONE = 'DONE'
};

위와 같이 enum으로 정의하면 코드를 더 읽기 쉽고 이해하기 쉽게 만들 수 있습니다. 또한 enum을 사용하면 스위치문을 사용하듯이 숫자에 기반한 표현식을 사용할 수 있고 reverse mapping을 생성할 수 있습니다.


나: 그럼 enum과 as const 중에 enum을 더 추천하는거야?

그건 프로젝트의 요구사항에 따라 다릅니다. 만약 자바스크립트로 된 프로젝트에서 타입스크립트를 작성하고 싶다면 as const를 사용할 수 있습니다.

더 복잡하고 큰 프로젝트에서 더 일관성 있게 enumeration을 작성하고 싶다면 enum이 더 공식적이고 가독성, 유지보수 측면에서 좋습니다.


여기까지 대화를 나누고 나니 다른 자료도 궁금해졌다. 그러다 이 logqwerty.log 블로그를 보게 되었고 더 깊이 이해하게 되었다. 타입스크립트 공식문서의 내용에 따르면 enumas const는 탄생한 목적 자체가 다르다. as const는 type assertion을 위해 만들어졌다. 아래 코드를 살펴보자

let x = "hello" // string으로 추론
let x = "hello" as const; // 'hello'로 추론

typescript는 let으로 선언할 경우에 'hello'라는 리터럴로 추론되는 게 아니라 상위 타입인 string으로 추론이 된다. as const를 사용하면 리터럴 자체로 타입을 한정시킬 수 있다.

그렇다면 처음부터 const로 선언하면 될 것 같지만 objectarray는 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 constenum 모두 사용하면 좋겠지만 enumeration을 생성할 목적으로 만들어진 enum을 사용하는 것이 나아보인다.

enum과 const enum

위의 블로그를 더 보다보니 enumconst 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.
profile
프론트엔드 개발자

0개의 댓글