TypeScript 독학 가이드 5 - 인터페이스

learning by Seven Fingers Studio 18분
TypeScriptInterface객체타입타입시스템웹개발

객체 다루다 보면 “이 객체 어떤 속성 있었지?” 헷갈릴 때 많죠. JavaScript는 실행해봐야 알고… TypeScript의 인터페이스 쓰면 이런 고민이 사라져요. 객체 구조를 명확하게 정의할 수 있거든요.

인터페이스가 뭔가요?

인터페이스는 객체의 형태를 정의하는 타입이에요. “이 객체는 이런 속성들을 가져야 해!”라고 선언하는 거죠.

interface User {
  name: string;
  age: number;
}

const user1: User = {
  name: "김철수",
  age: 25
};

console.log(user1);

실행 결과:

✓ 컴파일 성공
{ name: '김철수', age: 25 }

인터페이스에 정의된 대로 객체를 생성했습니다

속성이 빠지거나 타입이 안 맞으면 에러 나요:

const user2: User = {
  name: "이영희"
  // age 없음 - 에러!
};

const user3: User = {
  name: "박민수",
  age: "25"  // 문자열 - 에러!
};

실행 결과:

✗ 타입 에러
Property 'age' is missing in type
Type 'string' is not assignable to type 'number'

필수 속성이 없거나 타입이 맞지 않으면 에러가 발생합니다

선택적 속성

모든 속성이 필수는 아니에요. 있어도 되고 없어도 되는 속성은 ?를 붙입니다.

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);

실행 결과:

✓ 컴파일 성공
{ name: '김철수', age: 25, email: 'kim@example.com' }
{ name: '이영희', age: 30 }

선택적 속성은 있어도 되고 없어도 됩니다

읽기 전용 속성

한번 설정하면 바꿀 수 없는 속성은 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;            // 에러!

실행 결과:

✗ 타입 에러
Cannot assign to 'id' because it is a read-only property

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));

실행 결과:

✓ 컴파일 성공
15
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);

실행 결과:

✓ 컴파일 성공
{ '김철수': 90, '이영희': 85, '박민수': 88, '최준호': 92 }

문자열 키로 어떤 이름이든 추가할 수 있습니다

이러면 어떤 이름이든 키로 사용할 수 있어요. 값은 항상 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);

실행 결과:

✓ 컴파일 성공
{ name: '김철수', age: 20, studentId: '2024001', grade: 3 }

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);

실행 결과:

✓ 컴파일 성공
{ id: 1, firstName: '철수', lastName: '김', age: 25, email: 'kim@example.com' }

여러 인터페이스를 동시에 확장할 수 있습니다

인터페이스 병합

같은 이름의 인터페이스를 여러 번 선언하면 자동으로 합쳐져요.

interface User {
  name: string;
}

interface User {
  age: number;
}

// 자동으로 합쳐짐
const user: User = {
  name: "김철수",
  age: 25
};

console.log(user);

실행 결과:

✓ 컴파일 성공
{ name: '김철수', age: 25 }

같은 이름의 인터페이스는 자동으로 병합됩니다

이 기능은 라이브러리 타입을 확장할 때 유용해요. 근데 일반적으로는 헷갈릴 수 있으니 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}원`);
}

실행 결과:

✓ 컴파일 성공
상품명: 노트북
가격: 1500000원

API 응답 구조를 명확하게 정의했습니다

이렇게 API 응답 형태를 인터페이스로 정의하면 자동완성도 되고 타입 안전성도 보장돼요.

interface vs type

“인터페이스랑 type 키워드랑 뭐가 다른가요?” 자주 받는 질문이에요.

공통점

둘 다 객체 타입을 정의할 수 있어요:

interface UserInterface {
  name: string;
  age: number;
}

type UserType = {
  name: string;
  age: number;
};

차이점

  1. 확장 방법

    • interface: extends 사용
    • type: & 사용
  2. 선언 병합

    • interface: 가능
    • type: 불가능
  3. 사용 범위

    • interface: 객체, 클래스, 함수
    • type: 모든 타입 (유니온, 튜플 등)

실무에서는 객체는 interface, 나머지는 type 쓰는 경우가 많아요. 팀 컨벤션 따르면 됩니다.

정리하면

  • interface: 객체 구조를 정의하는 타입
  • 선택적 속성: ? 붙이면 필수 아님
  • readonly: 읽기 전용 속성
  • 확장: extends로 다른 인터페이스 상속
  • 인덱스 시그니처: 동적 키 허용
  • 병합: 같은 이름 자동 합쳐짐

다음 글에서는 클래스에 대해 배워볼게요. 접근 제한자, 상속, 추상 클래스까지 알아봅시다!


다음 글 보기

← 이전 글
TypeScript 독학 가이드 4 - 함수와 타입
다음 글 →
TypeScript 독학 가이드 6 - 클래스
← 블로그 목록으로