타입스크립트 type vs interface: 언제 뭘 써야 할지 헷갈린다면? (완벽 정리)

 

타입스크립트(TypeScript), type 쓸까 interface 쓸까? 완벽 정리!

 

안녕하세요!

 

타입스크립트(TypeScript)를 사용하다 보면 typeinterface라는 두 가지 키워드를 만나게 되는데요.

 

둘 다 타입을 정의할 때 사용하는데, 많은 경우 서로 바꿔 써도 큰 문제가 없을 정도로 비슷해 보입니다.

 

하지만 알고 보면 둘 사이에는 중요한 차이점들이 있고, 각각 더 유용하게 쓰이는 상황들이 있습니다.

 

이번 글에서는 typeinterface가 어떻게 다른지, 그리고 언제 어떤 것을 선택하는 것이 좋을지 자세히 알아보겠습니다.

 

본격적인 비교에 앞서, 각각의 기본적인 사용법부터 가볍게 살펴볼까요?

 

1. 기본 사용법: typeinterface로 객체 타입 정의하기

먼저 객체의 모양, 즉 어떤 속성(property)들을 가져야 하는지 정의하는 방법을 알아보겠습니다.

// interface 사용 예시
interface User {
  name: string;
  age: number;
}

// type 사용 예시
type UserType = {
  name: string;
  age: number;
};

const user1: User = { name: '앨리스(Alice)', age: 25 };
const user2: UserType = { name: '밥(Bob)', age: 30 };

 

이 예시를 보면, interface를 쓰든 type을 쓰든 객체의 타입을 정의하는 방식이 거의 똑같다는 것을 알 수 있습니다.

 

User라는 이름의 interface는 이름(name)은 문자열(string) 타입, 나이(age)는 숫자(number) 타입이어야 한다고 정의합니다.

 

UserType이라는 이름의 type도 똑같이 정의하고 있습니다.

 

사용하는 데 있어서는 사실상 차이가 없어 보입니다.

 

2. 확장하기 (Extend)

기존에 정의된 타입을 바탕으로 새로운 타입을 만들고 싶을 때, 즉 타입을 '확장'하고 싶을 때는 어떻게 할까요?

 

interface는 '상속'이라는 개념을 사용해서 확장합니다.

// interface 확장 예시
interface User {
  name: string;
  age: number;
}

// User 인터페이스를 상속받아 Admin 인터페이스 정의
interface Admin extends User {
  role: string; // 역할(role) 속성 추가
}

const admin: Admin = {
  name: '앨리스(Alice)',
  age: 30,
  role: '관리자(Administrator)',
};

 

위 코드처럼 Admin 인터페이스는 User 인터페이스를 extends 키워드로 상속받았습니다.

 

이렇게 하면 AdminUser가 가진 nameage 속성을 그대로 물려받으면서, 추가로 role이라는 속성까지 가지게 됩니다.

 

type의 경우에는 '교차 타입(intersection type)'이라는 것을 사용해서 확장합니다.

// type 확장 예시
type UserType = {
  name: string;
  age: number;
};

// UserType과 새로운 객체 타입을 & 로 합쳐 AdminType 정의
type AdminType = UserType & {
  role: string;
};

const adminType: AdminType = {
  name: '밥(Bob)',
  age: 35,
  role: '관리자(Admin)',
};

 

type& (앰퍼샌드) 기호를 사용하는데요, 이는 여러 타입을 하나로 합쳐주는 역할을 합니다.

 

AdminTypeUserType이 가진 속성 '그리고(&)' role 속성을 가진 새로운 타입을 의미합니다.

 

정리하자면, interfaceextends 키워드를 사용해 상속 방식으로 확장하고, type& 기호(교차 타입)를 이용해 여러 타입을 합치는 방식으로 확장한다는 점이 다릅니다.

 

3. 선언 병합 (Declaration Merging)

'선언 병합'은 interface만의 독특한 특징 중 하나인데요. 이게 무엇인지 알아보겠습니다.

 

interface는 이름이 같은 interface를 여러 번 선언하면, 타입스크립트(TypeScript)가 이것들을 자동으로 하나로 합쳐줍니다.

// interface 선언 병합 예시
interface User {
  name: string;
}

// 같은 이름의 User 인터페이스 또 선언
interface User {
  age: number;
}

// 병합된 User 인터페이스 사용
const user: User = { name: '앨리스(Alice)', age: 25 };
// 결과: User 인터페이스는 name과 age 속성 모두 가짐

 

보시는 것처럼 User라는 이름의 interface를 두 번 선언했지만, 타입스크립트(TypeScript)는 이 둘을 합쳐서 nameage 속성을 모두 가지는 하나의 User 인터페이스로 만들어줍니다.

 

라이브러리 같은 곳에서 기존 타입을 확장해야 할 때 유용하게 쓰일 수 있습니다.

 

하지만 type은 선언 병합을 지원하지 않습니다.

// type 선언 병합 시도 (오류 발생)
type UserType = {
  name: string;
};

// 같은 이름의 UserType을 또 선언하면 오류 발생
type UserType = {
  age: number; // 오류: 'UserType' 식별자가 중복되었습니다.
};

 

같은 이름으로 type을 다시 정의하려고 하면, 타입스크립트(TypeScript)는 이를 허용하지 않고 오류를 발생시킵니다.

 

결론적으로, interface는 선언 병합이 가능하지만, type은 불가능하다는 중요한 차이가 있습니다.

 

4. 유니언 타입(Union Types)과 교차 타입(Intersection Types)

타입들을 조합하는 방식에서도 차이가 있는데요.

 

type은 여러 타입 중 하나가 될 수 있음을 나타내는 '유니언 타입(Union Type)'이나, 여러 타입을 모두 합친 '교차 타입(Intersection Type)'을 정의하는 데 아주 유용합니다.

// type으로 유니언 타입 정의
type Status = '성공(success)' | '오류(error)' | '로딩 중(loading)';

// type으로 교차 타입 정의
type Name = { name: string };
type Age = { age: number };
type Person = Name & Age; // Name 타입과 Age 타입을 합쳐 Person 타입 생성

const person: Person = { name: '앨리스(Alice)', age: 25 };

 

유니언 타입은 | (파이프) 기호를 사용해서 "A 또는 B 또는 C" 와 같이 여러 타입 중 하나가 될 수 있음을 나타냅니다.

 

Status 타입은 '성공', '오류', '로딩 중'이라는 세 가지 문자열 값 중 하나만 가질 수 있습니다.

 

교차 타입은 위에서 확장할 때 봤던 것처럼 & 기호를 사용해서 "A 그리고 B" 처럼 여러 타입을 합칩니다.

 

Person 타입은 name 속성과 age 속성을 모두 가져야 합니다.

 

반면에, interface는 유니언 타입을 직접 정의할 수 없습니다.

// interface로 유니언 타입 정의 시도 (오류 발생)
// interface Status = "성공(success)" | "오류(error)" | "로딩 중(loading)"; // 오류 발생!

 

이렇게 interface를 사용해서 유니언 타입을 만들려고 하면 문법 오류가 발생합니다.

 

정리하면, type은 유니언 타입과 교차 타입을 자유롭게 정의할 수 있어 타입 조합에 매우 유연하지만, interface는 유니언 타입을 직접 정의할 수 없다는 차이점이 있습니다.

(물론, interface도 객체 타입을 확장하는 데는 여전히 강력합니다.)

 

5. 타입 별칭 (Type Aliases)

type 키워드의 또 다른 강력한 기능은 바로 '타입 별칭'을 만드는 능력입니다.

 

타입 별칭이란, 특정 타입에 새로운 이름을 붙여주는 것을 말합니다.

// type으로 타입 별칭 만들기

// 기본 타입(primitive)이나 유니언 타입에 별칭 부여
type ID = number | string; // ID 타입은 숫자 또는 문자열

// 튜플(tuple) 타입에 별칭 부여 (배열인데, 길이와 각 요소의 타입이 고정됨)
type Point = [number, number]; // Point 타입은 숫자로 된 요소 2개를 가진 배열

const id: ID = 123;
const point: Point = [10, 20];

 

type을 사용하면 숫자나 문자열 같은 기본 타입, 여러 타입을 묶은 유니언 타입, 배열 요소의 타입과 개수가 정해진 튜플 타입 등 다양한 종류의 타입에 새로운 이름을 붙여줄 수 있습니다.

 

이렇게 하면 복잡한 타입을 좀 더 간결하게 표현하거나 코드의 의도를 명확하게 전달하는 데 도움이 됩니다.

 

반면에 interface는 주로 객체 형태의 타입을 정의하는 데 사용되고, 이런 식의 다양한 타입에 별칭을 붙이는 기능은 없습니다.

6. 클래스 구현 (Implements) 관점

객체 지향 프로그래밍에서 중요한 개념인 클래스(class)와 관련해서도 typeinterface를 사용할 수 있습니다.

 

interface는 클래스가 특정 구조, 즉 어떤 속성이나 메서드(기능)를 반드시 갖도록 강제하는 데 자주 사용됩니다. 이때 implements라는 키워드를 씁니다.

// interface를 클래스에서 구현하는 예시
interface User {
  name: string;
  age: number;
}

class Person implements User {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

 

Person 클래스가 User 인터페이스를 implements 한다고 선언하면, 이 클래스는 반드시 User 인터페이스에 정의된 nameage 속성을 가지고 있어야 합니다.

 

사실 type으로 정의된 타입도 클래스가 implements 할 수 있습니다.

// type을 클래스에서 구현하는 예시
type UserType = {
  name: string;
  age: number;
};

class PersonType implements UserType {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

 

하지만 일반적으로 클래스의 구조를 정의할 때는 interface를 더 많이 사용하는 경향이 있습니다.

 

왜냐하면 interface는 위에서 본 '선언 병합' 기능이 있고, extends를 통한 상속 방식이 클래스 설계와 좀 더 자연스럽게 어울리기 때문입니다.

 

type도 클래스에 제약을 가할 수는 있지만, 주로 유니언 타입이나 복잡한 타입을 조합해서 정의하는 데 더 강점을 보입니다.

 

7. 상호 운용성 (Interoperability)

interfacetype은 서로 완전히 분리된 것이 아니라, 함께 섞어서 사용할 수도 있습니다.

 

예를 들어, interfacetype으로 정의된 타입을 확장할 수도 있고, 그 반대로 typeinterface를 확장할 수도 있습니다.

// interface가 type을 확장하는 예시
type Name = { name: string };

interface User extends Name {
  age: number;
}

const user: User = { name: '앨리스(Alice)', age: 25 };

 

위 코드에서는 User 인터페이스가 type으로 정의된 Nameextends 키워드로 확장했습니다.

// type이 interface를 확장하는 예시
interface User {
  name: string;
  age: number;
}

type Admin = User & {
  role: string;
};

const admin: Admin = { name: '밥(Bob)', age: 35, role: '관리자(Admin)' };

 

이번에는 Admin 타입이 User 인터페이스를 & 연산자를 사용해 확장했습니다.

 

이처럼 interfacetype은 서로를 확장하며 유연하게 조합해서 사용할 수 있다는 장점이 있습니다.

 

8. 복잡한 타입 표현

 

복잡한 구조의 타입을 만들어야 할 때, typeinterface보다 더 강력한 표현력을 제공합니다.

// type으로 복잡한 타입 정의
type ComplexType = string | number | { name: string };

const value1: ComplexType = '안녕하세요(Hello)'; // 가능
const value2: ComplexType = 123;           // 가능
const value3: ComplexType = { name: '앨리스(Alice)' }; // 가능

 

type은 유니언 타입(|), 교차 타입(&), 타입 별칭 등을 지원하기 때문에, 위 예시처럼 문자열, 숫자, 또는 특정 구조의 객체 중 어느 것이든 될 수 있는 복잡한 타입도 쉽게 정의할 수 있습니다.

 

반면에 interface는 주로 객체의 구조를 명확하게 설명하는 데 초점을 맞추기 때문에, 이런 다양한 형태를 포괄하는 복잡한 타입 조합을 표현하는 데는 type보다 덜 적합합니다.

 

9. 그래서, 언제 뭘 써야 할까요? (요약)

지금까지 typeinterface의 여러 차이점을 살펴봤는데요.

 

이제 어떤 상황에서 무엇을 쓰는 게 더 좋을지 정리해 보겠습니다.

 

interface는 이럴 때 사용하면 좋습니다:

  • 객체의 구조를 정의할 때, 특히 객체 지향 디자인 방식으로 개발할 때.
  • 클래스가 특정 구조를 따르도록 강제하고 싶을 때 (implements 사용).
  • 같은 이름으로 선언된 것들을 합쳐야 하는 '선언 병합' 기능이 필요할 때 (예: 외부 라이브러리의 타입을 확장하거나 수정할 때).

type은 이럴 때 사용하면 좋습니다:

  • 유니언 타입(|)이나 교차 타입(&) 등 여러 타입을 조합해서 복잡한 타입을 만들어야 할 때.
  • 기본 타입(string, number 등)이나 튜플 타입 등에 별칭을 붙여 사용하고 싶을 때.
  • 좀 더 유연하게 타입을 정의하고 조합해야 하는 시나리오에서.

결론: 한눈에 보는 interface vs type

특징 interface type
객체 타입 정의 ✅ 가능 ✅ 가능
기본 타입 별칭 ❌ 불가능 ✅ 가능
선언 병합 ✅ 가능 ❌ 불가능
확장 방식 extends (상속) & (교차 타입)
유니언 타입 정의 ❌ 불가능 ✅ 가능
교차 타입 정의 ❌ (확장은 가능) ✅ 가능
클래스 구현 ✅ 가능 (일반적) ✅ 가능 (덜 일반적)
표현력 객체 구조 정의에 적합 복잡한 타입 조합, 별칭 만들기에 적합
주요 사용처 객체 지향 디자인, 클래스와 함께 사용 유니언/교차 타입, 복잡한 타입 정의, 타입 별칭

 

정리하자면, interface는 객체 지향 프로그래밍 방식, 특히 클래스 설계나 상속과 관련해서 사용할 때 더 자연스럽고 강력한 면모를 보입니다.

 

반면에 type은 좀 더 유연해서, 다양한 타입을 조합하거나 별칭을 붙여 사용하는 등 복잡한 타입 관련 시나리오에서 빛을 발합니다.

 

실제 프로젝트에서는 이 둘의 장점을 잘 이해하고 상황에 맞게 선택하거나, 때로는 서로 보완적으로 함께 사용하는 것이 좋습니다.

 

이제 typeinterface 사이에서 덜 헷갈리시겠죠?