몽키 패치보다는 안전한 타입을 사용하기

JS는 이미 생성된 객체와 클래스에 임의의 속성을 추가할 수 있습니다. 이러한 패턴을 통해 windowdocument에 값을 할당하여 전역 변수를 만드는데 사용할 수 있죠. 이렇듯 런타임 시점에서 사용되는 프로토타입이나 글로벌 객체 등에 직접 변경을 가하는 것을 몽키 패치라고 합니다. 그러나 이는 일반적으로 좋은 생각이 아닙니다.

  • 서로 멀리 떨어진 부분들 간에 의존성이 생기고, 예상치 못한 사이드 이펙트를 유발합니다.
  • TS의 경우, 기본적으로 이렇게 임의로 추가된 속성에 대해서는 알 방법이 없습니다.

이 중 두번째 문제의 경우, 해결하기 위한 가장 쉬운 방법은 해당 객체를 any로 두는 것입니다. 하지만, 이 때는 타입 안정성을 상실하고, 언어 서비스를 사용할 수 없게 됩니다.

(document as any).monky = 'Tamarin';
(document as any).monkey = /Tamarin/;

가장 좋은 해결책은 애초에 글로벌 객체로부터 데이터를 분리하는 것입니다. 하지만, 분리할 수 없는 상황인 경우 두 가지 차선책이 존재합니다.

1. 인터페이스의 보강(augmentation) 기능을 사용

export {};

// 모듈 관점에서 제대로 동작하려면 `global` 선언이 필요합니다.
declare global {
  interface Document {
    monkey: string;
  }
}

document.monkey = 'Tamarin';  // OK

이 방법은 any보다 타입 안전성 측면에서도 더 안전하고, 에디터 상에서 제대로 된 피드백도 전달 받을 수 있습니다. 하지만, 보강은 전역적으로 적용되기 떄문에, 코드의 다른 부분이나 라이브러리로부터 분리할 수 없다는 단점이 있습니다. 또, 런타임 시점에서 이러한 보강을 적용할 방법이 없습니다. (런타임 도중에 프로퍼티가 추가되는 경우) 이러한 문제 때문에 프로퍼티를 optional하게 두게 되는데, 이 경우 더 정확할 수는 있으나 다루기에는 더 어려워집니다.

2. 더 구체적인 타입 단언문을 사용

interface MonkeyDocument extends Document {
  monkey: string;
}

(document as MonkeyDocument).monkey = 'Macaque';

이 방법은 직접적으로 Document 타입을 건드리지 않고 새로운 타입을 도입했기 때문에 앞선 방법에서의 모듈 영역의 문제를 해결할 수 있습니다. 이 경우 몽키 패치된 프로퍼티를 참조하는 경우에만 해당 단언을 사용하거나, 새로운 변수를 도입하면 됩니다.

하지만, 기본적으로 몽키 패치는 남용해서는 안 되며, 궁극적으로 더 잘 설계된 구조로 리팩토링하는 것이 올바른 방향입니다.