타입으로 의도를 표현하기
2025년 6월 15일
TypeScript를 사용하는 가장 큰 이유는 컴파일 단계에서 타입 오류를 발견하기 위해서입니다. 하지만 타입의 역할은 단순히 오류를 방지하는 것에서 끝나지 않습니다. 좋은 타입은 코드를 읽는 사람에게도 많은 정보를 전달합니다.
예를 들어 함수의 매개변수가 number인지 UserId인지에 따라 함수가 어떤 값을 기대하는지 바로 이해할 수 있고, 타입만 보더라도 데이터의 형식이나 비즈니스 규칙을 파악할 수 있습니다. 즉, 타입은 자료형을 표현하는 도구인 동시에 도메인의 의도를 표현하는 도구이기도 합니다.
이번 글에서는 타입을 통해 의도를 표현하여 가독성과 안정성을 높일 수 있었던 몇 가지 경험을 소개하려고 합니다
1. 도메인의 의미를 타입으로 표현하기
프론트엔드에서 개발하다 보면 API 응답에서 id라는 필드를 자주 접하게 됩니다. 이 값은 데이터를 식별하기 위한 값으로 대부분 number 또는 string 타입으로 정의합니다.
interface UserResponse { id: number;}
하지만 애플리케이션 내부에서 number라는 타입만으로는 이 값이 무엇을 의미하는지 알기 어렵습니다. 예를 들어 아래 함수를 보겠습니다.
function getUser(id: number) {}
이 함수는 단순히 숫자를 전달받는 것처럼 보입니다. 하지만 실제로는 사용자의 ID를 전달받아야 하는 함수입니다. (물론 파라미터 명을 userId로 하면되지만 타입으로 표현한다면 id라고 하더라도 타입을 통해 의미를 알 수 있습니다.)
프로젝트가 커질수록 UserId, OrderId, ProductId처럼 다양한 ID가 모두 number로 표현되는 경우가 많습니다.
type UserId = number;
type OrderId = number;
function getUser(userId: UserId) {}
const orderId: OrderId = 1;
getUser(orderId); // 정상
TypeScript는 두 타입을 모두 number로 취급하기 때문에 아무런 오류를 발생시키지 않습니다.
이럴 때는 Brand Type을 사용하여 타입에 의미를 부여할 수 있습니다.
declare const UserIdBrand: unique symbol;
declare const OrderIdBrand: unique symbol;
type UserId = number & {
readonly [UserIdBrand]: never;
};
type OrderId = number & {
readonly [OrderIdBrand]: never;
};
이제 두 타입은 모두 실제 값은 number이지만 서로 다른 타입으로 인식됩니다.
function getUser(userId: UserId) {}
const orderId = 1 as OrderId;
getUser(orderId); // ❌ 컴파일 오류
또한 함수의 의도도 더욱 명확해집니다.
function getUser(userId: UserId)
이 함수는 단순히 숫자를 받는 것이 아니라 사용자의 식별자를 전달받는다는 사실을 타입만으로도 이해할 수 있습니다.
타입은 단순히 값의 자료형을 표현하는 것이 아니라 값이 무엇을 의미하는지도 표현할 수 있습니다.
2. 데이터의 형식을 타입으로 표현하기
의도를 표현하는 것은 값의 의미뿐만이 아닙니다. 데이터가 일정한 형식을 가진다면 그 형식 역시 타입으로 표현할 수 있습니다. 예를 들어 이전 프로젝트에서 과일을 식별하기 위한 Key를 생성해야 하는 요구사항이 있었습니다. Key의 형식은 아래와 같았습니다.
{과일}:{Timestamp}
처음에는 단순히 string으로 정의할 수도 있습니다.
type FruitKey = string;
하지만 이 타입만으로는 어떤 문자열이 올바른 값인지 알 수 없습니다. TypeScript의 Template Literal Type을 사용하면 데이터의 형식을 그대로 표현할 수 있습니다.
type Fruit = "apple" | "banana" | "peach";
type FruitKey = `${Fruit}:${number}`;
이제 타입만 보더라도
apple:1751950200
banana:1751950200
처럼 어떤 형식의 문자열인지 바로 이해할 수 있습니다. 또한 잘못된 형식의 문자열을 사용하는 것도 방지할 수 있습니다.
const key: FruitKey = "orange:123";// ❌ 컴파일 오류
데이터 형식이 변경되더라도 타입 정의만 수정하면 영향을 받는 모든 코드를 컴파일 오류를 통해 확인할 수 있습니다. 타입은 단순히 문자열이라는 사실을 표현하는 것이 아니라 어떤 형식의 문자열인지까지 표현할 수 있습니다.
3. 유효한 상태만 타입으로 표현하기
지금까지는 값의 의미와 데이터의 형식을 표현했습니다. 하지만 타입은 도메인의 규칙도 표현할 수 있습니다. 최근 Form 컴포넌트를 개발하면서 아래와 같은 요구사항이 있었습니다.
- 데이터 유형은
A,B,C가 있습니다. - 모든 유형은
name,description,value를 입력합니다. C유형만expirationDate를 입력합니다.
처음에는 하나의 타입으로 정의했습니다.
interface FormField {
type: "A" | "B" | "C";
name: string;
description: string;
value: number;
expirationDate?: string;
}
하지만 이 타입은 실제 비즈니스 규칙을 제대로 표현하지 못합니다.
타입만 보면
- A도
expirationDate를 가질 수 있고 - B도
expirationDate를 가질 수 있으며 - C도
expirationDate가 없을 수 있습니다.
즉, 실제로는 존재할 수 없는 데이터까지 허용하고 있습니다.
실제 규칙은 다음과 같습니다.
- A는
expirationDate를 가질 수 없습니다. - B는
expirationDate를 가질 수 없습니다. - C는 반드시
expirationDate를 가져야 합니다.
이럴 때는 각 유형을 독립적인 타입으로 표현하는 것이 더 적합합니다.
interface BaseFormField {
name: string;
description: string;
value: number;
}
interface AFormField extends BaseFormField {
type: "A";
}
interface BFormField extends BaseFormField {
type: "B";
}
interface CFormField extends BaseFormField {
type: "C";
expirationDate: string;
}
type FormField =
| AFormField
| BFormField
| CFormField;
이제 타입은 실제 도메인의 규칙을 그대로 표현합니다.
const data: CFormField = {
type: "C",
name: "상품",
description: "...",
value: 100,
};
// ❌ expirationDate가 필요합니다.
반대로 A나 B에서 expirationDate를 추가해도 타입 오류가 발생합니다. 타입이 허용하는 데이터와 실제 비즈니스에서 허용하는 데이터가 일치하게 된 것입니다.
좋은 타입은 가능한 모든 데이터를 표현하는 것이 아니라, 유효한 데이터만 표현합니다. 이렇게 하면 타입 자체가 비즈니스 규칙을 설명하는 문서가 되고, 코드를 읽는 사람도 타입만으로 각 유형의 차이를 이해할 수 있습니다.
마치며
처음 TypeScript를 사용할 때는 변수와 함수의 타입을 정의하는 것만으로도 충분하다고 생각했습니다. 하지만 프로젝트가 커질수록 타입은 단순히 자료형을 나타내는 것이 아니라 도메인의 의도와 규칙을 표현하는 도구라는 것을 느끼게 되었습니다.
이번 글에서 소개한 방법들은 모두 같은 목적을 가지고 있습니다.
- 도메인의 의미를 표현한다.
- 데이터의 형식을 표현한다.
- 유효한 상태만 표현한다.
이처럼 타입이 더 많은 정보를 담을수록 코드를 읽는 사람은 타입만으로도 의도를 이해할 수 있고, 컴파일 단계에서 더 많은 실수를 예방할 수 있습니다.
최근에는 AI를 활용하여 코드를 작성하는 경우도 많아졌습니다. 의도가 잘 표현된 타입은 개발자뿐 아니라 AI에게도 좋은 문서가 됩니다. 타입만으로도 도메인의 규칙과 데이터의 형태를 이해할 수 있기 때문에, 더 정확한 코드 생성과 리팩터링에도 도움이 됩니다.
결국 좋은 타입은 단순히 타입 오류를 막기 위한 것이 아니라 사람과 컴파일러, 그리고 AI 모두에게 의도를 전달하는 가장 강력한 문서라고 생각합니다.