type과 interface, 무엇을 쓸까
TypeScript를 배우다 보면 자연스럽게 의문이 생깁니다. 객체 구조를 정의하는 방법이 두 가지 있는 것처럼 보입니다.
// 방법 1: interfaceinterface User { name: string; age: number;}// 방법 2: type aliastype User = { name: string; age: number;};
결과는 같아 보입니다. 두 방식으로 만든 User를 사용하는 코드는 동일하게 동작합니다. 그렇다면 무엇을 써야 할까요? 마치 커피숍에서 아메리카노와 롱블랙 중 무엇을 시킬지 고민하는 것처럼, 비슷해 보이지만 차이점이 있습니다.
차이점 1 — 선언 병합 (interface만 가능)
interface에는 같은 이름으로 여러 번 선언했을 때 자동으로 합쳐지는 특성이 있습니다. 이를 선언 병합(Declaration Merging)이라고 합니다.
interface Window { title: string;}interface Window { status: string;}// 위 두 선언이 자동으로 합쳐집니다const win: Window = { title: "내 앱", status: "running"};
같은 이름의 인터페이스를 두 번 선언해도 오류가 아닙니다. TypeScript가 두 선언을 하나로 병합합니다.
type은 이렇게 할 수 없습니다.
type Point = { x: number;};type Point = { // 오류: 식별자 'Point'가 중복되었습니다 y: number;};
선언 병합은 라이브러리를 만들 때 중요합니다. 예를 들어 라이브러리가 기본 Window 인터페이스를 제공하면, 사용자가 자신의 코드에서 Window 인터페이스를 한 번 더 선언해 속성을 추가할 수 있습니다. 이를 모듈 보강(Module Augmentation)이라고 합니다.
차이점 2 — 유니온과 인터섹션 (type만 가능)
앞에서 배운 유니온(|)과 인터섹션(&)을 이용한 타입 정의는 type으로만 할 수 있습니다.
// type으로는 가능type StringOrNumber = string | number;type Direction = "north" | "south" | "east" | "west";type Employee = PersonInfo & EmployeeInfo;// interface로는 불가능// interface StringOrNumber = string | number; // 문법 오류
인터페이스는 객체 구조를 정의하는 용도로 설계되었기 때문에, 기본 타입에 대한 유니온이나 단순 타입 별칭은 만들 수 없습니다.
차이점 3 — 확장 방식의 차이
인터페이스는 extends, 타입 별칭은 &로 확장합니다.
// interface 확장interface Animal { name: string;}interface Dog extends Animal { breed: string;}// type 확장type Animal = { name: string;};type Dog = Animal & { breed: string;};
기능적으로는 거의 같지만, 속성이 충돌할 때의 처리 방식이 다릅니다. 앞 챕터에서 살펴봤듯이 extends는 충돌 시 즉시 오류를 내고, &는 never로 조용히 처리합니다.
차이점 4 — 표현할 수 있는 타입의 범위
type은 객체 구조뿐만 아니라 어떤 타입이든 별칭을 붙일 수 있습니다.
// 기본 타입 별칭type ID = number;type Name = string;// 유니온type Result = "success" | "failure";// 튜플type Coordinate = [number, number];// 함수 타입type Callback = (error: Error | null, result: string) => void;// 복잡한 조건부 타입 (고급)type NonNullable<T> = T extends null | undefined ? never : T;
interface로는 이런 표현들을 할 수 없습니다. 객체와 함수 시그니처 정도가 인터페이스의 영역입니다.
에러 메시지의 차이
미묘하지만 실용적인 차이가 하나 더 있습니다. TypeScript가 타입 오류를 표시할 때, interface는 이름이 그대로 나타나지만, type으로 정의한 복잡한 타입은 내부 구조가 펼쳐져 나타날 수 있습니다.
interface UserInterface { name: string; age: number;}type UserType = { name: string; age: number;};function greet(user: UserInterface) {}// 오류 메시지: 'UserInterface' 형식의 인수가...function greetType(user: UserType) {}// 오류 메시지: '{ name: string; age: number; }' 형식의 인수가...
인터페이스는 이름이 보여서 오류 메시지가 더 짧고 명확합니다. 복잡한 타입일수록 이 차이가 두드러집니다.
실무 가이드라인
그렇다면 실제로 코드를 쓸 때 어떻게 결정할까요? 정답은 없지만, 많은 TypeScript 개발자들이 따르는 기준이 있습니다.
객체 구조를 정의할 때는 interface를 우선 사용합니다.
// 클래스, API 응답, 컴포넌트 props 등 객체 구조에는 interfaceinterface ApiResponse { data: User[]; total: number; page: number;}interface ButtonProps { label: string; onClick: () => void; disabled?: boolean;}
- 나중에 확장(
extends)하기 좋습니다. - 오류 메시지가 명확합니다.
- 라이브러리를 만든다면 선언 병합을 통해 사용자가 확장할 수 있습니다.
유니온, 인터섹션, 기본 타입 별칭에는 type을 사용합니다.
// 유니온이나 특수한 타입 표현에는 typetype Status = "pending" | "active" | "banned";type ID = string | number;type Nullable<T> = T | null;type EventHandler = (event: MouseEvent) => void;
일관성을 유지합니다.
팀이나 프로젝트에서 한 가지 방식으로 통일하는 것이 가장 중요합니다. 오늘은 interface, 내일은 type을 번갈아 쓰면 코드를 읽는 사람이 혼란스럽습니다.
Google의 TypeScript 스타일 가이드는 interface를 기본으로 권장하고, type은 유니온이나 인터섹션처럼 인터페이스로 표현할 수 없는 경우에만 씁니다. 반대로 일부 팀은 type을 기본으로 쓰기도 합니다. 어느 쪽이든 팀 내에서 합의하고 일관성을 유지하는 게 핵심입니다.
| 상황 | 권장 |
|---|---|
| 객체 구조 정의 | interface |
| 유니온 타입 | type |
| 인터섹션 타입 | type 또는 interface extends |
| 기본 타입 별칭 | type |
| 함수 타입 단독 정의 | type |
| 라이브러리 공개 API | interface (선언 병합 지원) |
interface와 type의 차이와 선택 기준을 정리했습니다. 이제 마지막으로 객체를 더 유연하고 안전하게 다루는 두 가지 도구를 배웁니다. 있어도 되고 없어도 되는 선택적 속성, 그리고 한번 정해지면 절대 바꿀 수 없는 읽기 전용 속성입니다.