TypeScript의 Branded Types으로 런타임 유형 안전성 개선하기

park.js·2024년 9월 17일
2

FrontEnd Develop log

목록 보기
30/37

원문 작성자: Matías Hernández, 참고 링크: Improve Runtime Type Safety with Branded Types in TypeScript

코드의 안전성과 신뢰성을 높이기 위해 타입스크립트를 사용한다는 것은 이미 잘 알려진 사실이다. 하지만 인생이 다 그렇듯 만능과 완벽은 존재하지 않는다.
특히 런타임 에러를 막기 위해서는 더 정교한 타입 시스템이 필요할 수 있다. 이때 브랜디드 타입(Branded Types)이 도움을 줄 수 있다.

이번 글에서는 브랜디드 타입(Branded Types)이 무엇인지, 어떻게 사용하는지, 그리고 어떤 장점이 있는지 살펴보겠다.


런타임과 빌드타임: 타입스크립트의 한계 이해하기

이 부분 알면 넘어가시고 모르시면 꼭 보시고 기본으로 깔고 가셔야합니다.

  • 런타임(Runtime): 코드가 실제로 실행되는 시점. 런타임에 발생하는 에러는 코드가 실행되는 도중에 감지되므로, 문제 해결이 까다롭고 실행 중에 예기치 않은 동작을 유발할 수 있다.
  • 빌드타임(Build Time): 코드를 작성하고 컴파일하는 시점으로, 타입스크립트는 이 단계에서 타입 검사를 수행한다.
    잘못된 타입 사용이나 타입 불일치를 컴파일 타임에 미리 감지해줌으로써 런타임 에러를 줄일 수 있다.

타입스크립트는 주로 빌드타임에 타입 검사를 통해 코드를 검증하고, 안전성을 높인다.

그러나!!!!!! 타입스크립트의 기본 타입만으로는 런타임에 발생할 수 있는 모든 오류를 방지하기에는 부족할 때가 있다.
여기서 우리는 브랜디드 타입이 등장하게 된 배경을 알 수 있다.


타입스크립트에서 브랜디드 타입이 필요한 이유

타입스크립트는 빌드타임에 타입 검사를 통해 데이터가 올바르게 전달되는지 검증한다.
그러나 데이터의 구체성이나 특수성이 충분히 표현되지 않는 경우가 종종 발생한다.
예를 들어, 사용자, 게시물, 댓글을 다루는 애플리케이션을 만든다고 가정해보자.

type User = {
  id: string;
  name: string;
}

type Post = {
  id: string;
  ownerId: string;
  comments: Comment[];
}

type Comment = {
  id: string;
  timestamp: string;
  body: string;
  authorId: string;
}

위 코드를 보면 User, Post, Comment는 모두 id 속성을 가지고 있다.
그러나 이 속성들은 모두 단순히 string 타입이기 때문에, 타입스크립트 입장에서는 이 id들이 서로 구분되지 않는다.

즉, 아래와 같은 코드에서도 에러를 감지하지 못한다.

async function getCommentsForPost(postId: string, authorId: string) {
  const response = await api.get(`/author/${authorId}/posts/${postId}/comments`);
  return response.data;
}

const comments = await getCommentsForPost(user.id, post.id);  // 타입스크립트는 에러를 인식하지 못함

위 코드에서는 getCommentsForPost 함수를 호출할 때 매개변수의 순서가 잘못되었다.
그러나 타입스크립트는 user.idpost.id가 모두 string 타입이므로 문제를 인식하지 못한다.
이 때문에 런타임에 잘못된 응답을 받게 되고, 디버깅이 어려워진다.

타입스크립트가 더욱 정교한 타입 안전성을 제공하기 위해 탄생한 것이 바로 "브랜디드 타입"이다.


브랜디드 타입(Branded Types)이란?

브랜디드 타입은 기존 타입에 특정한 태그(brand)를 부여하여 더 구체적이고 유일한 타입을 만드는 방법이다.
이렇게 하면 단순한 기본 타입보다 더 명확하고 안전하게 데이터를 모델링할 수 있다.

간단한 브랜디드 타입 구현 예시

type Brand<K, T> = K & { __brand: T };
type UserID = Brand<string, "UserId">;
type PostID = Brand<string, "PostId">;

위 코드는 UserIDPostID라는 두 개의 새로운 타입을 만들어낸다. __brand 속성으로 고유성을 부여하여 타입을 더욱 세밀하게 구분할 수 있다.

이제 UserIDPostID는 단순한 string이 아닌 고유한 타입이 된다. 이를 활용하면 잘못된 데이터가 전달될 가능성을 줄일 수 있다.


개선된 예시

type UserID = Brand<string, "UserId">;
type PostID = Brand<string, "PostId">;

type User = {
  id: UserID;
  name: string;
}

type Post = {
  id: PostID;
  ownerId: UserID;
  comments: Comment[];
}

async function getCommentsForPost(postId: PostID, authorId: UserID) {
  const response = await api.get(`/author/${authorId}/posts/${postId}/comments`);
  return response.data;
}

const comments = await getCommentsForPost(user.id, post.id);  // ❌ 타입 오류 발생

이제 user.idpost.id가 각각 UserIDPostID 타입을 가지므로, 잘못된 인자를 전달할 경우 타입스크립트가 이를 감지할 수 있다.
이로써 컴파일 타임에 잘못된 타입 사용을 미리 방지할 수 있다.


브랜디드 타입의 한계와 개선된 구현(중요)

위 방법은 간단하고 유용하지만 몇 가지 문제점이 있다:
1. __brand 속성은 빌드타임에만 존재하며, 런타임에는 사라진다.
2. __brand 속성이 코드 자동 완성(IntelliSense)에 노출되어, 개발자가 오용할 수 있다.
3. 단순한 문자열 기반 태깅은 안전하지 않으며, 중복된 타입을 만들 수 있다.

뭐라는거지? 이게 왜 문제라는거지? 싶으신 저같은 분들을 위한 쉬운 설명

1. __brand 속성은 빌드타임에만 존재하며, 런타임에는 사라진다.

  • 타입스크립트는 컴파일 타임에만 타입을 체크하고, 그 결과물을 런타임에 사용할 자바스크립트 코드로 변환한다.
  • type Brand<K, T> = K & { __brand: T } 같은 브랜디드 타입은 타입스크립트가 컴파일 중에만 확인하고, 실제로 자바스크립트 코드로 변환된 후에는 사라진다. 즉, 런타임에는 __brand 속성이 존재하지 않게 된다.

왜 문제가 될까?

  • 브랜디드 타입은 런타임에 타입의 고유성을 보장하는 것이 아니라, 컴파일 시점에만 타입 검사를 위해 존재한다. 따라서 컴파일을 거친 이후 실제 실행 중에는 __brand가 사라지기 때문에, 런타임에 이 타입을 검사할 수 없다.
  • 즉, 타입 안정성을 보장하기 위해 브랜디드 타입을 사용하지만, 런타임에는 사라져버리기 때문에 컴파일 타임에서만 안전성을 확보할 수 있다는 한계가 있다.

2. __brand 속성이 코드 자동 완성(IntelliSense)에 노출되어, 개발자가 오용할 수 있다.

  • IntelliSense는 개발자가 코드 작성 시 자동 완성 기능을 제공하는 툴.
  • __brand라는 속성은 브랜디드 타입을 만들기 위해 우리가 태그로 사용한 속성이다.
    하지만 이 속성은 개발자가 직접 조작할 의도로 만든 것이 아니다.
  • 문제는 __brand가 자동 완성 기능(IntelliSense)에 노출된다는 것이다.
    개발자가 코드 작성 중에 이 속성을 볼 수 있고, 오용할 가능성이 생긴다.

예를 들어:

type UserId = Brand<string, "UserId">;

const userId: UserId = "12345" as UserId;

// IntelliSense 자동 완성 기능에서 `userId.__brand`를 볼 수 있게 된다.
  • 자동 완성 기능에서 __brand 속성이 노출되면, 개발자가 의도치 않게 __brand 속성을 참조하거나 조작할 수 있다. 이는 타입 안전성을 깨뜨릴 수 있는 잠재적인 문제이다.

3. 단순한 문자열 기반 태깅은 안전하지 않으며, 중복된 타입을 만들 수 있다.

  • type Brand<K, T> = K & { __brand: T }에서 T는 보통 "UserId""Email" 같은 문자열로 사용된다.
    그러나 문자열 기반으로 태그를 붙이는 것은 안전하지 않을 수 있다.

왜 문제가 될까?

  • 동일한 문자열 값을 사용하여 다른 브랜디드 타입을 만들게 되면 타입의 고유성이 사라지게 된다. 즉, 중복된 브랜디드 타입을 생성할 위험이 있다.

예시:

type UserId = Brand<string, "Id">;
type ProductId = Brand<number, "Id">;

const userId: UserId = "12345" as UserId;
const productId: ProductId = 67890 as ProductId;
  • 위 예시에서 "Id"라는 동일한 문자열을 다른 두 타입 (UserId, ProductId)에 태그로 사용했다. 이 경우, 타입스크립트는 UserIdProductId를 서로 구분하지 못할 수 있고, 의도치 않게 두 타입이 혼동될 수 있다.
  • 따라서 단순히 문자열로 태깅하는 것만으로는 완벽한 안전성을 보장할 수 없다는 한계가 있다.

이 문제를 해결하기 위해 unique symbol을 사용한 개선된 브랜디드 타입 구현이 필요하다.


개선된 브랜디드 타입 구현

declare const __brand: unique symbol;

type Brand<B> = { [__brand]: B };
export type Branded<T, B> = T & Brand<B>;

이제 __brandunique symbol로 선언함으로써 고유성을 보장한다. 이를 통해 더 이상 __brand 속성이 외부에 노출되지 않고, 같은 이름의 브랜드라도 고유한 심볼을 가지기 때문에 중복될 가능성이 없다.


브랜디드 타입이 가져다주는 이점

1. 명확성

브랜디드 타입을 사용하면 변수의 의도를 더 명확하게 표현할 수 있다. 예를 들어, UserIDPostID가 단순한 문자열이 아닌 고유한 타입으로 정의되어 각 용도의 혼동을 방지할 수 있다.

2. 안전성과 정확성

타입 간의 불일치를 더 쉽게 감지하고 런타임 오류를 줄일 수 있다. 잘못된 데이터가 전달될 경우 컴파일 타임에 에러를 발생시킨다.

3. 유지보수성

팀원들이 코드의 의도를 쉽게 이해할 수 있다. => 이게 정말 중요한 것 같다.


브랜디드 타입의 활용 예시

1. 커스텀 유효성 검사

브랜디드 타입을 사용해 이메일 주소와 같은 사용자 입력을 검증할 수 있다.

type EmailAddress = Brand<string, "EmailAddress">;

function validEmail(email: string): EmailAddress {
  // 이메일 유효성 검사 로직
  return email as EmailAddress;
}

2. 도메인 모델링

특정 도메인을 구체적으로 표현할 때 유용하다.
예를 들어, 자동차 제조 라인에서 다양한 속성을 안전하게 관리할 수 있다.

type CarBrand = Brand<string, "CarBrand">;
type EngineType = Brand<string, "EngineType">;

3. API 응답 및 요청

API 호출에서 성공 및 실패 응답을 구분하여 타입 안정성을 높일 수 있다.

type ApiSuccess<T> = T & { __apiSuccessBrand: true };
type ApiFailure = {
  code: number;
  message: string;
  error: Error;
} & { __apiFailureBrand: true };

결론

브랜디드 타입은 타입스크립트의 타입 시스템을 한층 강화하여 코드의 안전성과 명확성을 높여주는 강력한 도구.

  • 데이터의 형태와 의도를 더욱 구체적으로 표현
  • 컴파일 타임에 잘못된 사용을 감지

실습해보기

실습링크
솔루션링크

profile
참 되게 살자

0개의 댓글