우리 팀의 우아한 타입스크립트 컨벤션 정하기 여정

Dec.12.2022 신기훈

Web Frontend

팀에서의 코딩 컨벤션

여러 명이 코드를 같이 작성하는 프로젝트에서는 개인마다 다른 코딩 스타일, 생각들이 반영된 코드가 작성될 수 있습니다.
그렇게 되면서 점차 코드가 읽기 어려워지기도 합니다.
탭 또는 스페이스, 스페이스면 2칸 또는 4칸, 세미콜론을 쓰냐, 안 쓰냐 등의 취향을 탈 수 있는 것부터
변경하지 않는 변수는 let이 아니라 const로 선언하기, 사용하지 않는 import 문은 지워주기,
React에서 리스트 컴포넌트에 key를 강제하기처럼 단순히 취향이라고 하기보다 효율적이고 견고한 코드를 위한 컨벤션도 있습니다.

우리 팀이 새로 만들어지면서 컨벤션을 새로 정하자는 회의에서 자체적인 규칙을 정했고,
ESLint, Prettier, Husky를 이용해서 코드 리뷰하기 전 자신의 커밋에서 규칙을 맞춘 후에 코드 리뷰를 위한 PR을 올리게 하였습니다.
물론, 이후에 막상 써보니 이 룰은 별로 같은데? 이 룰도 추가되었으면 좋겠는데? 와 같은 것들이 있으면 팀원들의 합의를 통해서 수정도 하였고요.

즉, 위와 같은 절차로 툴이 어느 정도 팀원들의 코딩 스타일을 일정하게 맞춰줄 수 있게 되었는데요, 위에서 명시하지 못한 툴로는 강제하기 어렵거나 힘든 스타일도 있습니다.

복수에 대한 네이밍 컨벤션을 예로 들어보겠습니다.
업종에 대한 정보를 담고 있는 category라는 변수가 있다고 했을 때, 이를 담는 리스트에 대한 이름은 어떻게 할까요?
categories라고 할 수도 있고 categoryList라고 할 수도 있습니다.
user는 users라고 할 수도 있고 userList라고 할 수도 있습니다.
어디서는 users라고 하고 어디서는 userList라고 쓰면 볼 때마다 조금 헷갈리겠죠?
wolf, news, contents까지 들어오면 더 헷갈리기 시작합니다…-_-;

이러한 복수에 대한 네이밍을 위해서, 단수명 + List라는 규칙으로 약속을 정했습니다.
단순히 보면 +s를 붙이는 게 더 나을 수도 있는데 어떤 단어는 s를 붙이고, 어떤 단어는 es를, 어떤 단어는 ies를, 어떤 단어는 f를 v로 바꿔주고 es를…
어렵죠 영어는 ㅜ
그래서 네이밍할 때 이러한 수고를 덜고, List라는 이름만 보면, 아 이건 복수형이구나 라는 것을 알 수 있게 + list라는 규칙으로 정하였습니다.

갑자기 위에서 변수명에 대한 컨벤션을 왜 말했을까요?
위와 같은 컨벤션은 정답이 있는 게 아닙니다.
각각의 장단점이 있고, 정답에 가까울 수 있는 무엇인가가 있을 수는 있지만
같이 프로그래밍하는 동료들끼리의 합의된 하나의 약속만 있으면, 그것이 최선이라고 생각합니다.
그러한 과정에서 충분한 이야기가 있어야 하는 건 당연하고요.
정하고 나서도 피드백을 통해서 수정될 여지가 있어야 하는 것도 당연합니다.
이 글에서는 그 중, 타입스크립트로 개발을 해나가는 데 있어서 저희 팀에서 고려했던 점을 살펴보고 최종적으로 정한 컨벤션 3가지를 말씀드려보고자 합니다.

타입스크립트 – enum에 대한 컨벤션

enum?

타입스크립트를 이용하면서 겪는 고민도 있습니다. 그 중, enum에 대해서 이야기해보겠습니다.

enum의 정의를 Wikipedia에서 찾아보면 아래와 같습니다.[1]

컴퓨터 프로그래밍에서 열거형(enumerated type, enumeration), 이넘(enum), 팩터(factor ← R 프로그래밍 언어와 통계학의 범주형 변수에서 부르는 명칭)는 요소, 멤버라 불리는 명명된 의 집합을 이루는 자료형이다. 열거자 이름들은 일반적으로 해당 언어의 상수 역할을 하는 식별자이다.

기본적으로 다른 언어에는 enum이 있지만, Javascript에는 enum이 없죠(아직은).
하지만 우리는 Typescript를 사용하기에 enum을 사용할 수 있습니다.

AR, CN, JP, KR, GU, GT, GN, US의 8개 국가 코드가 있다 가정해보겠습니다.

type CountryCode = 'AR' | 'CN' | 'JP' | 'KR' | 'GU' | 'GT' | 'GN' | 'US'
const countryCode: CountryCode = 'AR'

이 국가 코드가 들어가는 변수에 대한 타입을 지정해줄 때, 위와 같이 union type으로 CountryCode를 만들고 이를 변수에서 사용할 수 있습니다. 그런데 이 경우에 GU, GT, GN이 어떤 나라에 대한 국가 코드인지 구분이 잘 되나요? 이보다는 각 value 설명으로 key값이 있으면 좋겠죠. 이럴 때, 3가지의 방법이 있습니다.

enum과 대체제 소개

  1. enum 사용
enum CountryCode {
  'Argentina' = 'AR',
  'China' = 'CN',
  'Japan' = 'JP',
  'Korea' = 'KR',
  'Guam' = 'GU',
  'Guatemala' = 'GT',
  'Guinea' = 'GN',
  'UnitedStates' = 'US',
}

const country: CountryCode = CountryCode.Guam
const countryList: CountryCode[] = [CountryCode.Korea, CountryCode.Guinea]
  1. const enum 사용
const enum CountryCode2 {
  'Argentina' = 'AR',
  'China' = 'CN',
  'Japan' = 'JP',
  'Korea' = 'KR',
  'Guam' = 'GU',
  'Guatemala' = 'GT',
  'Guinea' = 'GN',
  'UnitedStates' = 'US',
}

const country2: CountryCode2 = CountryCode2.Guam
const countryList2: CountryCode2[] = [CountryCode2.Korea, CountryCode2.Guinea]
  1. const assertions
const CountryCode3 = {
  'Argentina': 'AR',
  'China': 'CN',
  'Japan': 'JP',
  'Korea': 'KR',
  'Guam': 'GU',
  'Guatemala': 'GT',
  'Guinea': 'GN',
  'UnitedStates': 'US',
} as const

type CountryCode3Type = typeof CountryCode3[keyof typeof CountryCode3]  // 이렇게 별도의 union type을 지정해줘야 함

const country3: CountryCode3Type = CountryCode3.Argentina
const countryList3: CountryCode3Type[] = [CountryCode3.Argentina, CountryCode3.Guinea]

왜 enum을 단순히 사용하면 될 것 같은데, const enum과 const assertions까지 써가면서 다른 대안들을 제시했을까요?

Typescript에서 enum을 찾아보신 분들은 이미 아는 내용이겠지만, 위의 3가지 방법에는 각각의 장단점이 있습니다. 이와 관련해서도 여러 의견이 있습니다. 그에 대해서는 글의 마지막 참고 링크에 링크를 걸어두었으니, 여러 글을 읽어보시는 것도 좋을 것 같습니다.

이러한 여러 의견 속에서 저희가 협업을 효율적으로 하기 위해, 어떤 식의 의사결정을 하게 되었는지를 살펴보도록 하겠습니다.

먼저, 각각 방식에 대한 장단점을 살펴보도록 하겠습니다.

enum과 대체제의 장단점

  1. enum

    • 장점
      1. key 값에 의미 있는 값을 부여할 수 있습니다.
      2. type에 enum 자체를 지정하면 따로 key에 대한 type을 만들지 않아도 돼서 편리합니다.
      3. 값과 타입 모두 사용 가능하기 때문에, Object.keys, Object.entries를 이용해서 값들에 대한 순회가 편리합니다.(number value 사용하지 않을 때)
      4. enum을 쓰는 쪽에서는 오타를 낼 확률이 0%가 됩니다. 무조건 enum 값으로 써야 하기 때문입니다.(number value 사용하지 않을 때)
      5. value가 number 형일 때, 양방향 매핑이 가능합니다.
    • 단점
      1. tree-shaking이 되지 않습니다.
      2. js로 변환되고 나면 비교적 용량이 커집니다.
      3. value 값이 number가 들어가 있는 상태면, 문제가 발생합니다.
      4. value가 number 형일 때, 양방향 매핑이 가능합니다.
  2. const enum

    • 장점
      1. tree-shaking이 됩니다.
      2. js로 변환되고 나면, 매우 적은 용량을 차지합니다.
      3. key 값에 의미 있는 값을 부여할 수 있습니다.
      4. enum을 쓰는 쪽에서는 오타를 낼 확률이 0%가 됩니다. 무조건 enum 값으로 써야 하기 때문입니다.(number value 사용하지 않을 때)
    • 단점
      1. Object.keys, Object.entries를 이용해서 값들에 대해 순회를 할 수가 없습니다.
      2. –isolatedModules 옵션을 사용하면, 사용 불가합니다.
      3. ts 패키지를 만들고, 이를 다른 곳에서 사용한다면, 이를 타입용으로 사용할 수가 없습니다.
      4. cra, next에서 사용 불가합니다.
      5. babel 설정도 추가로 필요
  3. Object as const

    • 장점
      1. tree-shaking이 됩니다.
      2. enum에 비해서 js로 변환되고 나면, 적은 용량을 차지합니다.
      3. 값과 타입 모두 사용 가능하기 때문에, Object.keys, Object.entries를 이용해서 값들에 대해 순회를 할 수 있습니다.
      4. key 값에 의미 있는 값을 부여할 수 있습니다.
    • 단점
      1. 이것을 위한 별도의 union type을 만들어야 해서, 성가십니다.
      2. 원래의 의도가 enum과는 다르기 때문에, 쓰기가 약간은 망설여집니다.

위에서 세 가지 방식의 장단점에 대해서 나열해보았는데, 이에 대해서 표로 다시 한번 정리해보았습니다.
(민트색 배경이 상대적으로 좋음을 의미합니다)
각각의 장단점이 있지만, 저희는 최종적으로 enum을 사용하기로 하였는데요, enum의 단점이라고 여겨지는 것들을 어떻게 처리하기로 했는지, 그리고 enum을 사용하기로 결정한 장점이라고 생각한 부분에 대해서 이야기해보도록 하겠습니다.

enum과_대체제들의_비교

enum의 단점들 살펴보기

먼저 enum의 단점 a, b를 살펴보겠습니다.

rollup 기반 번들러에서 tree-shaking이 되지 않는다고 하였는데, 이는 번들러마다 다를 수 있을 것 같기도 하고, 추후 구현에 따라 tree-shaking이 가능해질 것도 같다고 생각합니다. vite[2] 기준으로는 현재도 tree-shaking이 되는 것을 확인했고요.

vite에서의_tree_shaking

또한 tree-shaking이 되지 않았다고 해도, 그 enum 선언부가 엄청나게 크지 않은 이상, 이런 정도의 용량 다이어트가 필요한가 싶기도 했고요. 정말 프로젝트의 번들링 크기를 극한으로 줄여야 하는 게 아닌 이상, 이 정도는 무시해도 괜찮을 거라 생각했습니다.

다음으로는 enum의 단점 c를 살펴보겠습니다.
enum의 value를 number로 사용할 때의 단점인데요,

enum CountryCode5 {
  Argentina = 1,
  China = 2,
  Japan = 3,
}

const country51: CountryCode5 = CountryCode5.China
const country52: CountryCode5 = 2
const country53: CountryCode5 = 10    // ...???

위는 enum의 value 값이 number일 때, 벌어질 수 있는 이슈입니다.

country51은 의도한 대로 CountryCode5.China를 입력했는데,
country52에서는 China에 해당하는 2 값을 직접 입력해주었습니다. (이걸 원하지는 않았다고 하더라도 오류는 안 납니다)

country53은 CountryCode5에는 없는 10이라는 값을 입력했는데 전혀 에러가 발생하지 않습니다.

또한 enum의 value가 number로 되어 있으면, enum을 Object.keys, Object.entries로 순회가 어려워집니다.

따라서 이러한 단점을 피하기 위해서 enum을 사용하더라도 value는 number가 아닌 string만 사용하는 게 좋다고 생각했습니다.

enum의 단점 d이자 장점 e는 value가 number형일 때, 양방향 매핑이 가능하다는 점입니다.

enum CountryCode7 {
  Argentina = 1,
  China = 2,
  Japan = 3,
}

const key = CountryCode7[1]               // Argentina
const value = CountryCode7['Argentina']   // 1
const keyList = Object.keys(CountryCode7)   // ['1', '2', '3', 'Argentina', 'China', 'Japan']

위와 같이, CountryCode7의 Argentina와 1이 양방향으로 매핑되어 있습니다. 하지만, 이러한 양방향 매핑이 어떨 때 좋은 쪽으로 사용할 수 있을지에 대한 케이스는 찾지 못하였습니다. 오히려, 이 경우에 Object.keys(CountryCode7)에 원치 않는 키값들이 추가로 나와서, 단점이 될 수도 있다는 생각이 들었습니다.

따라서, enum의 단점 c에서 언급한 것과 같이 enum을 사용하더라도 value는 number가 아닌 string만 사용하는 게 좋다고 생각했습니다.

enum의 장점 살펴보기

이제 enum의 장점에 대해서 좀 더 살펴보겠습니다.

enum CountryCode {
  'Argentina' = 'AR',
  'China' = 'CN',
  'Japan' = 'JP',
  'Korea' = 'KR',
  'Guam' = 'GU',
  'Guatemala' = 'GT',
  'Guinea' = 'GN',
  'UnitedStates' = 'US',
}

const country: CountryCode = CountryCode.Guam
// const countryList: CountryCode[] = [CountryCode.Korea, CountryCode.Guinea, 'US']   // 이렇게 enum이 아닌 string값 자체 X
const countryList: CountryCode[] = [CountryCode.Korea, CountryCode.Guinea]

const countryEntryList = Object.entries(CountryCode) // CountryCode의 key, value 순회

key 값에 Guam, Guatemala, Guinea 등과 같은 의미 있는 값을 부여할 수 있고, 이를 사용하는 곳에서 별도의 타입을 만들 필요 없이, CountryCode 자체를 타입으로 지정해줄 수 있습니다.

또한 Object.keys나 Object.entries를 이용해서 key, value 들에 대한 순회가 편리하고요,
이를 사용하는 곳에서 string literal을 직접 넣게 되면 에러가 발생하고, CountryCode.Guam과 같은 형태로 넣어주어야 하므로, 오타를 낼 확률이 0%가 됩니다.

이를 해주기 위해서 const enum은 const enum의 단점에서 언급한 점들 때문에, 선택하기가 망설여졌고, Object as const로 만들어두고 별도의 union type을 만들어서 사용하는 경우는 enum과 비슷하게 사용할 수 있지만, 결정적으로 별도의 union type을 만든다는 것이 수고스럽고, 명시적으로 보이지 않아서, 최종적으로 enum을 선택하게 되었습니다.

여기까지의 논의 내용들을 토대로, 최종적으로 enum을 쓰되, enum의 단점을 최소화하고, 장점을 극대화하기 위해서, value 값은 string 형으로 사용하자고 결론을 내렸습니다. (강제까지는 아니고 권유로 시작)

타입스크립트 – any vs unknown

anyscript…

타입스크립트 프로젝트의 tsconfig에서 strict를 true로 설정하거나, noImplicitAny를 true로 설정하면, 각종 변수에 타입을 지정하지 않았을 때 에러가 발생합니다. 타입스크립트에서 기본적으로 타입 추론이라는 것을 하는데, 추론한 근거가 없으면 암시적으로 any로 할당됩니다.
이러한 것을 못하게 하는 옵션이 noImplicitAny(말 그대로, no implicit any – 암시적인 any는 no)를 true로 설정하는 것이고, strict를 true로 설정하면 noImplicitAny도 true로 설정되죠.
타입스크립트 프로젝트에서 strict 모드를 설정하는 것은, 프로젝트에 엄격한 룰을 강제해서 추후에 코드를 수정하기 쉽게 만들어주는데, 이러한 프로젝트에서 막상 타입 지정하기가 귀찮아서 any로 일단 선언해두고 사용하는 경우가 있습니다. 이런 게 반복되고 도배되기 시작하면, 이건 타입스크립트가 아니라 anyscript라고 부를 수도 있을 만한 상황이 될 수도 있겠죠. 즉, 귀찮아서 잠시 사용할 수 있을지는 몰라도, any는 어떤 타입이든 모두 올 수 있음을 말하기 때문에 자바스크립트를 쓰는 것과 다름이 없어지는 상황이 되어서 권장하지 않습니다.

unknown?

비슷한 것으로 unknown[3]도 있는데, any와의 차이는 뭘까요? any는 아무거나 다 올 수 있고, 그 이후 어떤 동작을 해도 되는 것에 비해, unknown도 아무거나 다 올 수 있지만, 그 이후는 어떤 동작도 허용하지 않습니다. 말이 어려운데 코드를 통해서 살펴보겠습니다.

const sayNumber = (num: number) => {
  console.log('num: ', num)
}

const sum = (a: any, b: any): any => a + b
const three = sum(1, 2)
sayNumber(three)

const minus = (a: any, b: any): unknown => a - b
const five = minus(7, 2)
sayNumber(five) // 'unknown' 형식의 인수는 'number' 형식의 매개 변수에 할당될 수 없습니다.

sayNumber라는 함수가 있습니다. 이 함수는 number 형식의 num이라는 값을 입력받아서, 콘솔에 출력해주는 함수입니다.
다음으로 sum이라는 함수는 a, b를 입력 받아서 a+b를 리턴해주는 함수입니다. 이 함수의 return type을 보면 any로 명시하였죠. 변수 three는 sum 함수에 1, 2를 전달해서 3이라는 return 값을 받았지만 이 변수의 type은 any입니다. minus라는 함수는 a, b를 입력 받아서 a-b를 리턴해주는 함수입니다. 이 함수의 return type은 unknown으로 명시하였습니다. 변수 five는 minus 함수에 7, 2를 전달해서 5라는 return 값을 받았지만 이 변수의 type은 unknown입니다.

sayNumber함수에 any type의 three를 전달하면, 아무런 에러 없이 "num: 3"이 출력됩니다.
하지만 sayNumber함수에 unknown type의 five를 전달하면 ‘unknown’ 형식의 인수는 ‘number’ 형식의 매개 변수에 할당될 수 없습니다. 라는 에러 메시지가 나오게 됩니다.
타입을 unknown으로 지정은 할 수 있지만, 해당 타입으로 지정된 값을 사용할 때가 되면 그 타입이 어떤 타입인지 명확히 알아야 사용할 수 있게 막아주는 역할을 한다고 보면 됩니다.

위 코드의 경우 minus 함수의 return type을 명시적으로 지정해주거나 five 변수의 type을 number라고 타입 단언을 해주면 사용할 수 있게 됩니다.

const minus = (a: any, b: any): unknown => a - b
const five = minus(7, 2) as number
sayNumber(five)

물론 위의 예제처럼 타입 단언을 이용하기보다는 minus 함수의 타입을 명확하게 지정해주거나 generic을 사용하는 게 일반적이긴 하겠지만, unknown을 설명하기 위해 만든 예제입니다.

다시 정리해보면 any, unknown 둘 다 ‘아무거나’에 들어갈 수 있지만 unknown은 그것을 사용하는 쪽에서 특별한 처리를 강제하게 해서, 안전하게 사용할 수 있게 해줍니다.

unknown을 사용해야 할 상황

위에서 unknown에 대해서 알아보았는데, any와 같이 사용하지 말아야만 할까요?
적은 상황이지만, unknown은 아래의 예제와 같이 사용할 케이스가 있습니다.

const isNil = (param: unknown): boolean => param === null || param === undefined

isNil이라는 함수는 param을 입력받아서, 해당 param이 null이나 undefined인지를 체크해서 true/false를 리턴해줍니다. 이 경우, param은 단순히 null, undefined인지에 대한 체크만 하기 때문에, 어떠한 타입이 들어올지 몰라도 됩니다. 이때 unknown을 사용할 수 있습니다. 물론 이때 any를 사용할 수도 있지요. 프로젝트의 설정에 따라서 any를 아예 사용하지 못하게 하는 경우도 있는데, isNil의 param 같은 경우 unknown 타입이 적절하다고 생각합니다.

하나의 예시를 더 들어보겠습니다. [4]

function prettyPrint(x: unknown): string {
  if (Array.isArray(x)) {
    return "[" + x.map(prettyPrint).join(", ") + "]"
  }
  if (typeof x === "string") {
    return `"${x}"`
  }
  if (typeof x === "number") {
    return String(x)
  } 
  return "etc."
}

위 prettyPrint 함수는 x라는 param을 입력받는데, 그 param이 array, string, number일 때와 그 밖의 상황에 대해서 처리를 하고 있는데, 3번째 줄에 있는 map(), join()은 array에서 사용할 수 있는 javascript 내장함수입니다. x가 any일 때는, x가 어떤 것이라고 하더라도 map(), join()을 사용할 수 있는데, x가 unknown일 때는, x가 array일 때만 map(), join()을 사용할 수 있게 강제해줍니다. (그냥 사용한다면, ‘x’ is of type ‘unknown’. 이라는 에러 메시지를 보여줍니다)
따라서, any 대신 unknown을 사용해서 더 안전한 코드를 작성할 수 있게 됩니다.

any, unknown의 결론

위에서 any, unknown에 대해서 살펴보았습니다.

1. any는 아무때나 쓸 수 있지만, typescript를 쓰는 의미가 없게 되어버릴 수 있다.
2. unknown을 어쩔 수 없이 써야할 상황이 있을 수도 있으나, 이를 사용하는 쪽에서의 방어처리를 해서 안전하게 사용가능하다.

위와 같은 이유로, 궁극적으로 우리는 any를 사용하지 않기로 정했고, 피치 못하게 사용해야 할 상황이나, 꼭 필요한 상황에는 unknown을 사용하기로 정하였습니다. 그렇게 해서, 사용하는 쪽에서 한 번의 처리를 더 강제하고, 안전하게 사용을 유도하는 것이죠.

타입스크립트 – interface vs type alias

타입스크립트에서 함수의 매개변수를 나타내거나 각종 변수의 타입을 지정해주기 위해서 interface나 type alias를 사용할 수 있습니다. 개인마다의 취향에 따라, 기분에 따라 다르게 사용하기도 해서, 정리하는 시간을 가졌습니다.

함수의 type을 지정해줄 때

함수의 타입을 지정해주는 경우는 interface보다 type alias가 낫다고 생각하였습니다.

const getDoubleString = (id: number): string => String(id * 2)

type TGetDoubleString = (id: number) => string
const getDoubleString1: TGetDoubleString = (id) => String(id * 2)

interface IGetDoubleString {
  (id: number): string;
}
const getDoubleString2: IGetDoubleString = (id) => String(id * 2)

getDoubleString처럼 함수의 parameter와 리턴값에 직접 타입을 지정하는 경우면 상관이 없는데 getDoubleString1, getDoubleString2처럼 함수 자체의 타입을 지정하고자 할 때는 TGetDoubleString처럼 type alias로 지정할 수도 있고, IGetDoubleString처럼 interface로 지정해줄 수도 있습니다. 이 경우 type alias가 실제 함수의 모양과 비슷하게 보여서 더 낫다고 생각하였습니다. 이는 고차함수일 때 더 두드러집니다.

type TAdd = (a: number) => (b: number) => number
const add: TAdd = (a) => (b) => a + b
const five = add(2)(3)

위와 같이 add라는 고차함수를 TAdd라는 type alias로 나타내면 매우 직관적입니다.
반면에, 아래처럼 IAdd라는 interface로 add2의 타입을 지정해줄 수 있는데, type alias에 비해서 직관적이지 못하죠. 물론 IAdd3처럼 interface를 나타낼 수도 있긴 한데, 그래도 TAdd가 더 직관적으로 보입니다.

interface IAddInner {
  (b: number) : number
}

interface IAdd {
  (a: number): IAddInner
}

const add2: IAdd = (a) => (b) => a + b
const five2 = add2(2)(3)

interface IAdd3 {
  (a: number): (b: number) => number
}

const add3: IAdd3 = (a) => (b) => a + b
const five3 = add3(2)(3)

component의 props나 API 응답 객체의 type을 나타내는 경우

이 경우 interface를 사용하는 것을 더 선호했었는데, 팀에서 정리하다 보니 IDE에서의 미리보기에 둘의 지원이 다른 것을 발견하였습니다.

type TAnimal = {
  age: number
  name: string
}

const cat: TAnimal = {
  age: 8,
  name: 'myomyo',
}

interface IAnimal {
  age: number
  name: string
}

const cat2: IAnimal = {
  age: 6,
  name: 'meow',
}

위와 같이 cat은 TAnimal로, cat2는 IAnmial로 type을 지정하였습니다. 그리고 cat옆의 TAnimal, cat2 옆의 IAnimal에 마우스 커서를 가지고 가면 어떻게 될까요?

vscode에서의_type_interface

둘의 차이가 느껴지시나요? interface의 경우 미리보기가 되지 않습니다! 위 화면은 vscode의 화면이고 IntelliJ에서도 같은 결과입니다.
맥 기준 vscode에서는 cmd키를 누르고 마우스 위로 올렸을 때 그나마 인터페이스에서도 미리보기가 됩니다. (IntelliJ에서는 이렇게 해도 되지 않습니다)
물론 미리보기가 되지 않더라도 맥 기준 cmd키를 누르고 인터페이스를 클릭하면 인터페이스로 이동해서 어떤 것인지 확인할 수 있고, 다시 뒤로 돌아오기를 하면 되긴 하겠지만 바로 볼 수 있는 것과 비교해서는 불편함이 있죠.
거기에 저희 팀은 개인의 선호에 따라 IDE를 다르게 사용하고도 있습니다. 코드와 다르게 툴은 개인의 손에 제일 잘 맞는 게 생산성을 좋게 해줄 거라는 생각이 있기에, 굳이 통일하지 않았습니다. vscode파와 IntelliJ파가 있죠. (에네르기파는 없습니다…)
vscode에서는 cmd키 누르고 인터페이스 위로 이동해서 미리보기를 할 수라도 있지만 IntelliJ에서는 이동해야지만 확인할 수 있기에 어떻게 보면 작은 차이지만 type alias가 조금 더 낫다고 의견을 모았습니다.

선언 병합의 관점

이번에는 선언 병합[5]의 관점에서 interface와 type alias를 비교해보겠습니다.

interface IShape {
  width: number
  height: number
}

interface IShape {
  scale: number
}

const shape: IShape = {
  width: 12,
  height: 10,
  scale: 2,
}

위의 예제에서와 같이 interface는 선언 병합이 가능합니다. 그래서 이미 선언된 interface가 있어도, 추후 이 interface에 type에 대한 정보를 추가할 수 있죠.

type TShape = {
  width: number
  height: number
}

type TShape = { // 'TShape' 식별자가 중복되었습니다.
  scale: number
}

type alias의 경우에는 위와 같이 똑같은 이름을 사용할 수 없습니다. 식별자가 중복되었다고 하면서 에러가 발생합니다. 이러한 성질 때문에 이 프로젝트가 아닌 외부에서도 이 타입을 쓸 수 있다고 하면, type alias가 아닌 interface로 사용하는 것이 좋다고도 합니다.[6] 하지만 그렇지 않은 프로젝트라고 가정하면, 굳이 interface를 사용할 이유는 적어 보입니다. 오히려 실제 프로젝트에서는 interface로 사용하다가, 의도치 않게 선언 병합이 발생한 경우가 있었습니다. 과거에 특정 모델에 대한 type을 interface로 지정해두었는데, 다음에 특정 모델과 비슷한 type을 같은 이름의 interface로(모르고) 지정해서, 해당 데이터에는 다음에 지정한 type의 데이터만 있었는데 과거에 지정한 type의 값을 참고해서 에러가 발생했었죠. 이는 interface가 아닌 type alias로 지정되었으면 생기지 않을 일이기도 했습니다.

그래서 어떤 것을 사용할 것인가?

위에서 여러 가지 관점으로 interface와 type alias를 살펴보았는데
둘이 거의 비슷하지만, type alias가 가지는 장점은 다음 3가지라고 생각했습니다.

1. 함수의 type을 나타내는데 더 직관적이다.
2. IDE에서 미리보기를 더 잘 지원한다.
3. 원치 않는 선언 병합을 막아준다.

최종으로 저희 팀에서는 type alias를 사용하자고 결정하였습니다.

이 글에서의 예제들에서 type alias의 이름은 T로 시작하였고, interface의 이름은 I로 시작하였습니다. 실제로 T, I를 앞에 붙이는 네이밍 규칙은 사용하지 않고 있지만, type alias와 interface를 보기 쉽게 설명하기 위해서 붙였습니다.

결론

이 글에서 팀에서 진행하는 프로젝트의 컨벤션에 대해서 언급하였고, 간단한 변수명에 대한 컨벤션부터, 타입스크립트를 사용하면서 결정해야 할 여러 컨벤션을 정하는 과정에 대해서 살펴보았습니다.
enum의 경우 약간의 tree-shaking 이슈를 감안하더라도 그 영향이 미미하다고 한다면 개발의 편의성, 가독성을 위해서 enum을 사용하자로 정하였습니다. any는 사용하지 않기로, 특수한 경우에는 unknown을 사용하기로 정하였습니다. 마지막으로 interface와 type alias에 대해서 살펴보고, 최종적으로는 type alias를 사용하자고 정하였습니다.

이러한 결정은 글의 서두에 말한 것처럼 사용해보면서 충분히 바뀔 수 있는 약속입니다. 구성원 간의 충분한 토론이나 조사 등의 과정을 거치면서 모범 사례(best practice)를 찾아갈 수 있다고 생각합니다. 구성원끼리 열린 자세로 탐구 과정을 거치다 보면 서로 놓쳤던 부분도 알게 되고 경험을 공유하며 좋은 개발 문화가 만들어진다고 생각합니다.

좋은 문화를 함께 만들어 가실 분들도 찾고 있습니다. 배민외식업광장서비스팀에서 더 우아한 컨벤션을 만들어보시는 건 어떨까요?

배민외식업광장서비스팀 프론트엔드 개발자 모집 공고 바로 가기

배민외식업광장서비스팀 백엔드 개발자 모집 공고 바로 가기

팀에서 하는 일이 궁금하다면 클릭 – 배민외식업광장에서는 뭐해요?

참고 링크

https://velog.io/@skawnkk/interface-type-Alias – [TypeScript] 여러 타입 선언 방법과 interface & type Alias 비교

https://typescript-kr.github.io/pages/enums.html#유니언-열거형과-열거형-멤버-타입-union-enums-and-enum-member-types – Typescript Handbook – Enum

https://yrnana.dev/post/2022-02-04-enum-union-type – enum보다 union type을 사용하자

https://velog.io/@leehaeun0/typescript-enum-을-union으로-변경하기 – typescript enum 을 union으로 변경하기

https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums – Objects vs Enums

https://velog.io/@logqwerty/Enum-vs-as-const – Enum vs as const

https://handhand.tistory.com/277 – [TypeScript] enum 과 union type, 언제 써야할까?

https://yceffort.kr/2022/03/typescript-use-union-types-instead-enum – 내가 타입스크립트에서 Enum을 잘 쓰지 않는 이유

https://velog.io/@milkcoke/Typescript-enum-vs-Union-types – [Typescript] enum vs Union types

https://darrengwon.tistory.com/1310 – enum, const enum, as const의 enum화

https://velog.io/@sensecodevalue/Typescript-Enum-왜-쓰지-말아야하죠 – [Typescript] Enum 왜 쓰지 말아야하죠?

https://engineering.linecorp.com/ko/blog/typescript-enum-tree-shaking/ – TypeScript enum을 사용하지 않는 게 좋은 이유를 Tree-shaking 관점에서 소개합니다.

https://xpectation.tistory.com/218 – [TS] Typescript의 enum, const enum, as const 에 대해 알아보자

https://medium.com/@seungha_kim_IT/typescript-enum을-사용하는-이유-3b3ccd8e5552 – TypeScript enum을 사용하는 이유

https://ajdkfl6445.gitbook.io/study/typescript/enum-type-union-type – enum type 대신 union type으로 변경하기

https://blog.toycrane.xyz/typescript에서-효과적으로-상수-관리하기-e926db079f9 – Typescript에서 효과적으로 상수 관리하기

https://www.typescriptlang.org/docs/handbook/2/functions.html#unknown – typescript unknown

https://velog.io/@skawnkk/interface-type-Alias – [TypeScript] 여러 타입 선언 방법과 interface & type Alias 비교

[1] https://ko.wikipedia.org/wiki/열거형 – Wikipedia의 Enum 설명

[2] https://vitejs.dev – vite 공식홈페이지

[3] https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#new-unknown-top-type – What’s New in Typescript 3.0, New unknown top type

[4] https://blog.logrocket.com/when-to-use-never-and-unknown-in-typescript-5e4d6c5799ad/ – When to use never and unknown in TypeScript

[5] https://typescript-kr.github.io/pages/declaration-merging.html – typescript handbook(한글), merging interfaces

[6] https://medium.com/@martin_hotell/interface-vs-type-alias-in-typescript-2-7-2a8f1777af4c – Martin Hochel, Interface vs Type alias in TypeScript 2.7, "always use interface for public API’s definition when authoring a library or 3rd party ambient type definitions" 라고 언급