typescript

Typescript 태그된 유니온 적용에 대하여

Takhyun Kim 2022. 3. 20. 15:47

1. 태그된 유니온?

이펙티브 타입스크립트 책을 공부하면서, item 32 유니온의 인터페이스보다는 인터페이스의 유니온을 사용하기 파트에서 볼 수 있습니다.

아래와 같은 인터페이스가 있습니다.

interface Layer {
  layout: FillLayout | LineLayout | PointLayout;
  paint: FillPaint | LinePaint | PointPaint;
}

layout 이 LineLayout 이면서 paint 속성이 FillPaint 타입인 것은 다소 잘못된 설계 방식을 의미합니다.

이러한 조합을 라이브러리에서 허용한다면, 분명 오류가 발생할 여지를 제공하는 것이며, 이는 인터페이스를 다루는데 있어
많은 어려움을 야기시킵니다. 이를 개선하기 위해선 아래와 같이 각각 타입의 계층을 분리된 인터페이스로 둬야 합니다.

 

interface FillLayer {
  layout: FillLayout;
  paint: FillPaint;
}

interface LineLayer {
  layout: LineLayout;
  paint: LinePaint;
}

interface PointLayer {
  layout: PointLayout;
  paint: PointPaint;
}

type Layer = FillLayer | LineLayer | PointLayer;

위와 같이 Layer 를 정의할 경우 layout, paint 속성이 잘못된 조합으로 섞여 사용하는 경우를 방지할 수 있습니다.

위 패턴에서 각 interface 별 type 이라는 속성을 추가하여 일종의 인터페이스 "태그" 를 추가해보겠습니다.

interface FillLayer {
  type: "fill";
  layout: FillLayout;
  paint: FillPaint;
}

type 속성에 각 layer 에 맞는 태그를 추가하여, 런타임에 어떤 타입의 Layer 가 사용되는지 판단하는데 쓰입니다.

 

2. 태그된 유니온 적용

1번에서 설명드린 태그된 유니온을 회사 내 자체 라이브러리 작업을 하면서, 위 태그된 유니온을 적용해보았습니다.

우선, 아래 변경하기 전 interface 를 보겠습니다.

interface Annotation {
  /** Serves as id by contour */
  id: number

  /**
   * The method used is different depending on the mode
   * - (mode: polygon) = [[x, y], [x, y], [x, y]...]
   * - (mode: point) = [[x, y]]
   */
  points: Point[]

  /** If label is present, it will output instead of id */
  label?: string

  /** polygon pabel position = [x, y] */
  labelPosition?: Point

  /**
   * The data-attribute is added to the svg element
   * You can implement functions such as css styling based on the attributes
   */
  dataAttrs?: { [attr: string]: string }

  lineWidth?: number
}

Annotation 이라는 inteface 는 기본적으로 polygon, circle, point 와 같은 drawing 기능을 제공하기 위한 각 mode 별 interface 를 의미합니다. points 라는 속성은 Point[] 의 형태입니다. Point type 은 [number, number] 를 의미하며 이는 x, y 좌표값을 의미합니다.

주석을 보시면 아시겠지만 각 모드 별로 어느정도 정형화된 형식은 있지만 이를 모두 포괄적으로 설계하는 방향으로 Point[] 로 잡았습니다.

 

위와 같은 설계 방식은 각 mode 별 목적성이 정확하지 않을 경우, 초기 개발 속도 및 기본 기능을 구현하는데 있어서 이점으로 작용했습니다. polygon, line, freeLine 등 기능을 구현할 때 굉장히 빠르게 개발할 수 있었지만, circle mode 를 진행하고 각 mode 별 기능을 구체화 하는데 있어서 조금씩 불편한 부분이 생겼고, 이는 불필요한 validation 처리 타입 단언(as ~~) 과 같은 다소 좋지 않는 코드 퀄리티를 발생 시켰습니다.

 

circle mode 기능 구현을 위해 radius, center 와 같은 속성을 Annotation interface 에 optional 로 추가했고,
이는 태그된 유니온에 대한 설명 중, 각 용도에 맞지 않는 속성을 함께 사용하는 문제를 발생시켰습니다.
예를 들면 polygon mode 를 설정했지만 동시에 radius, center 와 같은 속성은 optional 로 제공해주기에 사용이 가능한 케이스를 발생시킵니다. 물론 코드 내에서도 polygon 실제로 해당 값을 사용하지 않고, 버그를 발생시키기 않겠지만 이는 설계적으로 잘못되었다는 것을 의미한다고 느꼈습니다. 그래서 각 mode 별 목적성, 추후 방향성이 정해지고 위와 같은 코드 퀄리티를 다운시키는 설계 방식을 리팩토링 했습니다.

 

리팩토링 방식의 가장 베이스는 태그된 유니온 방식이며, 각 모드 별 정형화된 interface 를 추가, 이를 유니온으로 묶었습니다.

type LineHeadMode = 'normal' | 'arrow'
type AnnotationMode = 'line' | 'freeLine' | 'polygon' | 'circle'

interface AnnotationBase {
  /** Serves as id by contour */
  id: number

  type: AnnotationMode

  /** If label is present, it will output instead of id */
  label?: string

  /** polygon pabel position = [x, y] */
  labelPosition?: Point

  /**
   * The data-attribute is added to the svg element
   * You can implement functions such as css styling based on the attributes
   */
  dataAttrs?: { [attr: string]: string }

  lineWidth?: number
}

interface LineAnnotation extends AnnotationBase {
  type: 'line'
  points: [Point, Point]

  headPoints?: Point[]
}

interface FreeLineAnnotation extends AnnotationBase {
  type: 'freeLine'
  points: Point[]
}

interface PolygonAnnotation extends AnnotationBase {
  type: 'polygon'
  points: Point[]
}

interface CircleAnnotation extends AnnotationBase {
  type: 'circle'
  center: Point
  radius: number
}

type Annotation = PolygonAnnotation | FreeLineAnnotation | LineAnnotation | CircleAnnotation

 

 

기존 Annotation 은 일부 속성을 변경해서 AnnotationBase interface 로 명칭을 변경했습니다. 각 mode 별로 공통적으로 가지고 있는 속성만을 추려냈습니다. 더불어 type 에는 AnnotationMode 라는 type 을 통해 정확한 타입을 정의해주었습니다.

 

이 후 각 mode 별로 AnnotationBase 를 extends 한 interface 를 추가하였으며, 1번 태그된 유니온에서 확인하셨던 방식과 같이
type(태그)를 추가, points 속성에는 각 mode 별 정확한 type 을 제공, 해당 타입에서만 필요한 속성이 있을 경우 해당 mode 의 interface 에 필요 속성을 추가하는 방식으로 설계하였습니다. 그리고 이 mode interface 를 유니온으로 묶어 Annotation type 을 정의했습니다.

 

이를 통해 기존 circle mode 구현을 위해 optional 로 center, radius 을 제공하여 polygon mode 에서 해당 속성들을 사용할 수 있는 케이스를 제거할 수 있었습니다. type 이 circle 일 경우에만 center, radius 속성을 사용할 수 있습니다.

 

더불어 각 interface 태그를 통해 조건문으로 타입 좁히기를 할 수 있고, 이를 통해 불필요한 타입 단언문을 삭제할 수 있었습니다. 

이전에는 타입 단언을 통해 이건 Point[] type, 저건 [Point, Point] type 을 지정해주었는데 이젠 type 을 통해 분기 처리,
타입 좁히기를 통해 해당 points 필드가 어떤 mode 의 interface 인지를 알 수 있기에 validation 처리도 많이 간소화 시킬 수 있었습니다.

 

3. 후기

이펙티브 타입스크립트 책을 읽으면서 처음으로 실무에 이론을 적용해본 케이스였고, 이번 타입 리팩토링을 통해 정말 많은 부분들을 개선하고 배울 수 있었습니다. 해당 이론들의 장점을 글로만 읽고, 실제 적용해보기 전에는 해당 방식이 가져다주는 이점을 100% 이해하기 어려웠는데, 분명 불편한 부분들이 있었고 책에서 언급해주었던 케이스와 매우 유사하여 이를 기반으로 리팩토링 후, 불편했던 부분들이 굉장히 많이 해소됨을 느낄 수 있었던 시간이였습니다.