[SIL:#1] TypeScript - Delegation Pattern

·2021년 9월 5일
0
post-thumbnail

Motivation

프로젝트를 진행하면서 함수는 점점 많아지는데 함수 사용 시 전처리 혹은 후처리를 해야하는 경우도 함께 증가하는게 너무 골치아프다.

찾아보니 데코레이터를 사용하면 해당 문제를 해결할 수 있다고는 하는데 아직은 실험적 기능 단계에 지나지 않아 해당 방법을 차용할 수는 없다.

그리고 사용하는 김에 기왕이면 타입추론이 자동으로 적용되어서 코드 작성 시 편리함을 누릴 수 있었으면 했고, 때로는 여러 함수를 조합해서 사용할 수 있었으면 좋겠다는 바람도 있다.

Agenda

앞선 내용을 좀 더 구체화하여 정의해보자.

문제가 되는 코드

type Version = 'old' | 'new';

function A(version: Version) {
  const c = C(version);
  ...
};
  
function B(version: Version) {
  const c = C(version);
  ...
};
...

위와 같은 형태에서 A, B 함수는 모두 동일 인자를 받으며 해당 인자를 기반으로 실행하는 함수를 동일하게 호출하고 있는 것을 알 수 있다.

동일 패턴 함수 정의가 많아질수록 불필요하게 재생성되는 코드를 방지할 필요가 분명 있어보인다.

Implement

해당 방식을 호출자 입장에서 바라보면 다음과 같이 될 것이다.

// A: (version: Version): unknown
A('old');

첫번째 인자는 A, B함수 내에서 처음 실행하고자 하는 C함수의 인자를 그대로 받아내고 있는 것을 알 수 있다.

하여, 이런 특징을 간직한 형태의 delegate 함수를 작성하면 다음과 같이 표현할 수 있을것이다.

function delegate<T extends unknown[], U, V>(
  funcA: (...args: T) => U | Promise<U>,
  funcB: (...args: [U | Promise<U>]) => V | Promise<V>,
  ): (...args: T) => V | Promise<V>;

해당 함수는 선언만 된 상태이므로 구현부를 마저 작성하면

function delegate<T extends unknown[], U, V>(
  funcA: (...args: T) => U | Promise<U>,
  funcB: (...args: [U | Promise<U>]) => V | Promise<V>,
): (...args: T) => U | Promise<U> | V | Promise<V> {
  return (...args: Parameters<typeof funcA>) => funcB(funcA(...args));
}

정리하자면, funcA는 C함수를 넣고, funcB는 A혹은 B함수의 구현부를 넣으면 되는 형태이다. 따라서 실행부는 다음과 같다.

type Version = 'old' | 'new';

function C(version: Version) {
  ...
};

// 함수명이 대문자인 경우 클래스형태이지만 예외적으로 함수명을 맞추기 위해 동일 이름으로 작성한다.
const A = delegate(C, (c) => {
  ...
});
  
const B = delegate(C, (c) => {
  ...
});
  
// 실행
A('old');
B('new');

위의 경우는 function C의 고정형 타입뿐 아니라 함수형 자체를 입력받을 수 있는 형태이므로

function C(version: Version, other: any[]) {
  ...
}
  
  ...
  
 A('old', []);

위와 같은 형태로 C가 선언될 경우 A 함수를 호출하는데 필요로 하는 인자값도 자동으로 추가되며 delegate 함수 자체가 Generic으로 정의되어있어 타입을 자동으로 추론할 수 있게된다.

Review

해당 방법을 사용하는 경우 A or B함수 안에서 this를 쓰게되면 binding 문제 등이 발생할 수 있으므로 이를 보완하기 위해 다음과 같은 방법을 동원하여 문제를 해결할 수 있다.

구현부


// 원형함수를 입력받아, 해당 원형함수의 인자값을 필요로 하는 함수를 반환하는 delegate 명세
function delegate<T extends unknown[], U>(func: (...args: T) => U | Promise<U>): (...args: T) => U | Promise<U>;

// 원형함수와 실행함수를 입력받아, 해당 원형함수의 인자값을 필요로 하는 함수를 반환하고 그로부터 입력받은 인자값을 토대로 원형함수를 실행한 후 이 값을 실행함수의 인자로 넣어 수행하는 delegate 명세
function delegate<T extends unknown[], U, V>(
  funcA: (...args: T) => U | Promise<U>,
  funcB: (...args: [U | Promise<U>]) => V | Promise<V>,
): (...args: T) => V | Promise<V>;

// 위 두 함수 명세를 구현하는 overloading 구현체
function delegate<T extends unknown[], U, V>(
  funcA: (...args: T) => U | Promise<U>,
  funcB?: (...args: [U | Promise<U>]) => V | Promise<V>,
): (...args: T) => U | Promise<U> | V | Promise<V> {
  return (...args: Parameters<typeof funcA>) => (funcB ? funcB(funcA(...args)) : funcA(...args));
}

실행부

const C = delegate((version: Version) => ...));

const A = delegate(C, (c) => ...));
const B = delegate(C, (c) => ...));

A('old');
B('new');

해당 방식은 class 내에서도 사용이 가능하나 delegate 패턴 사용 시 eslint에서 no-invalid-this 경고가 발생할 수 있는 단점을 갖고있다.

profile
백(곰) 개발자

0개의 댓글