[뽁] enum vs const enum vs as const

hansoom·2024년 3월 27일
0

BBOK

목록 보기
3/6
post-thumbnail

뽁 서비스를 개발하는 과정에서 토큰 access tokenrefresh token 이 두 가지의 토큰값과 isVisited 값 즉, 값에 따라서 온보딩 페이지로 redirect 시킬 지를 결정하는 value, 총 3가지 value를 쿠키에서 관리하기로 했다.

이 값들을 setget하는 함수들을 따로 libs폴더에 분리하여 리팩토링을 진행하는 과정에서 3가지의 value 값을 enum으로 아래와 같이 관리를 할지,

export enum CookieKey {
  accessToken = 'accessToken',
  refreshToken = 'refreshToken',
  isVisited = 'isVisited',
}

그리고 const enum 으로 아래와 같이 관리를 하는 것이 좋을 지

export const enum CookieKey {
  'accessToken' = 'accessToken',
  'refreshToken' = 'refreshToken',
  'isVisited' = 'isVisited',
}
export type TCookieKey = keyof typeof CookieKey;

as const로 상수로 관리하는 것이 좋을지 고민하게 되었다.

const CookieKey = {
  accessToken:'accessToken',
  refreshToken:'refreshToken',
  isVisited:'isVisited',
} as const;

enumconst enum 개념의 차이를 알기 전 Tree-shaking개념부터 살펴보고자 한다.

1. Tree-shaking 이란?

Tree-shaking이란 간단히 말해 사용하지 않는 코드를 삭제하는 기능을 말한다.

Tree-shaking을 통해 export 했지만 아무데서도 import 하지 않은 모듈이나 사용하지 않는 코드를 삭제해서 번들 크기를 줄여 페이지가 표시되는 시간을 단축할 수 있다.

2. enum 이란?

enum은 열거형 변수로 정수를 하나로 합칠 때 편리한 기능이다.
임의의 숫자나 문자열을 할당할 수 있으며 하나의 유형으로 사용해서 버그를 줄일 수 있다.

다음과 같이 아무것도 지정하지 않은 경우에는 0부터 숫자를 매깁니다.

export enum CookieKey {
  'accessToken', // 0
  'refreshToken', // 1
  'isVisited', // 3
}
console.log(CookieKey.accessToken); // 0

또 아래와 같이 임의의 숫자나 문자열을 할당할 수도 있다.

export enum CookieKey {
  'accessToken' = 'accesstoken',
  'refreshToken' = 'refreshtoken',
  'isVisited' = 'isvisited',
}

const value: CookieKey = CookieKey.accessToken; // 'accesstoken'

enum은 typescript가 자체적으로 구현하는 기능이다. javascript 에서는 사용할 수 없기 때문에 객체를 사용하는 코드를 자주 작성하게 된다.

2-1. enum 의 문제점

📝 enum의 문제점은 Tree-shaking이 지원하지 않는다는 점이다.
앞에서 enum의 개념을 살펴보았듯이 enum은 편리한 기능이지만, typescript가 자체적으로 구현했기 때문에 문제가 발생한다.

예시를 보면, 아래와 같이 typescript 코드를 구현하였다.

export enum MOBILE_OS {
  IOS,
  ANDROID
}

// 문자열을 할당한 경우
export enum MOBILE_OS {
  IOS = 'iOS',
  ANDROID = 'Android'
}

위의 코드를 트랜스 파일하면 아래와 같이 javascript 코드가 된다.

export var MOBILE_OS;
(function (MOBILE_OS) {
    MOBILE_OS[MOBILE_OS["IOS"] = 0] = "IOS";
    MOBILE_OS[MOBILE_OS["ANDROID"] = 1] = "ANDROID";
})(MOBILE_OS || (MOBILE_OS = {}));

// 문자열을 할당한 경우
export var MOBILE_OS;
(function (MOBILE_OS) {
    MOBILE_OS["IOS"] = "iOS";
    MOBILE_OS["ANDROID"] = "Android";
})(MOBILE_OS || (MOBILE_OS = {}));

javascript에 존재하지 않는 것을 구현하기 위해 typescript 컴파일러는 IIFE(즉시 실행 함수)를 포함한 코드를 생성한다.

그런데 Rollup과 같은 번들러는 IIFE를 '사용하지 않는 코드'라고 판단할 수 없어서 Tree-shaking이 되지 않는다.

결국 MOBILE_OS를 import 하고 실제로 사용하지 않더라고 최종 번들에는 포함된다

3. unionType 이란?

앞에서 살펴보았듯이 enum의 문제가 있기에 다음 대안으로는 unionType이 있다.

const MOBILE_OS = {
  IOS: 'iOS',
  Android: 'Android'
} as const;
type MOBILE_OS = typeof MOBILE_OS[keyof typeof MOBILE_OS]; // 'iOS' | 'Android'

위 코드는 아래와 같은 JavaScript 코드로 트랜스파일된다.

const MOBILE_OS = {
    IOS: 'iOS',
    Android: 'Android'
};

typescript 코드에서는 MOBILE_OS 타입을 정의한 이점을 그대로 누리면서,

javascript 로 트랜스 파일해도 IIFE가 생성되지 않으므로 Tree-shaking을 할 수 있게 된다.
=> javascript 객체로 enum 과 같은 타입 이점을 표현한 경우, 트랜스 파일된 javascript 와 별 차이가 없다!

4. const enum 이란?

그렇다면 const enumTree-shaking 관점에서 어떠할 지 알아볼 것이다.

typscript에서 const enum을 사용해보면 enum과 유사하다.
하지만 enum의 내용이 트랜스파일할 때 인라인으로 확장된다는 점에서 다르다.

const enum MOBILE_OS {
    IOS = 'iOS',
    ANDROID = 'Android',
}
const ios = MOBILE_OS.IOS

위 코드는 아래와 같은 JavaScript 코드로 트랜스파일된다.

const ios = "iOS" /* IOS */;

따라서 Tree-shaking 관점에서는 union types 와 같다. 즉, 사용하지 않으면 번들에 포함하지 않는다.

하지만 긴 문자열을 할당하는 경우에는 union types와 비교해 다소 불리한 점이 있다. 아래 예시를 살펴보면,

// TypeScript
const enum NAME {
  JUGEM = '寿限無寿限無五劫の擦り切れ海砂利水魚の…', // 일본에서 '김수한무 거북이와 두루미 삼천갑자 동방삭...'과 비슷한 용도로 사용하는 긴 이름입니다.
TARO = '다로', 
JIRO = '지로', 
} 
const isJugem = name === NAME.JUGEM
 
// JavaScript 트랜스파일 후
const isJugem = name === "u5BFFu9650u7121u5BFFu9650u7121u4E94u52ABu306Eu64E6u308Au5207u308Cu6D77u7802u5229u6C34u9B5Au306Eu2026" /* JUGEM */;

const enum은 constant member 만 가질 수 있다.
=> 컴파일시 코드가 사라져 보리기 때문에 constant 하지 않은 member 는 갖지 못한다.
'Const enum members are inlined at use sites.'

inline 간단한 개념
inline 처리된 함수 print()가 main() 함수에서 호출되고 있다.
이를 컴파일하면 아래와 같이 코드가 바뀐다.

#include <stdio.h>
inline void print()
{
    printf("Hello, world!\n");
}
int main()
{
    print();
    return 0;
}

컴파일 시

#include <stdio.h>
int main()
{
    printf("Hello, world!\n");
    return 0;
}

main() 함수에서 print() 함수 부분에 print() 함수 코드가 치환되어 들어가 있다. 이렇게 되면 우리는 함수의 호출 비용이나, 컴파일 후의 코드 양을 줄이는 등 성능 측면의 최적화를 이끌어낼 수 있다.

const enum은 inlined 가 되기 때문에, 코드가 가벼워지고 Tree-shaking이 가능하다.
또한 inlined 되기 때문에 사실상 readonly와 같이 동작한다.

4-1. const enum의 문제점

  • const enum은 Babel로 트랜스파일 될 수 없다.
    babel-plugin-const-enum를 사용하여 const enum → enum 또는 const enum → const object로 변경을 진행 시켜서 사용해야한다.

  • const enum은 런타임에 존재하지 않으며, 모든 참조가 컴파일 시점에 인라인으로 상수로 대체
    이로 인해 디버깅 시 원본 코드와 실제 실행 코드 간의 대응 관계를 이해하기 어려울 수 있다. 디버깅 시 변수명을 확인할 수 없고, 인라인된 상수 값만 보이게 된다.

  • 문자열 값을 유니코드로 생성해 빌드 크기가 커진다.
    프로젝트의 여러 파일에서 const enum을 참조할 경우, 각 참조마다 동일한 값이 복사되기 때문에 결과적으로 번들 크기가 커질 수 있다.
    (모든 참조가 컴파일 시점에 실제 값으로 인라인 치환)

const enum pitfall(https://www.typescriptlang.org/docs/handbook/enums.html#const-enum-pitfalls) 문제로 인해 const enum의 활용은 그리 권장되진 않는다. 그래서 3.4 버전 이후의 모던 타입스크립트에서부터는 as const 가 const enum의 역할을 대체하고 있다.

5. 그렇다면 어떤 거를 사용해야할까?

enum은 key-value 의 관계로 양방향 즉 이들의 관계성이 필요한 경우 사용하는 것이 좋을 거 같다. 다만, 사용하지 않는 코드도 존재할 수 있다는 Tree-shaking이 존재할 수 있다.
=> 🙆‍♀️양방향 mapping이 필요한 경우 사용해보자

const enum은 enum과 다르게 inlined 라는 특징으로 Tree-shaking이 가능하다는 장점에 존재하지만 앞서 보았듯 단점이 많이 파생되고 있다.
=> 🙆‍♀️ 차라리 inlined를 포기하고 as const를 사용해보자

as const는 Type의 추론 범위를 줄여서 해당 값 자체를 Type으로 만들어준다. 비록 양방향 바인딩을 해주진 못하지만, 상수가 아닌 것을 상수로 만들어 준다.
=> 🙆‍♀️ 양방향 mapping이 필요없고, value만 받아와 type 지정하고 싶다면 as const 를 사용해보자

6. 적용

  • libs 폴더의 쿠키 값 관리하는 함수들 정의
import { deleteCookie, getCookie, setCookie } from 'cookies-next';

import { CookieKey } from './cookieKey';

/**
 * 쿠키에서 아이템을 가져옴
 */
const getItemOrNull = async <T>(key: TCookieKey): Promise<T | null> => {
  try {
    const data = getCookie(key);
    return data ? (data as T) : null;
  } catch (error) {
    return null;
  }
};

/**
 * 쿠키에 아이템을 저장
 */
const setItem = <T>(key: TCookieKey, items: T) => {
  try {
    setCookie(key, items);
  } catch (error) {
    // TODO: log 파일에 저장
  }
};

/**
 * refresh token 값을 가져옴
 */
export const getRefreshToken = async () => {
  return getItemOrNull<string>(CookieKey.refreshToken);
};

/**
 * refresh token 값을 갱신
 */
const setRefreshToken = (refreshToken: string) => {
  setItem<string>(CookieKey.refreshToken, refreshToken);
};

/**
 * access token 값을 가져옴
 */
export const getAccessToken = async () => {
  return getItemOrNull<string>(CookieKey.accessToken);
};

/**
 * access token 값을 갱신
 */
const setAccessToken = (accessToken: string) => {
  setItem<string>(CookieKey.accessToken, accessToken);
};

/**
 * 최초 방문 여부를 확인
 */
export const checkIsVisted = () => {
  return !!getItemOrNull<boolean>(CookieKey.isVisited);
};

/**
 * 최초 방문 여부 값을 갱신
 */
export const setIsVisited = (value: boolean) => {
  setItem<boolean>(CookieKey.isVisited, value);
};

/**
 * 최초 방문 여부 값 삭제
 */
export const clearIsVisited = () => {
  deleteCookie(CookieKey.isVisited);
};
/**
 * access token, refresh token 값 삭제
 */
export const clearTokens = () => {
  deleteCookie(CookieKey.accessToken);
  deleteCookie(CookieKey.refreshToken);
};

/**
 * access token, refresh token 값 넣어줌
 */
export const setTokens = (accessToken: string, refreshToken: string) => {
  setAccessToken(accessToken);
  setRefreshToken(refreshToken);
};





출처
https://engineering.linecorp.com/ko/blog/typescript-enum-tree-shaking
https://xpectation.tistory.com/218

0개의 댓글