새소식

반응형
언어/자바스크립트(JavaScript)

[TypeScript] 타입스크립트 interface vs. type

2024.05.08
  • -
반응형

 

1. 들어가기에 앞서

순수 자바스크립트에 비해 뛰어난 생산성 향상을 가져다 주는 타입스크립트정적 타입을 명시함으로써 코드 자동 완성이나 잘못된 변수/함수 사용에 대한 사전 에러 알림을 통해 코드 디버깅을 매우 쉽게 만들어준다.

 

이러한 타입스크립트에는 "타입"을 정의하는 데 두 가지 방식이 존재한다: "types"와 "interfaces"

 

많은 사람들이 두 키워드의 차이를 느끼지 못하고 나 역시도 타입스크립트를 사용함에 있어서 어떠한 사용 기준 없이 두 키워드를 혼용해서 사용하곤 했었다.

 

하지만 사용할수록 점점 나만의 규칙들이 생겨나는 것을 느낄 수 있었고 이를 조금 더 명확히 해서 코드를 작성할 때 일관성을 지키고 싶어 많은 글들을 찾아보게 되었고 이번 기회에 이에 대한 회고를 이 포스팅에 남겨보려고 한다.

 

먼저 type과 interface의 차이를 알아본 다음 어떻게 사용하는 것이 좋을지 말해보려 한다.

 

시작에 앞서 한 가지 주의해야 할 점은 두 키워드가 사실 사용성 측면에 있어서 굉장히 유사하고, 사실 혼용해서 사용해도 아무 문제가 없는 경우가 대부분이기 때문에 딱 정해진 사용 관례라고 할 것이 아직은 존재하지 않는다고 한다. 하지만 팀에서 타입스크립트를 사용한다면 어느 정도 규칙을 정해야 깔끔하고 일관된 코드를 유지할 수 있기 때문에 관례를 새로 정하거나 기존에 사용하던 관례를 따르도록 권장된다.

 

2. Type들과 type 별칭

타입스크립트에는 'type'이라고 정해 놓은 여러가지 타입이 존재하는데, 이들은 모두 우리가 다루는 데이터의 형태 묘사를 돕는 것들이다. 이러한 형태 묘사는 곧, 사용하는 데이터의 설계도라고 할 수 있기 때문에 이를 잘 활용하면 자바스크립트 코드에서 탄탄한 설계를 기반으로 굉장히 멋진 것들을 쉽게 할 수 있게 된다.

 

기본 타입에는 string, boolean, number, array, tuple, enum이 있다.

 

하지만 우리가 다루는 타입은 이게 다가 아니다! 추가적으로 우리는 type aliases(타입 별칭)라는 기능을 통해 위 타입 이외의 타입을 다루게 될 수도 있다. 이는 쉽게 말해, type들에게 닉네임을 붙여주는 일이라고 생각하면 된다. 즉, 새로운 타입을 정의하는 느낌이 아니라 그저 우리가 만든 어떤 타입에게 괜찮은 이름 하나를 붙여주는 것이라고 생각하면 된다.

 

이러한 과정을 통해 코드를 더욱 가독성있고 이해하기 쉽게 만들 수 있다.

 

type MyNumber = number;
type User = {
  id: number;
  name: string;
  email: string;
}

 

위 코드를 보면 알겠지만 number라는 원시 타입에 대해 "MyNumber"라는 type alias를 만들었고, 이로 인해 "number"로 타입을 명시하는 대신 "MyNumber"를 대신 사용할 수 있게 된 것이다.

 

또한, 한 user를 나타내는 type alias를 만들 수도 있는데, 이는 user 객체의 데이터가 어떻게 보여야 하는지를 묘사한다.

 

위에서 type alias라고 계속 말하는 것을 보면 알 수 있겠지만, 사람들이 type vs. interface에 대해 논할 때, 사실은 type alias vs. interface에 대해 말하는 것이라고 할 수 있다.

 

3. 타입스크립트에서의 interface

타입스크립트에서는 interface object가 따라야만 하는 규칙 또는 요구사항의 집합으로 취급한다. 이러한 요구사항을 명확히 함으로써 우리에게  "저기요, 'Client'에는 'name'과 'address'가 있어야 합니다."라며 에러를 알려주는 것이다.

 

interface Client { 
    name: string; 
    address: string;
}

 

그리고 이러한 규칙들을 표현하는 또 다른 방식이 있는데, 이것은 "type annotations"이라는 것을 사용하는 방식이다. 이는 마치 "여기 'Client'가 이렇게 생겼어요." 라고 말해주는 것과 같고, interface에 정의해 놓은 것처럼 'name'과 'address' 속성을 그것들의 type과 함께 나열한다.

 

type Client = {
    name: string;
    address: string;
};

 

따라서 interface를 사용하든 type annotation을 사용하든 본질적으로 'Client'가 어떻게 구성되어야 하는지에 대한 설계도의 집합을 제공하는 측면에서는 같다고 볼 수 있다. 그래서 이는 사실 동일한 명령어 세트에게 두 개의 다른 이름을 주는 것과 마찬가지이다.

 

4. type과 interface의 차이

앞서 말했듯 type과 interface는 둘 다 직접 만든 데이터의 구조와 모양을 정의하는데 사용하지만 이들은 각자의 동작과 사용방식에 있어서 몇 가지 차이를 가진다.

 

4-1. Primitive Types(원시 타입)

<type을 사용하는 경우>

type MyNumber = number;

 

위 코드를 보면 원시 타입 number에 대한 별칭(alias)으로 MyNumber라는 type alias를 만들어낸 것을 볼 수 있다.

 

<interface를 사용하는 경우>

반면, interface는 number와 같은 원시 타입을 직접적으로 정의하는 용도로 사용될 수 없다.

 

4-2. Union Types

<type을 사용하는 경우>

type MyUnionType = number | string;

 

MyUnionType은 number 또는 string의 값을 가질 수 있는 타입으로 정의가 된 모습이다.

 

<interface를 사용하는 경우>

interface로는 union type을 직접적으로 표현하는 용도로도 사용되지 않는다.

 

그래서 이러한 목적(union type)에서 역시 type alias를 사용해야 한다.

 

4-3. Function Types

<type을 사용하는 경우>

type MyFunctionType = (arg1: number, arg2: string) => boolean;

 

2개의 인자를 취하는 함수에 대한 타입, MyFunctionType이다. 이는 number와 string 인자를 받아 boolean 타입을 반환(return)하는 함수의 모양을 보여준다.

 

<interface를 사용하는 경우>

interface MyFunctionInterface {
  (arg1: number, arg2: string): boolean;
}

이 interface로 선언한 MyFunctionInterface는 위와 같은 함수의 타입을 interface 만의 방식으로 표현한 모습이다.

 

4-4. Declaration Merging(선언 병합)

<interface를 사용하는 경우>

interface Person {
  name: string;
}

interface Person {
  age: number;
}

 

TypeScript는 이 두 Person interface를 name과 age 속성을 모두 가진 하나의 interface로 자동 병합해준다.

 

<type을 사용하는 경우>

type alias는 선언 병합을 지원하지 않는다. 만약 같은 이름의 type을 여러개 정의한 경우 TypeScript에서는 에러를 응답할 것이다.

 

4-5. Extends vs. Intersection

<Extends를 사용하는 경우>

interface A { propA: number; }
interface B extends A { propB: string; }

 

interface B는 interface A를 확장(extend)하는데, 이때 A로부터 propA 속성을 상속하고 A에는 없던 새로운 속성인 propB를 추가하여 확장한다.

 

<Intersection을 사용하는 경우>

type AB = A & { propB: string; }

 

AB타입을 생성할 때, intersection 기호(&)를 사용하여 A의 기존 속성들이 새로운 propB 속성과 함께 결합한다는 것을 의미한다.

 

4-5. Exteding 시의 충돌을 다루는 법

TypeScript는 같은 이름을 가진 속성들의 타입이 extend 시에 똑같도록 강제된다.

interface A { commonProp: number; }
interface B { commonProp: string; }
interface AB extends A, B { }
// Error: Property 'commonProp' must have the same type in A and B

 

충돌을 해결하기 위해서는, type이 매칭되도록 하거나 메서드 오버로드를 사용할 필요가 있다.

 

4-6. Tuple 타입

<type을 사용하는 경우>

type MyTupleType = [number, string];
const tuple: MyTupleType = [42, "hello"];

 

위 코드에서는 type을 사용하여 튜플 타입을 정의했고, 이 튜플 타입의 변주를 몇 가지 만들어낼 수도 있다.

 

<interface를 사용하는 경우>

interface MyTupleInterface {
  0: number;
  1: string;
}
const tuple: MyTupleInterface = [42, "hello"];

 

interface를 사용함으로써도 튜플 타입을 정의할 수 있고, 사용 방식은 type의 경우와 같다.

 

5. 언제 type vs. interface를 사용하는가

기존의 구조를 결합하거나 수정해야 할 때 interface를 사용할 수 있다. 만약 특정 라이브러리를 사용하거나 새로운 라이브러리를 만들 때 interface는 가장 좋은 선택지가 될 수 있다.

 

interface는 기존 코드를 더 쉽게 만들면서 선언한 것들을 병합하거나 확장하기에 용이하다. 그래서 또한 OOP의 관점에서 생각할 때 가독성이 좋은 코드를 만들 수 있게 된다.

 

Type은 더 강력한 기능을 요구할 때 고를 수 있을 것이다. TypeScript의 타입 시스템은 조건부 타입(conditional type), 제네릭 타입(generic), 타입 가드(type guard) 등등의 더욱 심화된 도구들을 제공해준다.

 

이러한 기능들은 우리에게 타입 통제를 쉽게 할 수 있게 해줄 뿐 아니라 코드의 강인함을 만들어주기도 하며, 강력하게 타입화된 애플리케이션을 만들 수 있게 해줄 것이다. 하지만 Interface는 이러한 측면에서 제약이 있다.

 

type과 interface를 본인 선호에 따라 둘 다 사용할 수 있는 경우들이 존재하는데 다음 상황들에서는 type alias를 사용해보자.

  • 기본 데이터 타입에 대해 새로운 이름을 붙이고 싶을 때(ex. 'string' or 'number')
  • union, tuple, function과 같은 더 복잡한 타입을 정의할 때
  • 함수를 재정의(overload)할 때
  • mapped type, conditional type, type guard처럼 고도된 기능을 사용할 때

 

Type은 일반적으로 더 유연하고 표현이 쉽다. Type이 interface는 주지 못하는 더 넓은 범위의 심화 기능을 제공하고 TypeScript가 더 TypeScript스럽게 다양한 기능을 확장하도록 해준다.

 

또한 Type alias를 사용하여 object type에 대한 자동으로 getter 메서드를 생성할 수 있지만 interface는 그렇지 못하다.

 

type Client = {
    name: string;
    address: string;
}
type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type clientType = Getters<Client>;
// The result is: 
// {
//     getName: () => string;
//     getAddress: () => string;
// }

 

mapped type, template literal, 'keyof' 연산자를 사용하여 어떠한 object type에 대한 getter 메서드든 자동으로 다 만들어낼 수 있다.

 

게다가, 많은 개발자들이 type을 선호하는데 그 이유는 functional programming 패러다임과 잘 어울리기 때문이라고 한다.

 

TypeScript의 type expression의 풍부함은 코드 상의 type 안전성을 유지하면서 composition과 immutability와 같은 함수형 개념들이 더 잘 작동하도록 도와준다.

 

 

 

 

 

 

반응형
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.