TypeScript 독학 가이드 5 - 인터페이스
객체 다루다 보면 “이 객체 어떤 속성 있었지?” 헷갈릴 때 많죠. JavaScript는 실행해봐야 알고… TypeScript의 인터페이스 쓰면 이런 고민이 사라져요. 객체 구조를 명확하게 정의할 수 있거든요.
인터페이스가 뭔가요?
인터페이스는 객체의 형태를 정의하는 타입이에요. “이 객체는 이런 속성들을 가져야 해!”라고 선언하는 거죠.
interface User {
name: string;
age: number;
}
const user1: User = {
name: "김철수",
age: 25
};
console.log(user1);
실행 결과:
인터페이스에 정의된 대로 객체를 생성했습니다
속성이 빠지거나 타입이 안 맞으면 에러 나요:
const user2: User = {
name: "이영희"
// age 없음 - 에러!
};
const user3: User = {
name: "박민수",
age: "25" // 문자열 - 에러!
};
실행 결과:
필수 속성이 없거나 타입이 맞지 않으면 에러가 발생합니다
선택적 속성
모든 속성이 필수는 아니에요. 있어도 되고 없어도 되는 속성은 ?를 붙입니다.
interface User {
name: string;
age: number;
email?: string; // 선택적 속성
phone?: string; // 선택적 속성
}
const user1: User = {
name: "김철수",
age: 25,
email: "kim@example.com"
};
const user2: User = {
name: "이영희",
age: 30
// email, phone 없어도 OK
};
console.log(user1);
console.log(user2);
실행 결과:
선택적 속성은 있어도 되고 없어도 됩니다
읽기 전용 속성
한번 설정하면 바꿀 수 없는 속성은 readonly를 붙여요.
interface User {
readonly id: number;
name: string;
age: number;
}
const user: User = {
id: 1,
name: "김철수",
age: 25
};
user.name = "김철수2"; // OK
user.id = 2; // 에러!
실행 결과:
readonly 속성은 수정할 수 없습니다
ID 같은 고유 값은 바뀌면 안 되니까 readonly로 지정하면 안전해요.
함수 타입 속성
객체 안에 함수도 정의할 수 있어요.
interface Calculator {
add(a: number, b: number): number;
subtract(a: number, b: number): number;
}
const calc: Calculator = {
add(a, b) {
return a + b;
},
subtract(a, b) {
return a - b;
}
};
console.log(calc.add(10, 5));
console.log(calc.subtract(10, 5));
실행 결과:
객체의 메서드를 인터페이스로 정의할 수 있습니다
화살표 함수 스타일
interface Calculator {
add: (a: number, b: number) => number;
subtract: (a: number, b: number) => number;
}
이것도 똑같이 동작해요. 스타일 차이일 뿐입니다.
배열 타입
배열도 인터페이스로 정의할 수 있어요.
interface StringArray {
[index: number]: string;
}
const fruits: StringArray = ["사과", "바나나", "오렌지"];
console.log(fruits[0]);
console.log(fruits[1]);
실행 결과:
인덱스 시그니처로 배열을 정의할 수 있습니다
근데 배열은 그냥 string[] 쓰는 게 더 간단해요. 인덱스 시그니처는 객체에서 더 유용합니다.
인덱스 시그니처
객체의 키를 동적으로 받고 싶을 때 사용해요.
interface ScoreBoard {
[name: string]: number;
}
const scores: ScoreBoard = {
"김철수": 90,
"이영희": 85,
"박민수": 88
};
scores["최준호"] = 92; // OK
console.log(scores);
실행 결과:
문자열 키로 어떤 이름이든 추가할 수 있습니다
이러면 어떤 이름이든 키로 사용할 수 있어요. 값은 항상 number 타입이어야 하고요.
인터페이스 확장
인터페이스는 다른 인터페이스를 확장할 수 있어요. 상속 같은 개념이에요.
interface Person {
name: string;
age: number;
}
interface Student extends Person {
studentId: string;
grade: number;
}
const student: Student = {
name: "김철수",
age: 20,
studentId: "2024001",
grade: 3
};
console.log(student);
실행 결과:
Student는 Person의 모든 속성을 포함합니다
Student는 Person의 속성을 전부 물려받고, 자기만의 속성(studentId, grade)도 추가한 거예요.
여러 인터페이스 확장
하나만 확장하는 게 아니라 여러 개도 가능해요:
interface Name {
firstName: string;
lastName: string;
}
interface Age {
age: number;
}
interface Contact {
email: string;
phone?: string;
}
interface User extends Name, Age, Contact {
id: number;
}
const user: User = {
id: 1,
firstName: "철수",
lastName: "김",
age: 25,
email: "kim@example.com"
};
console.log(user);
실행 결과:
여러 인터페이스를 동시에 확장할 수 있습니다
인터페이스 병합
같은 이름의 인터페이스를 여러 번 선언하면 자동으로 합쳐져요.
interface User {
name: string;
}
interface User {
age: number;
}
// 자동으로 합쳐짐
const user: User = {
name: "김철수",
age: 25
};
console.log(user);
실행 결과:
같은 이름의 인터페이스는 자동으로 병합됩니다
이 기능은 라이브러리 타입을 확장할 때 유용해요. 근데 일반적으로는 헷갈릴 수 있으니 extends를 쓰는 걸 추천합니다.
클래스와 인터페이스
클래스가 인터페이스를 구현(implements)할 수 있어요.
interface Animal {
name: string;
makeSound(): void;
}
class Dog implements Animal {
name: string;
constructor(name: string) {
this.name = name;
}
makeSound() {
console.log("멍멍!");
}
}
const dog = new Dog("바둑이");
console.log(dog.name);
dog.makeSound();
실행 결과:
클래스가 인터페이스를 구현했습니다
클래스는 다음 글에서 자세히 배울 거예요!
실전 예제: API 응답 타입
실무에서 인터페이스를 제일 많이 쓰는 게 API 응답 타입 정의예요.
interface ApiResponse<T> {
success: boolean;
data: T;
message?: string;
}
interface Product {
id: number;
name: string;
price: number;
category: string;
}
function fetchProduct(id: number): ApiResponse<Product> {
return {
success: true,
data: {
id: 1,
name: "노트북",
price: 1500000,
category: "전자제품"
}
};
}
const response = fetchProduct(1);
if (response.success) {
console.log(`상품명: ${response.data.name}`);
console.log(`가격: ${response.data.price}원`);
}
실행 결과:
API 응답 구조를 명확하게 정의했습니다
이렇게 API 응답 형태를 인터페이스로 정의하면 자동완성도 되고 타입 안전성도 보장돼요.
interface vs type
“인터페이스랑 type 키워드랑 뭐가 다른가요?” 자주 받는 질문이에요.
공통점
둘 다 객체 타입을 정의할 수 있어요:
interface UserInterface {
name: string;
age: number;
}
type UserType = {
name: string;
age: number;
};
차이점
-
확장 방법
- interface:
extends사용 - type:
&사용
- interface:
-
선언 병합
- interface: 가능
- type: 불가능
-
사용 범위
- interface: 객체, 클래스, 함수
- type: 모든 타입 (유니온, 튜플 등)
실무에서는 객체는 interface, 나머지는 type 쓰는 경우가 많아요. 팀 컨벤션 따르면 됩니다.
정리하면
- interface: 객체 구조를 정의하는 타입
- 선택적 속성:
?붙이면 필수 아님 - readonly: 읽기 전용 속성
- 확장:
extends로 다른 인터페이스 상속 - 인덱스 시그니처: 동적 키 허용
- 병합: 같은 이름 자동 합쳐짐
다음 글에서는 클래스에 대해 배워볼게요. 접근 제한자, 상속, 추상 클래스까지 알아봅시다!