디스패치 테이블과 TS

2024. 8. 6.

디스패치 테이블

기본적으로 다중 if ... elseif ... elseswitch는 지양하는 편이다. 가독성도 좋지 않고 분기를 위한 코드가 많다고 느껴지기 때문이다.

계산기를 위한 분기문을 작성하는 예시를 통해 확인해 보자.

//Bad
 
type Operater = '+' | '-' | '*' | '/';
 
const calculate = (operator: Operater, a: number, b: number) => {
  if (operator === '+') return a + b;
  else if (operator === '-') return a - b;
  else if (operator === '*') return a * b;
  else if (operator === '/') return a / b;
  else throw new Error('Invalid operator');
};
 
const calculate = (operator: Operater, a: number, b: number) => {
  switch (operator) {
    case '+':
      return a + b;
    case '-':
      return a - b;
    case '*':
      return a * b;
    case '/':
      return a / b;
    default:
      throw new Error('Invalid operator');
  }
};

계산기 함수가 매우 간단한 함수지만 예약어(else, case, break)들 때문에 많은 코드가 필요하다고 느껴진다.

이때 디스패치 테이블을 사용하면, 이런 예약어를 줄이고 가독성을 높일 수 있다.

const CALCULATE_DISPATH_TABLE = {
  '+': (a: number, b: number) => a + b,
  '-': (a: number, b: number) => a - b,
  '*': (a: number, b: number) => a * b,
  '/': (a: number, b: number) => a / b
};
 
type Operater = keyof typeof CALCULATE_DISPATH_TABLE;
 
const calculate = (operator: Operater, a: number, b: number) => CALCULATE_DISPATH_TABLE[operator](a, b);

내가 생각하는 디스패치테이블이 else if, switch 보다 좋다고 생각하는 이유는 다음과 같다.

  • 조건이 명시적이다. 객체의 key값이 조건이므로 해당 코드가 해야하는 일을 명시적으로 나타낸다.
  • 테이블을 사용하는 함수 외부에 위치할 수 있다.
  • 타입을 위한 추가적인 코드를 작성하지 않아도 된다.

다만 TypeScript에서 디스패치 테이블을 사용하다보면 인자 타입을 맞춰주지 못하거나 추론할 때 타입에러를 많이 만들 수 있다.

이를 위해 createDispatch라는 함수를 만들었다.

type AnyFunction = (...args: any[]) => any;
 
type FunctionMap = {
  [key: string]: AnyFunction;
};
 
function createDispatch<T extends FunctionMap>(obj: T) {
  const execute = <K extends keyof T>(key: K, ...args: Parameters<T[K]>): ReturnType<T[K]> => {
    const func = obj[key];
    return func(...args);
  };
 
  const isKey = (key: any): key is keyof T => {
    return key in obj;
  };
 
  return [execute, isKey] as const;
}

createDispath 함수는 두 가지 함수를 담은 튜플을 반환한다.

  • execute : 디스패치 테이블 실행 함수이다. 첫번째 인자로 디스패치 테이블의 키값과 키에 해당하는 함수의 인자를 인자로 받는다.
  • isKey : 해당 값이 디스패치 테이블의 값인지 체크하고 이후 타입가드를 수행한다.

createDispath 함수를 사용하면 타입스크립트를 통해 타입 힌트도 받을 수 있고 isKey를 통해 타입 가드도 받을 수 있다.

const CALCULATE_DISPATH_TABLE = {
  '+': (a: number, b: number) => a + b,
  '-': (a: number, b: number) => a - b,
  '*': (a: number, b: number) => a * b,
  '/': (a: number, b: number) => a / b
};
 
const [calculate, isOperator] = createDispatch(CALCULATE_DISPATH_TABLE);
 
const cal = (key: string, arg: { a: number; b: number }) => {
  if (isOperator(key)) {
    return calculate(key, arg.a, arg.b);
  }
};

dispatch.gif