유니온(Union)과 인터섹션(Intersection): 타입을 유연하게 조합하는 아키텍처

타입스크립트(TypeScript)를 단순한 '타입 검사기'를 넘어 '설계 도구'로 활용하게 해주는 핵심 기능이 바로 유니온(Union)과 인터섹션(Intersection)입니다. 이 두 개념을 이해하면 정적인 타입 시스템 안에서도 놀라울 만큼 유연하고 확장성 있는 아키텍처를 구축할 수 있습니다.
본 포스팅에서는 실무에서 바로 활용할 수 있는 풍부한 예제와 함께 유니온과 인터섹션의 개념을 깊이 있게 살펴보겠습니다.
1. 유니온 타입 (Union Types): "또는 (OR)"
유니온 타입은 값의 타입을 여러 개 중 하나로 허용하고 싶을 때 사용합니다. 세로 바($|$) 기호를 사용하여 정의하며, 집합론적으로는 합집합의 개념입니다.
실무 예제: 다중 상태 관리
API 요청의 상태를 정의할 때 유니온 타입은 빛을 발합니다.
type NetworkStatus = 'loading' | 'success' | 'error';
function handleResponse(status: NetworkStatus) {
switch (status) {
case 'loading':
console.log("데이터를 불러오는 중입니다...");
break;
case 'success':
console.log("성공적으로 완료되었습니다.");
break;
case 'error':
console.log("에러가 발생했습니다.");
break;
}
}
함수 매개변수의 유연성
하나의 함수가 다양한 형태의 입력을 처리해야 할 때 유용합니다.
function formatId(id: string | number) {
if (typeof id === 'string') {
return `ID_${id.toUpperCase()}`;
}
return `ID_${id.toFixed(0)}`;
}
console.log(formatId("user123")); // ID_USER123
console.log(formatId(456.7)); // ID_457
2. 인터섹션 타입 (Intersection Types): "그리고 (AND)"
인터섹션 타입은 여러 타입을 하나로 결합하여 모든 타입의 기능을 갖춘 새로운 타입을 만듭니다. 앰퍼샌드($\&$) 기호를 사용하며, 집합론적으로는 교집합의 개념이지만, 객체 관점에서는 속성의 결합으로 이해하는 것이 쉽습니다.
실무 예제: 엔티티 확장
기본 사용자 정보에 권한 정보를 합쳐서 새로운 관리자 타입을 정의할 수 있습니다.
interface User {
id: string;
name: string;
}
interface Permissions {
canEdit: boolean;
canDelete: boolean;
}
// User와 Permissions의 모든 속성을 가져야 함
type AdminUser = User & Permissions;
const admin: AdminUser = {
id: "admin-01",
name: "관리자",
canEdit: true,
canDelete: true
};
3. 아키텍처적 진화: 식별 가능한 유니온 (Discriminated Unions)
단순한 유니온을 넘어, 객체 내부에 공통된 '태그'를 두어 타입을 안전하게 좁히는(Narrowing) 기법입니다. 이는 현대적인 상태 관리와 리듀서(Reducer) 패턴의 근간이 됩니다.
예제: 메시지 시스템 설계
interface TextMessage {
type: 'text'; // 식별자(Discriminant)
content: string;
}
interface ImageMessage {
type: 'image'; // 식별자(Discriminant)
url: string;
size: number;
}
interface VideoMessage {
type: 'video'; // 식별자(Discriminant)
url: string;
duration: number;
}
type Message = TextMessage | ImageMessage | VideoMessage;
function renderMessage(msg: Message) {
// 공통 속성인 'type'을 통해 타입 가드(Type Guard) 역할 수행
switch (msg.type) {
case 'text':
// 여기서 msg는 TextMessage 타입으로 자동 추론됨
console.log(`텍스트: ${msg.content}`);
break;
case 'image':
// 여기서 msg는 ImageMessage 타입으로 자동 추론됨
console.log(`이미지 링크: ${msg.url}, 크기: ${msg.size}KB`);
break;
case 'video':
console.log(`비디오 재생시간: ${msg.duration}초`);
break;
}
}
4. 유니온과 인터섹션의 조합
때로는 이 두 가지를 혼합하여 더욱 복잡한 비즈니스 로직을 표현할 수도 있습니다.
type ResponseBase = {
timestamp: number;
};
type SuccessResponse = ResponseBase & {
status: 'success';
data: string[];
};
type FailureResponse = ResponseBase & {
status: 'failure';
error: { code: number; message: string };
};
type ApiResponse = SuccessResponse | FailureResponse;
const handleApi = (response: ApiResponse) => {
if (response.status === 'success') {
console.log(response.data.length); // 안전하게 접근 가능
} else {
console.error(response.error.message); // 안전하게 접근 가능
}
};
결론: 왜 유니온과 인터섹션인가?
- 예측 가능성: 잘못된 속성 조합이 생성되는 것을 컴파일 단계에서 방지합니다.
- 재사용성: 작은 인터페이스들을 조합(Composition)하여 복잡한 도메인 모델을 구축할 수 있습니다.
- 가독성: 코드만 보고도 해당 데이터가 어떤 상태를 가질 수 있는지 명확히 파악할 수 있습니다.
타입스크립트 아키텍처 설계에서 유니온과 인터섹션은 단순히 타입을 나열하는 수단이 아닙니다. 이는 데이터 간의 관계를 정의하고 로직의 흐름을 안전하게 가이드하는 강력한 설계 언어입니다.