개인 프로젝트를 진행 중에 유저를 다양한 역할로 나눠서 관리합니다. 성인 미만 스포츠 팀과 관련된 프로젝트이기 때문에 코칭스태프, 선수, 학부모, 기타로 역할을 나누었습니다.
회원가입 시, 클라이언트에게 role(역할)에 대한 데이터를 요구하고, 요구한 role 데이터에 대한 유효성 검사를 할 필요가 있습니다.
Controller Layer에서 유효성 검사를 진행하는 방법도 있지만, 코드가 복잡해지고 role에 대한 내용이 변동이 있을 때 마다 role 데이터에 대한 유효성 검사 코드가 작성된 모든 Controller 파일에서 수정이 이뤄져야 하기 때문에 좋은 방식은 아니라는 판단을 했습니다.
이런 상황에 사용할 수 있는 방법 중, DTO를 작성하고 DTO 파일 내부에서 클라이언트에서 보내오는 데이터에 대한 유효성 검사를 진행할 수 있습니다.
Nestjs에서는 class-validator 라이브러리를 사용해 타입 및 형식 별로 데코레이터를 각 프로퍼티에 붙혀서 프로퍼티의 값의 유효성을 검사할 수 있습니다.

기본적인 형태는 enum이라는 열거형 자료형을 사용해서 특정 데이터에 대해 타입이 아닌 값을 정의할 수 있고, IsEnum 데코레이터에 enum 자료형을 넘겨, 열거 된 값들로 데이터를 한정할 수 있고, 유효성 검사를 진행할 수 있습니다.
여기서 유의할 점(이라고 적고 개인적으로 헷갈린 점)은 데코레이터에 정의한 값이 코드의 컴파일 단계에서 사용되는 것이 아니라 단지 데이터 교환 간 유효성 검사에만 사용된다는 점입니다. 컴파일 단계에서 role의 데이터를 타입이 아닌 값으로 정의하기 위해서는 유니온 타입으로 값을 구분해서 role이라는 프로퍼티에 작성해줘야 추후 다른 위치에서 role 프로퍼티를 사용할 때 타입으로써 정의될 수 있습니다.
위의 코드는 동작함에는 문제가 없으나 재사용성과 유지보수 측면에서는 좋지 못한 코드라고 할 수 있습니다.
직접 타이핑 했기 때문에 오타의 우려가 있으며, 다른 DTO에서 role에 대한 유효성 검사 및 타입 지정을 할 때, 같은 코드를 또 다시 작성해야하는 번거로움이 있고, Controller에서 작성했을 때와 마찬가지로 role의 종류가 많아질 때마다 모든 코드를 다시 직접 찾아 수정해야하기 때문입니다.
그래서 하나씩 해결해보고자 합니다. 먼저 IsEnum 데코레이터에 들어가는 enum 데이터를 상수로 관리할 수 있도록 분리하는 과정을 거쳐보겠습니다.

기존 IsEnum 데코레이터에 들어가는 객체를 enum 자료형으로 분리하여 데코레이터에 넣어줬습니다. 이 코드도 문제는 없습니다만 조금더 개선할만한 부분이 있어 보입니다. 일단 role 프로퍼티에 들어가는 값들도 type으로 분리해보겠습니다.

이렇게 해서 추후에 늘어날 DTO에서 role에 대한 유효성 검사 및 타입지정을 할 때, 조금 더 나은 코드가 되었지만 여기에서도 조금 더 개선할 여지는 보입니다. 일단 role의 종류가 많아지게 되면 enum과 type에서 모두 수정을 해줘야하는 번거로움도 존재해보입니다. 또한, enum 자료형에 대해 조금 더 검색해보면 enum 자료형을 사용하는 것에 대해 꽤 많은 부정적 의견들이 존재하는데 그 이유는 보통 하단과 같습니다.
1. enum 자료형은 Typescript에서만 지원하는 자료형이며 Javascript에서는 지원하지 않습니다.
- 이러한 이유로, enum 자료형은 javascript로 컴파일 시 IIFE 즉, 즉시실행함수로 구현되어 트리 쉐이킹의 이점을 받을 수 없습니다.
- 트리 쉐이킹이란, 사용하지 않는 데이터나 모듈을 번들링 시 제외하여 용량을 줄이는 개념을 말합니다. (나무를 흔들어 쓸모없는 것을 털어낸다는 의미)
2. enum 자료형은 정의되어있지 않은 프로퍼티에도 접근이 가능하며, 에러를 반환하지 않습니다.
이 두 가지의 이유로 프로젝트에서는 enum 사용을 최소화하기로 결정하고, enum을 대체할 수 있는 방법을 찾았습니다.
Javascript의 객체를 사용하면서, Typescript의 const assertion 즉, 타입 단언을 통해 객체의 프로퍼티를 readonly로 만들어 리터럴 타입으로 객체 내부의 값을 추론하게 만들어 enum과 비슷한 방법으로 상수를 관리할 수 있습니다.
여기에서 리터럴 타입이란 기본적인 언어의 primitive type이 아닌 다른 값 자체로 타입을 지정한 타입을 말하며, admin: "admin" 이라면 string 리터럴 타입이라고 볼 수 있는 개념입니다.
조금 더 설명하면, let 변수와 const 변수에서 차이를 보이는데 let 변수와 const 변수에 같은 값의 문자열을 할당해도, let 변수는 string을 타입으로 가지고, const 변수는 문자열 자체를 타입으로 가집니다. 그 이유는, let 변수는 값이 변할 수 있기 때문에 조금 더 넓은 의미의 타입인 string으로 타입을 추론하고, const 변수는 값이 변할 수 없기 때문에 리터럴 타입으로 타입을 추론하게 됩니다.
개념은 어려울 수 있으나, 코드로 보면 조금 더 간단합니다.


위의 두 장의 사진처럼 일반 객체를 사용했을 때는 각 프로퍼티에 담긴 값의 타입을 추론하지만 (let 변수처럼 접근하여 재할당이 가능하기 때문에), 타입 단언을 사용한 객체는 각 프로퍼티에 담긴 값 자체를 타입으로 추론합니다 (const로 단언했기 때문에 객체 내부의 프로퍼티를 const 변수처럼 readonly(읽기 전용)으로 변함). 우리가 enum을 사용하는 이유는 값의 타입이 필요한 것이 아니라 값 자체가 필요한 것이기 때문에 타입 단언을 통해 값을 가지고 있을 수 있게 됐습니다.
추가적으로, DTO의 role 프로퍼티에 작성되어야하는 유니온 타입도 위에서 사용한 타입 단언을 사용한 객체를 통해 확장하여 작성이 가능합니다.

처음에는 이 코드의 진행과정이 이해가 가지 않았으나 순서대로 보면서 이해를 하게 되었습니다.
typeof USER_ROLE은 { ADMIN: 'admin'; USER: 'user'; ETC: 'etc' } 가 됩니다. 이유는 타입 단언을 통해 각 프로퍼티의 타입을 값 자체로 가질 수 있도록 했기 때문입니다.
또한, typeof USER_ROLE의 keyof기 때문에 ADMIN | USER | ETC가 되고, 이것들이 키 맵핑이 되어 "admin" | "user" | "etc" 가 되는 것 입니다.
그렇게 되면 role 프로퍼티에 할당했던 유니온 타입과 같은 형태가 되고, 이제는 타입 단언을 적용한 객체의 내용만 변경해주면 type도 같이 변경될 수 있도록 구현이 되었습니다.
최종 코드의 형태는 하단과 같습니다.

'Javascript&Typescript' 카테고리의 다른 글
| 이터러블(iterable)과 이터레이터(iterator) (0) | 2023.02.07 |
|---|---|
| Call Stack과 Memory Heap으로 알아보는 불변성을 지켜야 하는 이유 (0) | 2023.02.02 |