Item 24 일관성 있는 별칭 사용하기

Alias

const borough = { name: 'Brooklyn', location: [40.688, -73.979] }
const loc = borough.location

borough.location 배열에 loc이라는 별칭(alias)를 만들었다.

별칭을 변경하면 원래 속성값에서도 변경된다.

> loc[0] = 0
> borough.location
[0, -73.979]
  • 별칭을 남발해서 사용하면 제어 흐름을 분석하기 어렵다.

Alias and Redundant Codes

다각형을 나타내는 다음과 같은 자료구조가 있다고 가정한다.

interface Coordinate {
  x: number
  y: number
}

interface BoudingBox {
  x: [number, number]
  y: [number, number]
}

interface Polygon {
  exterior: Coordinate[]
  holes: Coordinate[][]
  bbox?: BoundingBox
}

어떤 속성이 다각형에 포함되는지 체크하는 함수를 만든다.

function isPointInPolygon(polygon: Polygon, pt: Coordinate) {
  if (polygon.bbox) {
    if (pt.x < polygon.bbox.x[0] || pt.x > polygon.bbox.x[1] ||
        pt.y < polygon.bbox.y[0] || pt.y > polygon.bbox.y[1]) {
      return false
    }
  }
  // ...
}

이 코드는 타입 체크를 포함해 잘 동작하지만 반복되는 부분이 존재한다. 특히 polygon.bbox는 3줄에 걸쳐 5번이나 등장한다. 임시 변수를 도입해 중복을 없애보자.

임시 변수 도입 - 별칭은 타입 체커에게 혼동을 야기한다

function isPointInPolygon(polygon: Polygon, pt: Coordinate) {
  const box = polygon.bbox
  if (polygon.box) {
    if (pt.x < box.x[0] || pt.x > box.x[1] ||
            // ~~~                ~~~  객체가 'undefined'일 수 있습니다.
        pt.y < box.y[0] || pt.y > box.y[1]) {
            // ~~~                ~~~  객체가 'undefined'일 수 있습니다.
      return false
    }
  }
  // ...
}

코드는 동작하지만 편집기에서 오류로 표시된다. 그 이유는 별칭을 만듦으로써 제어 흐름 분석을 방해했기 때문이다.

function isPointInPolygon(polygon: Polygon, pt: Coordinate) {
  const box = polygon.bbox
  if (box) { // 속성 체크에 box를 사용하도록 변경
    if (pt.x < box.x[0] || pt.x > box.x[1] ||
        pt.y < box.y[0] || pt.y > box.y[1]) {
      return false
    }
  }
  // ...
}

위처럼 "별칭은 일관성 있게 사용한다"는 규칙을 지켜 속성 체크에 box를 사용하면 오류를 없앨 수 있다.

객체 비구조화

하지만 코드를 읽는 사람에게는 문제가 남아있는데, box와 bbox는 같은 값인데 다른 이름을 사용한 것이다.

객체 비구조화(Destructuing Assignment)를 사용하면 간결한 문법으로 일관된 이름을 사용할 수 있다.

function isPointInPolygon(polygon: Polygon, pt: Coordinate) {
  const { bbox } = polygon
  if (bbox) { // 속성 체크에 box를 사용하도록 변경
    if (pt.x < bbox.x[0] || pt.x > bbox.x[1] ||
        pt.y < bbox.y[0] || pt.y > bbox.y[1]) {
      return false
    }
  }
  // ...
}

객체 비구조화를 사용할 땐 다음과 같은 두 가지를 주의하자.

  1. 전체 bbox 속성이 아니라 xy가 선택적 속성일 경우엔 속성 체크가 더 필요하다. 따라서 타입의 경계에 null 값을 추가하는 것이 좋다. (Item 31)

  2. bbox에는 선택적 속성이 적합했지만 holes는 그렇지 않다. holes가 선택적이라면, 값이 없거나 빈 배열([])이었을 것이다. 차이가 없는데 이름을 구별한 것이다. 빈 배열은 'holes 없음'을 나타내는 좋은 방법이다.

별칭은 타입 체커뿐만 아니라 런타임에 혼동을 야기한다

const { bbox } = polygon
if (!bbox) {
  calculatePolygonBbox(polygon) // polygon.bbox가 채워진다 - bbox는 not null
  // 이제 polygon.bbox와 bbox는 다른 값을 참조한다.
}

제어 흐름 분석은 지역변수에는 꽤나 잘 동작하지만, 객체 속성에서는 주의해야 한다.

function fn(p: Polygon) { /* ... */ }

polygon.bbox   // 타입이 BoudingBox | undefined
if (polygon.bbox) {
  polygon.bbox // 타입이 BoudingBox
  fn(polygon)
  polygon.bbox // 타입이 BoudingBox
}

fn(polygon) 호출은 polygon.bbox를 제거할 가능성이 있으므로 타입을 BoundingBox | undefined로 되돌리는 것이 안전할 것이다. 그러나 함수를 호출할 때마다 속성 체크를 반복해야 하기 때문에 좋지 않다.

그래서 타입스크립트는 함수가 타입 정제를 무효화하지 않는다고 가정한다. 그러나 실제로는 무효화될 가능성이 있다.

polygon.bbox로 사용하는 대신 bbox 지역 변수로 뽑아내서 사용하면 bbox의 타입은 정확히 유지되지만, polygon.bbox의 값과 같게 유지되지 않을 수 있다.

Summary

  • 별칭은 타입스크립트가 타입을 좁히는 것을 방해한다. 따라서 변수에 별칭을 사용할 때는 일관되게 사용해야 한다.

  • 비구조화 문법을 사용해 일관된 이름을 사용하는 것이 좋다.

  • 함수 호출이 객체 속성의 타입 정제를 무효화할 수 있다는 점을 주의해야 한다. 속성보다 지역 변수를 사용하면 타입 정제를 믿을 수 있다.

Last updated