TypeScript 설계: Interface vs Type Alias & Generics
Interface vs Type의 구분 기준과 Generics를 활용한 API 설계 패턴을 작성해보았습니다.
TypeScript 설계: Interface vs Type Alias & Generics
이번 글에서는 협업하기 좋은 코드를 만드는 두 가지 핵심 무기, Interface vs Type의 구분 기준과 Generics를 활용한 API 설계 패턴에 대해 작성해보겠습니다.
Interface vs Type Alias
“객체 타입을 정의할 때 interface를 써야 해요, type을 써야 해요?” 이 질문에 대한 답은 명확합니다. 목적에 따라 다릅니다.
Interface
Interface는 ‘객체의 모양’을 정의하는 데 특화되어 있습니다. 가장 큰 특징은 선언 병합(Declaration Merging)이 가능하다는 점입니다. 똑같은 이름으로 Interface를 두 번 선언하면, 자동으로 하나로 합쳐집니다.
// 라이브러리 개발자나 확장성 높은 설계를 할 때 유리
interface User {
name: string;
}
// 나중에 누군가 같은 이름으로 기능을 추가함 (오류 아님!)
interface User {
age: number;
}
// 결과: User는 name과 age를 모두 가짐 (자동 병합)
const kim: User = { name: 'Kim', age: 30 };
┌────────────────┐ ┌────────────────┐
│ interface User │ │ interface User │
│ name: string │ │ age: number │
└───────┬────────┘ └───────┬────────┘
│ │
└───────────┬───────────┘
▼
최종 User Interface
┌───────────────────────┐
│ name: string │
│ age: number │
└───────────────────────┘
따라서 확장이 열려 있어야 하는 모델(Model) 정의나 객체 지향적인 상속(extends)이 필요할 때는 Interface가 유리합니다.
Type Alias
Type은 새로운 이름을 붙이는 것입니다. Interface가 할 수 없는 유니온(Union, OR 연산)이나 튜플, 원시 값에 이름을 붙일 때 강력합니다.
// 1. 유니온 타입 (Interface로는 불가능)
type Status = 'SUCCESS' | 'ERROR' | 'LOADING';
// 2. 복잡한 타입 조합 (Intersection)
type UserInfo = { name: string };
type UserAuth = { token: string };
// 두 타입을 합쳐서 새로운 타입을 만듦 (& 연산자)
type Admin = UserInfo & UserAuth;
Type은 선언 병합이 불가능합니다. 따라서 데이터의 값 자체를 한정하거나, 유틸리티 타입으로 데이터를 가공할 때 주로 사용합니다.
설계 원칙
- Interface: 객체 모델 정의 (확장성, 상속이 필요한 경우)
- Type Alias: 단순 데이터, Union, 튜플, 함수 타입 (엄격한 정의가 필요한 경우)
Generics
함수가 데이터를 인자로 받아 유연하게 동작하듯, Generics는 타입을 인자로 받아 유연하게 재사용하게 해줍니다. 특히 서버와 통신할 때 API 응답 타입을 표준화하는 데 필수적입니다.
흔한 비유보다는 실무적인 관점에서 보자면, Generics는 데이터를 담는 규격화된 봉투(Envelope) 역할을 합니다.
서버에서 내려주는 응답 구조가 보통 이렇다고 가정해봅시다.
{
"code": 200,
"message": "성공",
"data": { ...실제 데이터... }
}
Generics를 쓰지 않으면 UserResponse, ProductResponse 등을 일일이 다 만들어야 하며, 이는 엄청난 중복입니다.
이러한 문제를 해결하려면 ApiResponse<T>로 표준화하면 됩니다. T(Type)라는 자리를 비워두고, 실제 데이터가 들어올 때 타입을 주입하는 방식입니다.
// 1. 공통 껍데기(Envelope) 만들기
interface ApiResponse<T> {
code: number;
message: string;
data: T; // 여기가 핵심! T에 실제 데이터 타입이 주입됨
}
// 2. 실제 데이터 모델 정의
interface User {
id: number;
name: string;
}
interface Product {
id: number;
price: number;
}
// 3. 조립해서 사용 (재사용성 200%)
// T 자리에 User를 주입 -> data는 User 타입이 됨
const userRes: ApiResponse<User> = {
code: 200,
message: 'OK',
data: { id: 1, name: 'Kim' }
};
// T 자리에 Product를 주입 -> data는 Product 타입이 됨
const productRes: ApiResponse<Product> = {
code: 200,
message: 'OK',
data: { id: 1, price: 50000 }
};
console.log(userRes.data.name); // 자동 완성 지원
console.log(productRes.data.price); // 자동 완성 지원
1. 봉투 준비 (ApiResponse<T>)
┌───────────────────────────────┐
│ RESPONSE ENVELOPE │
│ - code: 200 │
│ - message: "OK" │
│ - data: [ T ] ◀── (구멍 뚫린 공간)
└───────────────────────────────┘
2. 내용물 주입 (Type Injection)
만약 <User>를 넣으면? 만약 <Product>를 넣으면?
┌──────────────────┐ ┌─────────────────────┐
│ ... │ │ ... │
│data: [ User 객체 ]│ │data: [ Product 객체 ]│
└──────────────────┘ └─────────────────────┘
(타입이 딱 맞게 들어감) (자동 완성 & 검사 가능)
만약 Generics가 없었다면 data: any를 썼을 것이고, 그랬다면 자동 완성도 안 되고 타입 안전성도 사라졌을 것입니다.
마치며
TypeScript 설계의 핵심은 ‘유연함’과 ‘엄격함’의 조화입니다.
- Interface를 사용해 객체의 뼈대를 잡고 확장 가능성을 열어야합니다.
- Type Alias를 사용해 구체적인 값의 범위(Union)나 조합을 엄격하게 정의해야합니다.
- Generics를 사용해 중복 코드를 없애고, 어떤 데이터가 들어오든 유연하게 받아주는 표준화된 틀을 만들어야합니다.
메타데이터
- post_id
- 371da0c3349a
- slug
- typescript-설계-interface-vs-type-alias-generics-371da0c3349a
- url
- https://medium.com/@DevOZ/typescript-%EC%84%A4%EA%B3%84-interface-vs-type-alias-generics-371da0c3349a
- canonical_url
- https://medium.com/@DevOZ/typescript-%EC%84%A4%EA%B3%84-interface-vs-type-alias-generics-371da0c3349a
- author_url
- https://medium.com/@DevOZ
- status
- ok
- fetched_at
- 2026-06-22 00:13:37