몽키 패치보다는 안전한 타입을 사용하기
JS는 이미 생성된 객체와 클래스에 임의의 속성을 추가할 수 있습니다.
이러한 패턴을 통해 window
나 document
에 값을 할당하여 전역 변수를 만드는데 사용할 수 있죠.
이렇듯 런타임 시점에서 사용되는 프로토타입이나 글로벌 객체 등에 직접 변경을 가하는 것을 몽키 패치라고 합니다.
그러나 이는 일반적으로 좋은 생각이 아닙니다.
- 서로 멀리 떨어진 부분들 간에 의존성이 생기고, 예상치 못한 사이드 이펙트를 유발합니다.
- 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
타입을 건드리지 않고 새로운 타입을 도입했기 때문에 앞선 방법에서의 모듈 영역의 문제를 해결할 수 있습니다.
이 경우 몽키 패치된 프로퍼티를 참조하는 경우에만 해당 단언을 사용하거나, 새로운 변수를 도입하면 됩니다.
하지만, 기본적으로 몽키 패치는 남용해서는 안 되며, 궁극적으로 더 잘 설계된 구조로 리팩토링하는 것이 올바른 방향입니다.