본문 바로가기

공부

[이펙티브 타입스크립트] 4장 타입 설계

[ 아이템 36 ] 해당 분야의 용어로 타입 이름 짓기

엄선된 타입, 속성, 변수의 이름은 의도를 명확히 하고 코드와 타입의 추상화 수준을 높여 줍니다. 잘못 선택한 타입 이름은 코드의 의도를 

왜곡하고 잘못된 개념을 심어 주게 됩니다. 

동물들의 데이터베이스를 구축한다고 가정해 보겠습니다.

interface Animal {
  name: string
  endangered: boolean
  habitat: string
}

const leopard: Animal = {
  name: 'Snow Leopard',
  endangered: false,
  habitat: 'tundra',
}

이 코드에는 네 가지 문제가 있습니다. 

  • name으로 동물의 학명인지 일반적인 명청인지 알 수 없음.
  • endagered 속성 의도가 멸종 위기만 인지 멸종도 포함 인지 명확하지가 않음.
  • habitat 서식지는 너무 광범위함.
  • 객체의 변수명이 leopard 이지만 name이 다른 의도로 사용된 것인지 불분명함.

위 예제의 문제를 해결하기 위해서는 작성한 사람을 찾아서 의도를 물어봐야 하지만, 작성한 사람이 기억하지 못하거나 회사에 없는 경우 최악의 상황이 펼쳐질 수 있습니다. 

interface Animal {
  commonName: string
  genus: string
  species: string
  status: ConservationStatus
  climates: KoppenClimate[]
}
type ConservationStatus = 'EX' | 'EW' | 'CR' | 'EN' | 'VU' | 'NT' | 'LC'
type KoppenClimate = | 'Af' | 'Am' | 'As' | 'Aw' | 'BSh' | 'BSk' | 'BWh' | 'BWk'
  | 'Cfa' | 'Cfb' | 'Cfc' | 'Csa' | 'Csb' | 'Csc' | 'Cwa' | 'Cwb' | 'Cwc' | 'Dfa'
  | 'Dfb' | 'Dfc' | 'Dfd' | 'Dsa' | 'Dsb' | 'Dsc' | 'Dwa' | 'Dwb' | 'Dwc' | 'Dwd'
  | 'EF'  | 'ET'
  
const snowLeopard: Animal = {
  commonName: 'Snow Leopard',
  genus: 'Panthera',
  species: 'Uncia',
  status: 'VU', //취약종(vulnerable)
  climates: ['ET', 'EF', 'Dfd'], // 고산대(alpine) 또는 아고산대(subalpine)
}

이 코드에서는 다음 세 가지를 개선했습니다. 

  • namecommonName, genus, species 등 더 구체적인 용어로 대체
  • endangered는 동물 보호 등급에 대한 IUCN의 표준 분류 체계 타입의 status로 변경
  • habitat은 기후를 뜻하는 climates로 변경 

데이터를 훨씬 명확하게 표현하고 있으며 정보를 찾기 위해 사람에 의존할 필요가 없습니다. 

코드를 표현하고자 하는 모든 분야에는 주제를 설명하기 위한 전문 용어들이 있습니다. 자체적으로 용어를 만들어 내려고 하지 말고, 수년, 수십 년, 수 세기에 걸쳐 다음 예제 온 해당 분야에 이미 존재하고 사용 중인 용어를 사용해야 합니다. 

타입, 속성, 변수에 이름을 붙일 때 명심해야 할 세 가지 규칙이 있습니다. 

  1. 동일한 의미를 나타낼 때는 같은 용어를 사용해야 합니다. 정말 의미적으로 구분이 되어야 하는 경우에만 다른 용어를 사용. 
  2. data, info, thing, item, object, entity 같은 모호하고 의미 없는 이름은 피해야 합니다. 
  3. 이름을 지을 때는 포함된 내용이나 계산 방식이 아니라 데이터 자체가 무엇인지를 고려해야 합니다. 

[ 아이템 37 ] 공식 명칭에는 상표를 붙이기

interface Vector2D {
  x: number
  y: number
}
function calculateNorm(p: Vector2D) {
  return Math.sqrt(p.x * p.x + p.y * p.y)
}

calculateNorm({ x: 3, y: 4 }) // 정상, 결과는 5
const vec3D = { x: 3, y: 4, z: 1 }
calculateNorm(vec3D) // 정상 결과는 동일하게 5

이 코드는 구조적 타이핑 관점에서는 문제가 없기는 하지만, 수학적으로 따지면 2차원 백터를 사용해야 합니다. 

calculateNorm 함수가 3차원 백터를 허용하지 않게 하기 위해서는 '상표(brand)'를 붙이면 됩니다.

interface Vector2D {
  _brand: '2d'
  x: number
  y: number
}
function vec2D(x: number, y: number): Vector2D {
  return { x, y, _brand: '2d' }
}
function calculateNorm(p: Vector2D) {
  return Math.sqrt(p.x * p.x + p.y * p.y) 
}

calculateNorm(vec2D(3, 4)) // OK, returns 5
const vec3D = { x: 3, y: 4, z: 1 }
calculateNorm(vec3D)
              ~~~~~ '_brand' 속성이 ...형식에 없습니다.

상표(_brand)를 사용해서 calculateNorm 함수가 Vector2D 타입만 받는 것을 보장합니다. 그러나 vec3D 값에 _brand: '2d' 라고 추가하는 것 같은 악의적인 사용은 막을 수 없습니다. 

 

상표 기법은 타입 시스템 내에서 표현할 수 없는 수많은 속성들을 모델링하는 데 사용되기도 합니다. 

예를 들어, 목록에서 한 요소를 찾기 위해 이진 검색을 하는 경우를 보겠습니다. 

function binarySearch<T>(xs: T[], x: T): boolean {
  let low = 0,
    high = xs.length - 1
  while (high >= low) {
    const mid = low + Math.floor((high - low) / 2)
    const v = xs[mid]
    if (v === x) return true
    ;[low, high] = x > v ? [mid + 1, high] : [low, mid - 1]
  }
  return false
}

이진 검색은 이미 정렬된 상태를 가정하기 때문에 목록이 정렬되어 있지 않다면 잘못된 결과가 나옵니다. 

목록이 정렬되어 있다는 의도를 상표 기법을 사용해서 표현해 보도록 하겠습니다. 

type SortedList<T> = T[] & { _brand: 'sorted' }

function isSorted<T>(xs: T[]): xs is SortedList<T> {
  for (let i = 1; i < xs.length; i++) {
    if (xs[i] > xs[i - 1]) {
      return false
    }
  }
  return true
}

function binarySearch<T>(xs: SortedList<T>, x: T): boolean {
  return true
}

binarySearch를 호출하려면, 정렬되었다는 상표가 붙은 SortedList 타입의 값을 사용하거나 isSorted를 호출하여 정렬되었음을 증명해야 합니다.