타입스크립트 기초 강좌 #3 interface와 type alias

15 분 소요

지난 시간에는 기본 타입들을 정리하면서 객체 타입을 인라인으로 적었습니다. 그런데 같은 모양을 여러 곳에서 써야 할 때 매번 인라인으로 적으면 코드가 금방 지저분해지죠. 이번 글에서는 객체와 함수의 타입 모양에 이름을 붙여 재사용하는 두 도구 — **interface**와 **type alias**를 다룹니다.

인라인 타입의 한계

다음 코드를 보세요.

인라인 타입의 반복
function logUser(user: { id: string; name: string; email: string }): void {
  console.log(user.name);
}
 
function saveUser(user: { id: string; name: string; email: string }): void {
  // ...
}
 
const me: { id: string; name: string; email: string } = {
  id: 'u-1', name: '철수', email: 'cheolsu@example.com',
};

{ id: string; name: string; email: string }이 세 번 반복됩니다. 필드를 하나 추가하려면 세 군데 모두 고쳐야 하고, 오타나 누락이 생기면 타입 검사가 어긋나요.

해결책 — 이 모양에 이름을 붙입니다.

type alias

type 키워드로 타입에 별명을 지을 수 있습니다.

type alias
type User = {
  id: string;
  name: string;
  email: string;
};
 
function logUser(user: User): void {
  console.log(user.name);
}
 
function saveUser(user: User): void {
  // ...
}
 
const me: User = {
  id: 'u-1', name: '철수', email: 'cheolsu@example.com',
};

훨씬 깔끔하죠. User라는 이름 하나로 같은 모양을 여러 곳에서 가리킵니다.

type객체뿐 아니라 어떤 타입에도 별명을 붙일 수 있습니다.

다양한 type alias
type ID = string;                              // 원시 타입에도
type Point = [number, number];                 // 튜플
type Color = 'red' | 'green' | 'blue';         // union
type Maybe<T> = T | null;                      // 제네릭 (#6에서)
type Callback = (err: Error | null, value: string) => void;  // 함수

이름 짓기 컨벤션은 보통 PascalCase(User, OrderItem)입니다.

interface

같은 객체 타입을 interface 키워드로 표현할 수도 있습니다.

interface
interface User {
  id: string;
  name: string;
  email: string;
}
 
function logUser(user: User): void {
  console.log(user.name);
}

위의 type alias와 거의 같은 결과입니다.

interface객체와 함수 형태에만 쓸 수 있다는 점이 type alias와 다릅니다 (원시 타입이나 union에는 못 씀). 그 외에는 거의 같이 동작해요.

interface vs type — 어느 쪽을 써야 하나?

가장 자주 받는 질문입니다. 결론부터 말하면:

거의 모든 경우 같이 동작한다. 작은 차이가 있긴 하지만 처음에는 신경 쓰지 않아도 된다.

대부분의 팀이 다음 둘 중 하나를 선택해 일관되게 사용합니다.

  • type 우선 — 모든 곳에 type을 쓰고 객체에도 type을 씀. 이유: 일관성. React/Next.js 커뮤니티가 이쪽 경향
  • interface 우선 — 객체엔 interface, 그 외엔 type. 이유: 객체에 더 잘 맞고 약간의 추가 기능 (선언 병합)

이 시리즈에서는 type 우선 스타일로 진행합니다. 이유는 (1) 표현력이 더 넓어서 모든 케이스를 같은 키워드로 다룰 수 있고 (2) 최근 React/TS 커뮤니티의 다수 선택이라서요. 다만 interface도 익숙해져야 합니다 — 라이브러리 코드를 읽을 때 자주 만나니까요.

작은 차이들

1. 선언 병합 (declaration merging)

interface는 같은 이름으로 여러 번 선언되면 자동으로 합쳐집니다.

interface 선언 병합
interface User {
  name: string;
}
 
interface User {
  age: number;
}
 
const u: User = { name: '철수', age: 30 };  // ✓ 합쳐져서 둘 다 필요

type은 같은 이름으로 두 번 선언하면 에러:

type 중복 선언 금지
type User = { name: string };
type User = { age: number };  // 🚫 에러

선언 병합은 외부 라이브러리의 타입을 확장할 때 유용합니다 (Window, Express.Request 같은). 일상 코드에서는 거의 안 쓰지만 가끔 필요한 강력한 기능이에요.

2. 확장 문법

interface는 extends로 상속받는 모양:

interface 확장
interface Animal {
  name: string;
}
 
interface Dog extends Animal {
  breed: string;
}
 
const d: Dog = { name: '바둑이', breed: '진돗개' };

type은 & (intersection)으로 합치는 모양:

type intersection
type Animal = { name: string };
 
type Dog = Animal & { breed: string };
 
const d: Dog = { name: '바둑이', breed: '진돗개' };

거의 같은 결과지만 미묘한 차이가 있어요. 충돌 케이스에서는 동작이 달라질 수 있는데 (interface는 에러로 막아주는 반면 &never로 만들어버리는 등) 일상적인 사용에선 큰 차이가 안 느껴집니다.

3. type만 가능한 것들

union, tuple, 함수 타입, 매핑된 타입 등은 type alias만으로 표현 가능합니다.

type만 가능
type Status = 'pending' | 'active' | 'deleted';   // union
type Point = [number, number];                     // tuple
type Reducer<T> = (state: T, action: any) => T;    // 함수 시그니처를 별명으로

interface는 객체/함수 모양에만 쓸 수 있어 이런 표현은 못 합니다.

객체 타입 더 자세히

이제 type 또는 interface로 정의된 객체 타입의 다양한 표현을 살펴봅시다.

옵셔널 프로퍼티

옵셔널
type User = {
  id: string;
  name: string;
  age?: number;          // 있어도 되고 없어도 됨
  email?: string;
};
 
const a: User = { id: 'u-1', name: '철수' };                          // ✓
const b: User = { id: 'u-2', name: '영희', age: 28 };                  // ✓
const c: User = { id: 'u-3', name: '민수', age: 35, email: 'm@x.com' };// ✓

readonly

readonly
type User = {
  readonly id: string;
  name: string;
};
 
const u: User = { id: 'u-1', name: '철수' };
u.name = '영희';     // ✓
u.id = 'u-2';        // 🚫

인덱스 시그니처 — 동적 키

키 이름을 미리 모를 때 사용합니다.

동적 키
type StringMap = {
  [key: string]: string;
};
 
const dict: StringMap = {
  apple: '사과',
  banana: '바나나',
  cherry: '체리',
};
 
dict['durian'] = '두리안';  // 추가 가능

{ [key: string]: T }는 "문자열을 키로 갖고 T 타입의 값을 갖는 객체"를 의미합니다. 보통 알려진 키 + 동적 키를 섞어 쓸 때 유용해요.

알려진 키 + 동적 키
type FormData = {
  name: string;
  email: string;
  [extra: string]: string;   // 그 외 임의의 string 키도 허용
};

메소드 시그니처

객체에 함수가 들어가는 모양도 표현 가능합니다.

메소드
type Counter = {
  value: number;
  increment(): void;
  add(n: number): number;
};
 
const c: Counter = {
  value: 0,
  increment() { this.value += 1; },
  add(n) { return this.value + n; },
};

함수 시그니처를 프로퍼티로 적은 형태도 같은 의미입니다.

프로퍼티 형태
type Counter = {
  value: number;
  increment: () => void;
  add: (n: number) => number;
};

두 표기는 거의 같은데 미묘한 차이(strictFunctionTypes 옵션 하에서)가 있어요. 일상적으론 둘 다 잘 동작합니다.

함수 타입 별명

함수 시그니처에도 이름을 붙일 수 있습니다.

함수 타입 별명
type Comparator = (a: number, b: number) => number;
 
const ascending: Comparator = (a, b) => a - b;
const descending: Comparator = (a, b) => b - a;
 
[3, 1, 4, 1, 5].sort(ascending);

Comparator라는 이름으로 같은 함수 모양을 여러 곳에서 재사용. 콜백이 자주 등장하는 라이브러리 API에서 흔히 보이는 패턴입니다.

타입 합치기 — 실전

기존 타입을 결합해 새 타입을 만드는 패턴은 자주 등장합니다.

두 타입 합치기

intersection
type Person = { name: string; age: number };
type Employee = { company: string; salary: number };
 
type EmployedPerson = Person & Employee;
 
const me: EmployedPerson = {
  name: '철수',
  age: 30,
  company: 'Acme',
  salary: 50000,
};

A & B는 "A의 모든 속성과 B의 모든 속성을 다 가진 타입"입니다.

일부 필드만 가져오기

기존 타입에서 일부 골라 쓰기
type User = { id: string; name: string; email: string; age: number };
 
type UserPreview = Pick<User, 'id' | 'name'>;
//   = { id: string; name: string }
 
type UserWithoutAge = Omit<User, 'age'>;
//   = { id: string; name: string; email: string }

PickOmit은 타입스크립트 내장 유틸리티 타입입니다. 실전에서 정말 자주 쓰여요. #7에서 더 자세히 다룹니다.

직접 해보기 — 작은 도서관 시스템

지금까지 배운 걸 종합한 작은 예시.

library.ts
type Book = {
  readonly id: string;
  title: string;
  author: string;
  publishedYear?: number;
  tags: string[];
};
 
type Member = {
  readonly id: string;
  name: string;
  email: string;
};
 
type Loan = {
  bookId: string;
  memberId: string;
  borrowedAt: Date;
  dueDate: Date;
  returnedAt?: Date;
};
 
type LoanWithDetails = Loan & {
  book: Book;
  member: Member;
};
 
function isOverdue(loan: Loan): boolean {
  if (loan.returnedAt) return false;
  return new Date() > loan.dueDate;
}
 
const book1: Book = {
  id: 'b-1',
  title: '타입스크립트 입문',
  author: '저자',
  publishedYear: 2024,
  tags: ['프로그래밍', '입문'],
};
 
const member1: Member = {
  id: 'm-1',
  name: '철수',
  email: 'cheolsu@example.com',
};
 
const loan1: Loan = {
  bookId: book1.id,
  memberId: member1.id,
  borrowedAt: new Date('2026-04-01'),
  dueDate: new Date('2026-04-15'),
};
 
console.log(isOverdue(loan1));  // 오늘 날짜 기준

type 정의들이 차곡차곡 쌓이고, 그 위에서 isOverdue 같은 함수가 안전하게 동작합니다. 이런 게 타입스크립트의 진짜 가치예요 — 데이터 모양을 미리 명시하고 그걸 기반으로 함수와 컴포넌트를 만드는 흐름.

흔한 함정

1. interface 중복 선언이 의도와 다르게 합쳐짐

같은 이름의 interface를 두 곳에 쓰면 자동으로 합쳐집니다. 이게 의도한 거면 좋지만, 우연히 다른 모듈에서 같은 이름을 쓰면 예상 못한 합병이 일어날 수 있어요. 큰 코드베이스에서는 type이 더 안전한 면이 있습니다.

2. 객체 리터럴의 excess property check

excess property check
type User = { name: string };
 
const u: User = { name: '철수', age: 30 };  // 🚫 age는 User에 없음
 
const data = { name: '철수', age: 30 };
const v: User = data;  // ✓ 객체 변수를 통해 할당하면 OK (excess check 안 함)

객체 리터럴을 직접 할당할 때만 추가 속성 검사가 강합니다. 변수를 거치면 좀 더 관대해져요. 처음 보면 이상한데 의도된 동작입니다.

3. union vs intersection 혼동

A | B는 "A 또는 B 중 하나" (union, #4), A & B는 "A와 B의 합성" (intersection).

객체에 대해서는 헷갈리기 쉬운데, intersection이 "둘 다의 속성을 가진 객체"라는 점을 기억하세요. union은 "둘 중 하나의 모양"이고요.

마무리

이번 글에서는 객체와 함수의 타입에 이름을 붙이는 두 도구를 다뤘습니다.

  • type — 어떤 타입이든 별명 가능 (객체, 원시, union, tuple, 함수 ...)
  • interface — 객체/함수 모양 전용. 선언 병합 가능
  • 거의 모든 경우 호환되며, 팀 컨벤션 따라 일관되게 사용
  • 옵셔널 ?, readonly, 인덱스 시그니처 같은 객체 타입 표현
  • & (intersection)으로 타입 합치기, Pick/Omit으로 일부 골라 쓰기

지금까지의 타입은 하나의 모양이었어요. "이건 User", "이건 Book". 그런데 실제로는 "이건 User 또는 Guest", "상태는 loading 또는 success 또는 error" 같이 여러 가능성 중 하나인 경우가 많습니다. 다음 글인 "타입스크립트 기초 강좌 #4 Union / Literal / Narrowing"에서는 그런 경우를 다루는 강력한 도구들 — union 타입, literal 타입, narrowing을 살펴보겠습니다.