객체의 설계도 — 인터페이스
건물을 짓기 전에 건축가는 설계도를 그립니다. 설계도에는 방이 몇 개인지, 각 방의 크기는 얼마인지, 창문은 어디에 있는지가 적혀 있습니다. 실제 건물은 나중에 지어지지만, 설계도가 있기 때문에 공사팀 모두가 같은 그림을 보며 일할 수 있습니다.
TypeScript의 interface가 바로 그 설계도입니다. 객체가 어떤 속성을 가져야 하고, 각 속성의 타입은 무엇인지를 미리 정의합니다. 그 설계도를 바탕으로 실제 객체가 만들어집니다.
인터페이스 기본 문법
JavaScript에서 사용자 정보를 담는 객체는 이런 모습이었습니다.
const user = { name: "이민준", age: 22, email: "[email protected]"};
아무 제약 없이 만들 수 있지만, 어디선가 user.naem처럼 오타를 내거나 age에 문자열을 넣어도 JavaScript는 아무 말도 하지 않습니다. 런타임에서야 이상한 동작으로 나타납니다.
TypeScript에서는 이 구조를 인터페이스로 먼저 정의합니다.
interface User { name: string; age: number; email: string;}
interface 키워드 뒤에 이름을 쓰고, 중괄호 안에 속성 이름과 타입을 나열합니다. 속성 구분은 세미콜론(;)을 씁니다. 이제 이 인터페이스를 타입으로 사용합니다.
interface User { name: string; age: number; email: string;}const user: User = { name: "이민준", age: 22, email: "[email protected]"};
설계도(User)를 따르는 건물(user)이 완성되었습니다. TypeScript는 이제 user.naem처럼 존재하지 않는 속성에 접근하면 즉시 오류를 알려줍니다. 또한 age에 "스물둘"이라는 문자열을 넣으려 하면 타입 불일치 오류가 발생합니다.
설계도를 벗어난 건물은 지어질 수 없는 것처럼, 인터페이스를 벗어난 객체는 TypeScript가 허용하지 않습니다.
중첩 객체 타이핑
현실의 데이터는 보통 단층 구조가 아닙니다. 사용자는 주소를 가지고, 주소는 다시 도시와 우편번호로 구성됩니다. 인터페이스는 이런 중첩 구조도 자연스럽게 표현합니다.
interface Address { city: string; zipCode: string;}interface User { name: string; age: number; email: string; address: Address;}const user: User = { name: "이민준", age: 22, email: "[email protected]", address: { city: "서울", zipCode: "04524" }};
Address 인터페이스를 별도로 정의하고, User 인터페이스의 address 속성 타입으로 사용했습니다. 설계도 안에 또 다른 설계도를 참조하는 방식입니다.
이렇게 하면 user.address.zipCode에 숫자를 넣으려 하거나, user.address.country처럼 존재하지 않는 속성에 접근하려 할 때 TypeScript가 즉시 알려줍니다.
물론 인터페이스를 따로 분리하지 않고 인라인으로 쓸 수도 있습니다.
interface User { name: string; age: number; email: string; address: { city: string; zipCode: string; };}
간단한 중첩 구조라면 이렇게 써도 됩니다. 다만 Address 같은 구조가 여러 곳에서 재사용된다면 별도 인터페이스로 분리하는 것이 좋습니다.
인터페이스 확장 (extends)
설계도도 물려받을 수 있습니다. 기본 설계도가 있고, 거기에 기능을 추가한 확장 설계도를 만드는 방식입니다.
예를 들어, 기본 사용자 정보가 있고 관리자에게는 추가 권한 정보가 필요하다고 해봅시다.
interface User { name: string; age: number; email: string;}interface Admin extends User { role: string; permissions: string[];}const admin: Admin = { name: "박지수", age: 28, email: "[email protected]", role: "superadmin", permissions: ["read", "write", "delete"]};
Admin extends User는 "Admin은 User의 모든 속성을 가지면서 추가로 role과 permissions도 갖는다"는 뜻입니다. 관리자를 만들 때 사용자 정보(name, age, email)를 반드시 포함해야 하고, 여기에 관리자 전용 속성(role, permissions)이 더해집니다.
여러 인터페이스를 동시에 확장하는 것도 가능합니다.
interface Timestamped { createdAt: Date; updatedAt: Date;}interface SoftDeletable { deletedAt: Date | null;}interface Post extends Timestamped, SoftDeletable { title: string; content: string;}
Post는 Timestamped와 SoftDeletable의 모든 속성을 포함하면서 자신만의 속성도 추가합니다. 여러 관심사를 작은 인터페이스로 분리하고, 필요한 곳에서 조합하는 방식입니다.
함수 속성 타이핑
인터페이스는 함수 속성도 정의할 수 있습니다. 현실에서도 설계도에 "이 방에는 인터폰 설비가 있어야 한다"는 식의 기능 요구사항을 적는 것처럼요.
interface Greeter { name: string; greet(): string; farewell(message: string): void;}const greeter: Greeter = { name: "안내봇", greet() { return `안녕하세요, ${this.name}입니다.`; }, farewell(message: string) { console.log(`${message}, 안녕히 가세요.`); }};
greet(): string은 매개변수 없이 문자열을 반환하는 메서드, farewell(message: string): void는 문자열 매개변수를 받고 반환값이 없는 메서드를 정의합니다.
인터페이스를 실제로 써보기
지금까지 배운 내용을 모아서 간단한 할 일 관리 시스템의 타입을 설계해봅시다.
interface Todo { id: number; title: string; completed: boolean; createdAt: Date;}interface TodoList { todos: Todo[]; addTodo(title: string): Todo; completeTodo(id: number): void; getTodoById(id: number): Todo | undefined;}
TodoList 인터페이스는 할 일 목록과 그 목록을 다루는 함수들을 함께 정의합니다. 실제 구현이 어떻게 되든, 이 설계도를 따르는 객체는 반드시 todos 배열을 가지고, addTodo, completeTodo, getTodoById 함수를 제공해야 합니다.
이렇게 인터페이스로 설계도를 먼저 그리면, 여러 명이 협업할 때 "내가 만든 함수는 이런 구조의 객체를 받는다"는 약속을 코드로 표현할 수 있습니다. 설계도를 공유하면 공사팀이 같은 기준으로 일하듯이요.
이제 객체의 구조를 정의하는 설계도, 인터페이스를 익혔습니다. 그런데 현실에서는 "사용자 ID가 숫자일 수도 있고 문자열일 수도 있다"처럼 여러 타입 중 하나를 받아야 하는 경우가 생깁니다. 다음 챕터에서는 "이것 또는 저것"을 표현하는 유니온 타입을 알아봅니다.