본문 바로가기

[Project] 프로젝트 삽질기23 (feat Enum)

어가며

사이드 프로젝트에서 푸시 알림을 활용한 서비스를 개발하고 있습니다. 개발하는 과정에서, 팀원이 제게 Enum은 왜 사용했는지 물었습니다. 그 과정에서 관련된 대답을 제대로 할 수 없었습니다. 코드를 생각하지 않고 작성하고 있었다는 것을 깨달았습니다. 지금부터라도 코드를 생각하면서 작성하기 위해 노력하고자 합니다. 이 글은 Enum에 대한 생각을 정리하기 위해 작성됐습니다.

 

 
 

 


 
 
 
 

 

 

 

 

 

 

Enum, 사용할까? 말까? 

프로젝트를 개발하면서, Enum을 활용한 코드를 작성했습니다. 그러다 팀원이 Enum을 사용하면 Tree-Shaking 관점에서 비효율적이지 않나요라는 물음을 줬고, 이 물음에 대답할 수 없었습니다. Enum을 활용하면 키로 값을 얻을 수 있고, 값으로 키를 얻을 수 있어서 다양하게 활용할 수 있다고 생각했습니다. 하지만 Enum이 어떤 점에서 비효율적인 코드를 생성하는지는 알 수 없었습니다. 이번 기회에 Enum은 무엇인지, 생각 없이 Enum을 활용하면 어떤 점이 좋지 못하는지, 그럼에도 불구하고 Enum을 활용하는 이유는 무엇인지 알아야겠다고 생각했습니다. 그럼 먼저 Enum이란 무엇인지 알아보겠습니다.

 

 

 

 


 

 

 

 

 

 

Enum이란

이넘은 특정 값들의 집합을 의미하는 자료형입니다. 타입스크립트에서 이넘은 문자형과 숫자형을 지원합니다. 

 

enum Direction {
  Up,
  Down,
  Left,
  Right,
}

 

위와 같이 설정하면 각 0, 1, 2, 3의 값을 갖게 됩니다. 아래와 같은 예를 숫자형 이넘이라 합니다.

 

Direction.Up // 0
Direction.Down // 1
Direction.Left // 2
Direction.Right // 3

 

아래처럼 작성하면 문자형 이넘을 활용할 수 있습니다.

 

enum Direction {
  Up = 'UP',
  Down = 'DOWN',
  Left = 'LEFT',
  Right = 'RIGHT',
}

 

 

이넘에서 keyof를 사용할 때 주의해야 합니다. keyof를 사용해야 되는 상황에서는 keyof typeof를 사용해야 합니다.

 

 

enum LogLevel {
  ERROR, WARN, INFO, DEBUG
}

// 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
type LogLevelStrings = keyof typeof LogLevel;

function printImportant(key: LogLevelStrings, message: string) {
    const num = LogLevel[key];
    if (num <= LogLevel.WARN) {
       console.log('Log level key is: ', key);
       console.log('Log level value is: ', num);
       console.log('Log level message is: ', message);
    }
}
printImportant('ERROR', 'This is a message');

 

 

 

 

 

 

또한 숫자형 이넘을 사용할 때는 리버스 매핑을 활용할 수 있습니다. 이넘의 키(key)로 값(value)을 얻을 수 있고 값(value)으로 키(key)를 얻을 수도 있습니다.

 

enum Enum {
  A
}
let a = Enum.A; // 키로 값을 획득 하기
let keyName = Enum[a]; // 값으로 키를 획득 하기

 

 

하지만 리버스 매핑은 문자형 이넘에는 존재하지 않습니다. 그럼 이런 Enum에는 어떤 문제점이 있을까요? 이에 대해 알아보겠습니다. 

 

 

 

 

 

 


 

 

 

 

 

 

Enum의 문제점

결론부터 작성해보면, Enum을 활용하면 Tree-shaking이 되지 않습니다. Tree-shaking이란 사용하지 않는 코드를 삭제하는 기능을 말합니다. 나무를 흔들면 죽은 잎사귀들이 떨어지는 모습에 착안해 Tree-shaking이라고 부릅니다. Tree-shaking을 통해 export 했지만 아무 데서도 import하지 않은 모듈이나 사용하지 않는 코드를 삭제하는 것을 말합니다. 

 

 

enum Direction {
  Up = 'UP',
  Down = 'DOWN',
  Left = 'LEFT',
  Right = 'RIGHT',
}

 

 

만약 위의 코드를 트랜스 파일 하면 아래와 같은 JS 코드가 됩니다.

 

 

'use strict'
var Direction
;(function (Direction) {
  Direction['Up'] = 'UP'
  Direction['Down'] = 'DOWN'
  Direction['Left'] = 'LEFT'
  Direction['Right'] = 'RIGHT'
})(Direction || (Direction = {}))

 

 

JS에 존재하지 않는 것을 구현하기 위해 TS 컴파일러는 IIFE(즉시 실행 함수)를 포함한 코드를 생성합니다. 그런데 일부 번들러는 IIFE를 '사용하지 않는 코드'라고 판단할 수 없어 Tree-shaking이 되지 않습니다. 

 

 

 

 


 

 

 

 

 

 

Enum의 대안

Tree-Shaking이 되지 않을 수 있는 Enum 대신, 사용할 수 있는 것으로 const enum과 Union이 있습니다. 

 

const enum과 Union

아래와 같이 const enum을 활용하면 아래의 코드처럼 변경됩니다.

 

const enum Direction {
  Up = 'UP',
  Down = 'DOWN',
  Left = 'LEFT',
  Right = 'RIGHT',
}

const left = Direction.Left
'use strict'
const left = 'LEFT' /* Left */

 

 

일반 Enum을 활용하는 것보다, tree-shaking 문제를 해결함에 있어서 효과적이라고 볼 수 있습니다. 그러나 만약 --isolatedModules가 켜져 있다면, 해당 작업을 수행할 수 없게 됩니다. 또한 const enum은 Babel로 트랜스파일할 수 없다는 특징이 있습니다. 즉 const enum 보다 Union을 활용하는 것을 검토해봐야 합니다.

 

 

const Direction = {
  Up: 'UP',
  Left: 'LEFT',
  Right: 'RIGHT',
  Down: 'DOWN',
} as const

type Direction = typeof Direction[keyof typeof Direction]

for (const d of Object.values(Direction)) {
  console.log(d)
}

 

위 코드를 보면, as const를 사용하여, Direction에 const assertion을 추가했습니다. 이는 타입스크립트에서 타입 추론의 범위를 줄이는 효과를 가져옵니다. 여기서 const assertion이란 무엇일까요? 간단히 알아보겠습니다.

 

 

const assertion

타입스크립트는 타입 추론 기능을 지원합니다. 변수에 할당하는 리터럴의 타입을 보고 해당 변수의 타입을 자동으로 지정해줍니다. 

 

 

let hello = 'world';

 

예를 들어 위와 같이 코드를 작성했다고 한다면, 타입을 직접 지정해도 되겠지만, 타입 추론으로 인하여 hello라는 변수에 아래와 같이 타입이 지정됩니다.

 

 

 

출처 : https://medium.com/@seungha_kim_IT/typescript-3-4-const-assertion-b50a749dd53b

 

 

위에서는 let 변수로 선언해서 사용했지만, const 변수로 선언하면 string 대신 world 타입으로 추론됩니다.

 

 

 

출처 : https://medium.com/@seungha_kim_IT/typescript-3-4-const-assertion-b50a749dd53b

 

 

let, const 어떻게 선언하냐에 따라 타입 추론 방식이 달라집니다. 이때 const assertion을 활용하면 let도 const와 같이 타입 추론 규칙을 지정할 수 있습니다. 

 

let hello = 'world' as const; // ts, tsx 파일에서
let hello = <const>'world'; // ts 파일에서

 

위와 같이 작성하면 아래와 같이 타입 추론이 됩니다.

 

 

출처 : https://medium.com/@seungha_kim_IT/typescript-3-4-const-assertion-b50a749dd53b

 

 

let 변수로 hello을 선언했음에도 마치 const 변수로 선언한 것처럼 타입 추론이 진행돼습니다. 만약 hello에 다른 값을 할당하려고 하면, 컴파일 타임 에러가 나게 됩니다. 이런 const assertion은 객체에서 잘 활용될 수 있습니다.

 

 

출처 : https://medium.com/@seungha_kim_IT/typescript-3-4-const-assertion-b50a749dd53b

 

 

자바스크립트의 경우 변수가 const로 선언되었더라도, 객체 내부의 속성은 변경될 수 있습니다. 이런 경우 타입 추론의 범위를 좁혀주기 위해 const assertion을 사용할 수 있습니다.

 

 

// 하나의 속성에 대한 const assertion
const obj = {
  hello: 'world' as const,
  foo: 'bar'
};
// 모든 속성에 대한 const assertion
const obj = {
  hello: 'world',
  foo: 'bar'
} as const;

 

위와 같이 const assertion을 활용하면 아래와 같이 나오게 됩니다.

 

 

출처 : https://medium.com/@seungha_kim_IT/typescript-3-4-const-assertion-b50a749dd53b

 

위의 경우는 하나의 속성에 const assertion을 적용한 경우입니다.

 

 

출처 : https://medium.com/@seungha_kim_IT/typescript-3-4-const-assertion-b50a749dd53b

 

 

위의 경우는 모든 속성에 대한 const assertion을 적용한 경우입니다. 즉 const assertion을 활용하면 타입 추론의 범위를 좁혀줄 수 있습니다. 그럼 다시 아래의 예시를 살펴보겠습니다.

 

 

 

 

 

 

const로 선언했다 할지라도, 객체이기 때문에 바뀔 수 있는데 Direction에 as const를 사용함으로써 readonly를 강제할 수 있습니다. 이를 통해 타입을 정의한 이점을 그대로 가져오면서, IIFE가 생성되지 않아서 Tree-Shaking 할 수 있습니다. 그럼 이런 Enum은 어떤 목적 때문에 사용하는 것일까요? 이에 대해 알아보겠습니다. 

 

 

 

 

 


 

 

 

 

 

 

 

 

Enum을 사용하는 목적

TypeScript enum은 JavaScript에는 없는 개념으로, 일견 상수, 배열, 혹은 객체와 비슷해 보이기도 합니다. 그냥 상수, 배열, 객체를 써도 될 것 같은데, 굳이 enum을 쓰는 이유가 뭘까요?

 

 

추상화 수단

Enum은 객체를 외부에서 어떤 형태로, 어떻게 이용하게 할 것인가를 고민할 수 있게 만들어주는, 여러 클래스에 걸쳐서 공통적으로 사용되는 객체의 규격을 정의하는 추상화의 수단입니다. 예를 들어, 위에서 반복해서 봤던 Direction을 살펴보겠습니다. 

 

const Direction: string = 'up' // string?

 

 

위와 같이 사용한다면, 타입이 string 타입이 됩니다. 하지만 string 타입을 활용하기엔 범위가 넓습니다. 이를 개선하기 위해 리터럴 타입과 유니온을 활용해서 Direction 변수에 대한 타입의 범위를 좁힐 수 있습니다.

 

 

type Direction = 'up' | 'down' | 'left' | 'right'
const code: Direction = 'up'

 

 

위와 같이 code 변수에 만약 'upTown'과 같이 타입에 맞지 않는 값을 대입하면 컴파일 에러가 나게 됩니다. 위와 같이 타입의 범위를 좁힐 수 있지만, 만약 살펴봐야 할 방향이 많아지게 된다면 코드가 길어지게 됩니다. 이런 경우 enum을 사용해서 리터럴의 타입과 값에 이름을 붙여서 코드의 가독성을 높일 수 있습니다.

 

 

export enum Direction = {
  Up = 'UP',
  Left = 'LEFT',
  Right = 'RIGHT',
  Down = 'DOWN',
}

Direction.Up === 'UP'
Direction === 'UP' | 'LEFT' | 'RIGHT' | 'DOWN'

 

 

위와 같이 타입의 범위도 좁히고, 가독성도 높일 수 있습니다. 이때 위에서 살펴본 tree-shaking의 관점에서 enum이 단점이 있을 수 있으니 아래와 같이 const assertion을 활용해서 enum을 대체해서 사용하는 것을 살펴봤습니다.

 

 

const Direction = {
  Up: 'UP',
  Left: 'LEFT',
  Right: 'RIGHT',
  Down: 'DOWN',
} as const

type Direction = typeof Direction[keyof typeof Direction]

for (const d of Object.values(Direction)) {
  console.log(d)
}

 

 

Enum과 객체

그렇다면 일반 객체를 사용하는 것과 Enum을 사용하는 것과의 차이는 어떤 것이 있을까요? 먼저 첫 번째로 객체는 속성을 자유롭게 변경할 수 있지만, Enum은 속성을 변경할 수 없다는 특징이 있습니다. 두 번째로 객체의 속성은 넓은 타입으로 타입 추론이 이루어지지만, enum은 항상 리터럴 타입이 사용됩니다. 세 번째로 객체의 속성은 모든 값이 들어올 수 있지만 enum은 문자열 또는 숫자 형만 허용됩니다. 이러한 차이를 통해 Enum은 일반 객체보다 자유도는 떨어지지만, 대신 허용 가능한 값들을 제한하기 때문에 변경 범위가 최소화될 수 있는 장점이 있다고 생각했습니다. 그럼 Enum을 보다 잘 활용하려면 어떻게 해야 하는지 알고 싶었습니다. 지금부터 Enum을 '잘' 활용하는 방법에 대해 알아보겠습니다.

 

 

 

 


 

 

 

 

 

 

 

Enum을 '잘' 활용하는 방법

위에서 Enum은 일반 객체보다 자유도는 떨어지지만, 대신 허용 가능한 값들을 제한하기 때문에 변경 범위가 최소화될 수 있는 장점이 있다고 표현했습니다. 이 뿐 아니라 Enum을 클래스로 활용하면 더 많은 장점을 가질 수 있습니다. 하지만 타입스크립트에서는 Enum을 열거형으로만 사용할 수 있는데, ts-jenum 라이브러리를 활용하면 Java의 Enum과 같이 Static 객체로 Enum을 다룰 수 있습니다. 먼저 ts-jenum의 간단한 사용법을 알아보고, Enum을 클래스로 활용할 때의 장점에 대해 알아보겠습니다. 아래의 글은 이동욱 님의 ts-jenum으로 응집력 있는 TS 코드 작성하기 (feat. EnumClass) 글을 참고했습니다.

 

 

 

1. 설치

ts-jenum은 별도의 데코레이터를 제공합니다. 설치는 아래와 같이 할 수 있습니다.

 

npm i ts-jenum

 

 

2. 사용법

먼저 ts-jenum을 사용한 클래스는 다음과 같이 생성할 수 있습니다.

 

 

 

import { Enum, EnumType } from 'ts-jenum';

@Enum('code') // (1)
export class EJobLevel extends EnumType<EJobLevel>() { // (2)
    // (3)
    static readonly IRRELEVANT = new EJobLevel("IRRELEVANT", '경력무관', 0, 99,);
    static readonly BEGINNER = new EJobLevel("BEGINNER", '인턴/신입', 0, 0,);
    static readonly JUNIOR = new EJobLevel("JUNIOR", '주니어', 1, 3);
    static readonly MIDDLE = new EJobLevel("MIDDLE", '미들', 4, 7);
    static readonly SENIOR = new EJobLevel("SENIOR", '시니어', 8, 20);

    private constructor(readonly _code: string, readonly _name: string, readonly _startYear, readonly _endYear,) {
        super();
    }
}

 

 

 

글을 작성하신 이동욱 님의 글을 보면, ts-jenum으로 만든 클래스와 일반 Enum을 구분하기 위해 ts-jenum 클래스는 prefix로 E를 붙여서 사용하고, 지칭은 EnumClass라고 한다고 합니다. 아래 ts-jenum으로 만들어진 클래스들은 EnumClass라고 하겠습니다.

 

먼저 @Enum 데코레이터부터 살펴보겠습니다. 데코레이터는 @Enum('필드명')와 같이 작성하며, 필드명으로는 EnumClass 메인 Key가 될 필드를 지정합니다. 여기서는 _code 필드의 getter 메서드인 code를 메인 Key로 사용합니다. 해당 Key는 절대 중복이 되어선 안됩니다 EnumClass의 static class 들의 구분자 역할을 하기 때문입니다.

 

두 번째로 extends EnumType<EJobLevel>()를 살펴보겠습니다. EnumClass를 활용하려면 ts-jenum 이 제공하는 EnumType을 꼭 상속받아야 합니다. 이때 EnumType은 제네릭 타입으로 상속받은 EnumClass를 꼭 사용해야 합니다. EnumClass에서 제공하는 여러 편의 메서드들 (find, values, valueByName 등등)을 사용할 때 타입 명시가 필요하기 때문입니다.

 

마지막으로 static readonly IRRELEVANT~~ 부분을 살펴보겠습니다. 여기서는 Enum 타입을 선언하듯이 EnumClass의 타입을 선언합니다. 여기서 선언된 IRRELEVANT, BEGINNER, JUNIOR, MIDDLE, SENIOR 등이 EnumClass의 타입으로 작동합니다. 이렇게 작성된 EnumClass는 다음과 같이 활용 가능합니다.

 

 

 

2. Enum Class 예제

그럼 예를 통해 EnumClass의 활용 방법에 대해 자세히 알아보겠습니다.

 

 

 

import { Enum, EnumType } from 'ts-jenum';

@Enum('code')
export class JobLevel extends EnumType<JobLevel>() {
  static readonly IRRELEVANT = new JobLevel('IRRELEVANT', '경력무관', 0, 99);
  static readonly BEGINNER = new JobLevel('BEGINNER', '인턴/신입', 0, 0);
  static readonly JUNIOR = new JobLevel('JUNIOR', '주니어', 1, 3);
  static readonly MIDDLE = new JobLevel('MIDDLE', '미들', 4, 7);
  static readonly SENIOR = new JobLevel('SENIOR', '시니어', 8, 20);

  private constructor(
    readonly _code: string,
    readonly _name: string,
    readonly _startYear,
    readonly _endYear,
  ) {
    super();
  }

  get code(): string {
    return this._code;
  }

  get name(): string {
    return this._name;
  }

  get startYear(): number {
    return this._startYear;
  }

  get endYear(): number {
    return this._endYear;
  }

  static findName(code: string): string {
    return this.values().find((e) => e.equals(code))?.name;
  }

  static findByYear(year: number): JobLevel {
    return this.values().find(
      (e) => e.betweenYear(year) && e !== this.IRRELEVANT,
    );
  }

  betweenYear(year: number): boolean {
    return this.startYear <= year && this.endYear >= year;
  }

  getPeriod(): string {
    return `${this.startYear} ~ ${this.endYear}`;
  }

  equals(code: string): boolean {
    return this.code === code;
  }

  toCodeName() {
    return {
      code: this.code,
      name: this.name,
    };
  }
}

 

 

 

3-1. 데이터들 간의 연관관계를 표현 후, 상태와 행위 한 곳에서 관리하기

예를 들어 데이터베이스에 저장된 영문자 BEGINNER는 화면에서는 인턴/신입으로 노출되어야 한다고 가정해보겠습니다. 이뿐 아니라 IRRELEVANT(경력 무관), JUNIOR(주니어), MIDDLE(미들), SENIOR(시니어) 도 마찬가지로 한 세트의 데이터들입니다. 이럴 경우 EnumClass를 활용하면 많은 도움이 됩니다. 

 

 

 

@Enum('code')
export class JobLevel extends EnumType<JobLevel>() {
  static readonly IRRELEVANT = new JobLevel('IRRELEVANT', '경력무관', 0, 99);
  static readonly BEGINNER = new JobLevel('BEGINNER', '인턴/신입', 0, 0);
  static readonly JUNIOR = new JobLevel('JUNIOR', '주니어', 1, 3);
  static readonly MIDDLE = new JobLevel('MIDDLE', '미들', 4, 7);
  static readonly SENIOR = new JobLevel('SENIOR', '시니어', 8, 20);

  ....
}

 

 

 

만약 클래스에서 code, name의 정보만 가져오고 싶다면 아래와 같은 메서드를 활용할 수 있습니다.

 

 

@Enum('code')
export class JobLevel extends EnumType<JobLevel>() {
  ....
  toCodeName() {
    return {
      code: this.code,
      name: this.name,
    };
  }
}

 

 

 

만약 JobLevel에서 경력 기간이 아닌, code와 코드에 맞는 이름만을 출력하고 싶다면, 다음과 같이 클래스의 메서드로 쉽게 변환이 가능합니다.

 

 

예를 들어 다음과 같은 도메인 로직이 있다고 가정해봅니다.

 

  • 경력 무관: 0 ~ 99년 차
  • 신입/인턴: 0년 차
  • 주니어: 1~3년 차
  • 미들: 4 ~ 7년 차
  • 시니어: 8 ~ 20년 차

 

이때 해당 사용자의 연차를 기준으로 등급이 어떻게 되는지 확인하는 기능이 필요하다면 어떻게 해야 할까요?

 

 

 

getJobLevel(workYear) {
  if(workYear === 0) {
    return '신입/인턴';
  } else if (workYear >= 1 && workYear <= 3) {
    return '주니어';
  } else if (workYear >= 4 && workYear <= 7) {
    return '미들';
  } else if (workYear >= 8 && workYear <= 20) {
    return '시니어';
  } else {
    return '경력무관';
  }
}

 

 

만약 EnumClass를 활용하지 않는다면, 경력 무관의 경력을 살펴보고 싶다면, if문을 통해 경력에 따른 연차를 모두 비교 분석해야 합니다.  만약 이렇게 코드를 작성한다면 코드가 지저분해질 수 있습니다. 이 뿐 아니라 경력에 맞게 세밀하게 JobLevel을 분류해야 한다면 더더욱 if문의 분기 처리가 복잡해질 텐데, EnumClass를 활용해서 클래스 안에 workYear에 맞게 연차를 찾는 메서드가 있다면, 응집력 있는 코드가 될 수 있습니다. 

 

 

 

@Enum('code')
export class JobLevel extends EnumType<JobLevel>() {
  static readonly IRRELEVANT = new JobLevel('IRRELEVANT', '경력무관', 0, 99);
  static readonly BEGINNER = new JobLevel('BEGINNER', '인턴/신입', 0, 0);
  static readonly JUNIOR = new JobLevel('JUNIOR', '주니어', 1, 3);
  static readonly MIDDLE = new JobLevel('MIDDLE', '미들', 4, 7);
  static readonly SENIOR = new JobLevel('SENIOR', '시니어', 8, 20);
  ...

  // 전체 JobLevel 중 해당 연차에 포함되는 JobLevel 탐색
  static findByYear(year: number): JobLevel {
    return this.values().find(
      (e) => e.betweenYear(year) && e !== this.IRRELEVANT,
    );
  }

  // 해당 JobLevel의 연차 범위내에 포함되는지 확인
  betweenYear(year: number): boolean {
    return this.startYear <= year && this.endYear >= year;
  }
  ...
}

 

 

이렇게 되면 다음과 같이 JobLevel.findByYear(workYear) 한 줄로 연차로 역량 레벨 조회를 할 수 있습니다.

 

 

 

it.each([
  [0, '인턴/신입'],
  [1, '주니어'],
  [5, '미들'],
  [11, '시니어'],
])('연차 %s인 경우 역량 등급은 %s이다', (year, grade) => {
  const result = JobLevel.findByYear(Number(year));

  expect(result.name).toBe(grade);
});

 

 

테스트 코드를 작성하면 다음과 같이 작성할 수 있습니다.

 

 

 

 

3-2. 데이터 그룹 관리

만약  채용 공고 작성을 위해 인턴부터 시니어까지의 레벨을 묶어서 크게 신입/경력직 구분이 필요하다고 가정해보겠습니다.

 

 

출처 :&nbsp;https://jojoldu.tistory.com/621

 

이를 쉽게 할 수 있으려면 if-else문을 활용하면 됩니다.

 

 

getJobLevelGroup(jobLevel: string) {

  if(jobLevel === 'BEGINNER' || jobLevel === 'JUNIOR' || jobLevel === 'IRRELEVANT') {
    return 'NEWCOMER'; // 신입
  } else if (jobLevel === 'MIDDLE' || jobLevel === 'SENIOR') {
    return 'EXPERIENCED'; // 경력
  } else {
    throw new Error ('해당하는 그룹이 없습니다.');
  }
}

 

 

하지만 위와 같이 작성하면 동일하게 코드의 응집력이 떨어지게 됩니다. 응집력이 높은 코드를 작성하기 위해서는 역량 레벨 그룹으로 EnumClass를 만들어서 역량 레벨 EnumClass를 포함시키면 됩니다. 

 

 

@Enum('code')
export class JobLevelGroup extends EnumType<JobLevelGroup>() {
  static readonly NEWCOMER = new JobLevelGroup('NEWCOMER', '신입', [
    JobLevel.BEGINNER,
    JobLevel.JUNIOR,
    JobLevel.IRRELEVANT,
  ]);

  static readonly EXPERIENCED = new JobLevelGroup('EXPERIENCED', '경력직', [
    JobLevel.MIDDLE,
    JobLevel.SENIOR,
    JobLevel.IRRELEVANT,
  ]);

  private constructor(
    readonly _code: string,
    readonly _name: string,
    readonly _jobLevels: JobLevel[],
  ) {
    super();
  }

  // 미리 static Map이나 리터럴 객체를 만들어 두면 매번 values 순회하는 것을 방지할 수도 있습니다.
  static findByJobLevel(jobLevel: JobLevel): JobLevelGroup {
    return this.values().find((group) =>
      group._jobLevels.some((level) => level.equals(jobLevel)),
    );
  }

  get code(): string {
    return this._code;
  }
}

 

 

위와 같이 작성한다면, 아래와 같이 역량 레벨에 맞춰 그룹을 찾을 수 있습니다.

 

 

it('JobLevel로 JobLevelGroup를 찾는다', () => {
  const jobLevel = JobLevel.MIDDLE;
  const result = JobLevelGroup.findByJobLevel(jobLevel);

  expect(result).toBe(JobLevelGroup.EXPERIENCED);
});

 

 

 

위와 같이 설정한다면, 응집력이 높은 코드를 작성할 수 있습니다. 즉 정리해보면, EnumClass를 활용하면 클래스의 역할과 책임을 부여해서 상태와 행위를 EnumClass 한 곳에서 관리할 수 있습니다. 이를 통해 응집력 있는 코드를 작성할 수 있습니다. 또한 데이터들의 연관관계에 대해 명확하게 살펴볼 수 있습니다.

 

 

이 글을 정리하면서, 다음과 같이 생각할 수 있었습니다.

 

단순하게 Enum을 활용하게 되면 tree-shaking 문제가 있기에 문제가 생길 수 있습니다. 하지만 단순 객체를 활용하는 것보다, 타입의 범위를 줄일 수 있다는 장점이 있다고 생각했습니다. 또한 Enum은 일반 객체보다 활용에 대한 자유도가 떨어지다 보니, 활용 방안이 많지는 않지만 자유도가 적다 보니, 변경 범위가 최소화될 수 있어서 수정 범위 또한 적다는 장점이 있다고 생각했습니다. 이러한 Enum을 tree-shaking의 문제를 해결하면서 사용하려면 const assertion과 Union Type을 활용하고, 만약 응집력 있는 코드를 작성하고 싶다면 EnumClass를 활용해야겠다고 생각했습니다. 

 

 

 


 

 

 

 

마치며

앞으로도 팀의 발전을 돕는 개발자가 되기 위해 노력하려 합니다. 팀에 필요한 부분이 무엇일지 고민하면서, 팀에 도움이 된다면, 열심히 공부해서 실무에 적용할 수 있는 개발자가 되기 위해 노력하고 싶습니다. 팀의 성장에 기여할 수 있는 개발자가 되겠습니다. 

 

 

 

 

 


 

 

 

 

 

참고 및 출처

 

ts-jenum 으로 응집력 있는 TS 코드 작성하기 (feat. EnumClass)

TypeScript의 Enum은 딱 열거형으로서만 사용할 수 있습니다. 다른 언어에서 Enum을 Static 객체로 사용해본 경험이 있는 분들이라면 이 지점이 굉장히 답답하다는 것을 느낄 수 있는데요. Java에서 Enum

jojoldu.tistory.com

 

Java Enum 활용기 | 우아한형제들 기술블로그

{{item.name}} 안녕하세요? 우아한 형제들에서 결제/정산 시스템을 개발하고 있는 이동욱입니다. 이번 사내 블로그 포스팅 주제로 저는 Java Enum 활용 경험을 선택하였습니다. 이전에 개인 블로그에 E

techblog.woowahan.com

 

Home

yceffort

yceffort.kr

 

TypeScript enum을 사용하지 않는 게 좋은 이유를 Tree-shaking 관점에서 소개합니다. - LINE ENGINEERING

안녕하세요. LINE Growth Technology UIT 팀의 Keishima(@pittanko_pta)입니다. 이번 글에서는 TypeScript의 enum을 사용하지 않는 편이 좋은 이유를 Tree-shaking 관점에서 소개하겠습니다.

engineering.linecorp.com

 

TypeScript enum을 사용하는 이유

(본 글은 TypeScript 입문자 중 enum 기능이 있는 다른 언어를 사용해 본 경험이 없는 분들을 위해 쓰여졌습니다. 예제 코드를 TypeScript playground에 붙여 넣고, 마우스 포인터를 변수 위에 둬서 변수의

medium.com

 

[타입스크립트] 타입스크립트 이넘 타입

들어가며 개발을 하면서, 자바스크립트의 언어적 특성 때문에 많은 불편함을 느꼈습니다.  타입 에러를 제대로 확인 못하고 코드를 배포했다가 서버에 문제가 생긴 적이 있었습니다. 런타임 시

overcome-the-limits.tistory.com

 

GitHub - reforms/ts-jenum: TypeScript Enum like java.lang.Enum

TypeScript Enum like java.lang.Enum. Contribute to reforms/ts-jenum development by creating an account on GitHub.

github.com