프론트엔드

typescript로 상수와 타입을 선언하는 방법: as const, enum

yoxxin 2023. 6. 16. 18:04

앱을 개발하다보면 상수를 선언해야할 때가 있다.

예를 들어 카드결제와 관련한 앱을 만든다고 생각해보자.

카드 식별을 할 때 각 은행별로 할당된 코드 code로 식별한다고 생각하자.

1. as const로 상수와 타입 선언하기

as const 키워드를 이용해 은행 코드를 상수로 선언할 수 있다

export const BankCodeList = {
  BCCard: '361',
  ShinHanCard: '366',
  KakaoBank: '090',
  HyunDaiCard: '367',
} as const;

그러면 BankCodeList의 value들에 해당하는 은행 코드 타입은 typeof, keyof 키워드를 이용해 선언할 수 있다.

export type BankCode = (typeof BankCodeList)[keyof typeof BankCodeList];
// BankCode '361' | '366' | '090' | '367'

또한 은행 코드와 카드 이름을 매핑하는 상수도 만들 수 있을 것이다.

export const CardName = {
  [BankCodeList.BCCard]: 'BC카드',
  [BankCodeList.ShinHanCard]: '신한카드',
  [BankCodeList.KakaoBank]: '카카오뱅크',
  [BankCodeList.HyunDaiCard]: '현대카드',
} as const;

2. enum으로 상수와 타입 선언하기

as const를 이용해 상수를 선언할 수도 있지만, 위 예제와 같이 string 타입의 상수를 선언하는 경우엔 enum을 고려할 수도 있다.

자바스크립트에는 없고 타입스크립트가 제공하는 enum에 대한 자세한 내용은 각자 공식문서를 참고해보고, 먼저 코드를 통해 핵심만 알아보자.

위에서 선언한 BankCodeList 객체와 BankCode타입을 enum을 이용하면 다음과 같이 선언할 수 있다.

// string enum
export enum BankCode {
  BCCard = '361',
  ShinHanCard = '366',
  KakaoBank = '090',
  HyunDaiCard = '367',
}

// enum은 타입, 값으로 사용할 수 있다.
const BCCardCode:BankCode.BCCard = BankCode.BCCard;
console.log(BCCardCode); // '361'
const backCode: BankCode = BankCode.KakaoBank; // BankCode.[key] 전부 할당 가능

엥? 타입 선언은 어디갔는지 의문을 가질 수 있다.

실제로 enum은 그 자체를 타입으로 사용할 수 있고, BankCode.BCCard 와 같이 런타임때 value로도 접근할 수 있다.

이렇게 string enum을 쓰면 타입선언을 위해 keyof typeof 같은 문법을 안쓰고 상수 선언과 동시에 타입까지 선언할 수 있다.

즉 타이핑이 줄어들어서 편리하다.

또한 enum은 사용처에서 리터럴이 아니라 enum 상수를 import 해서 사용하도록 강제하는데, 이는 개발자의 오타를 방지하는 효과도 있다.

부록: number enum

enum 객체 내부 value의 값이 number인 number enum의 경우 reverse mapping이라고 해서 컴파일이 특이하게 된다.

// number enum
export enum BankCode {
  BCCard = 361,
  ShinHanCard = 366,
  KakaoBank = 90,
  HyunDaiCard = 367,
}

// js 컴파일 시
export var BankCode;
(function (BankCode) {
    BankCode[BankCode["BCCard"] = 361] = "BCCard";
    BankCode[BankCode["ShinHanCard"] = 366] = "ShinHanCard";
    BankCode[BankCode["KakaoBank"] = 90] = "KakaoBank";
    BankCode[BankCode["HyunDaiCard"] = 367] = "HyunDaiCard";
})(BankCode || (BankCode = {}));

// reverse mapping으로 인해 값으로 키에 접근할 수 있다.
console.log(BankCode[BankCode.BCCard]) // BCCard
console.log(BankCode[361]) // BCCard

// reverse mapping으로 인해 Object.keys로 접근할 시 value까지 key값으로 들어간다.
console.log(Object.keys(BankCode)); // ["90", "361", "366", "367", "BCCard", "ShinHanCard", "KakaoBank", "HyunDaiCard"]

reverse mapping 때문에 사용할 때 혼란이 올 수 있고, 버그가 생길 수 있기 때문에 number enum의 사용은 추천하지 않는다.

즉 number타입의 상수가 필요할때는 enum을 지양하자.

부록2: tree shaking

enum 사용을 고려할 때, tree shaking도 고려대상이다.

Tree shaking is a term commonly used in the JavaScript context for dead-code elimination. – webpack doc

위 설명과 같이 tree shaking은 코드로 작성은 되어 있지만 런타임때 사용하지 않는 코드를 번들링 시 제거하는 기법이다.

따라서 번들 사이즈를 줄일 수 있다.

많은 포스팅에서 enum은 tree shaking이 안된다는 이유로 사용을 꺼린다고 하는데.. 직접 검증해보았다.

검증 방식은 enumimport는 하지만 직접 사용을 하지않는 경우를 코드로 작성했다.

Webpack

enum Direction이 App.tsx에서 import만 되었고, 사용되지 않아 트리셰이킹이 된 모습

"webpack": "^5.86.0" 기준 enumtreeshaking이 된다.

Rollup

Rollup은 enum을 사용하지 않음에도 import를 했다는 이유로 "dead-code"가 남아있다

“Rollup”: “3.25.1” 기준 enumtreeshaking이 되지 않는다.

vite

해당 글에서 viteenum treeshaking이 된다고 한다.

즉 번들러에 따라 enum tree-shaking 여부가 달라진다.

사실.. enum을 선언해서 사용하지 않는 일이 얼마나 될까?

또 번들링 사이즈를 불가피하게 극한으로 줄여야하는 상황이 아니라면 enum 선언 정도의 크기는 무시해도 괜찮지 않을까..라는 개인적인 의견이다.

마무리

타입스크립트에서 상수를 선언할 때는 as constenum을 사용할 수 있다.
number enum의 경우 reverse mapping이 일어나기 때문에 양방향 매핑이 꼭 필요한 경우가 아니라면 사용을 추천하지 않는다.
필요하다면 동작을 확실히 알고 사용하자
string 상수를 선언할 때는 enum을 고려해볼만 하다.

Reference

https://www.kabuku.co.jp/developers/good-bye-typescript-enum
https://techblog.woowahan.com/9804/#toc-3
https://xpectation.tistory.com/218
https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/#all-enums-are-union-enums
TS playground