2025년 3월
export 없이 모듈 사용하기
모듈의 구성 요소를 export없이 외부로 노출하는 방법 - feat. python
경계에 의한 복잡성
프로젝트를 진행하다보면, 모듈의 구성요소를 외부로 노출하고 싶어지는 때가 생깁니다. 우리는 그럴 때마다 export 키워드를 사용합니다. 하지만 아래와 같은 경우라면 어떨까요?
export const handler0 = defineHandler("handler0", {
match: () => { ... },
handle: () => { ... }
});
export const handler1 = defineHandler("handler1", {
match: () => { ... },
handle: () => { ... }
});
export const handler2 = defineHandler("handler2", {
match: () => { ... },
handle: () => { ... }
});a와 b가 독립적이라면 이와 같이 나누는 것은 아키텍쳐 관점에서 흔합니다. 하지만 이런 편의를 위해서 c · d 등의 파일을 추가한다면, handlers.ts에 boilerplate 코드가 쌓이게 됩니다.
또한 named export를 사용하면, 모든 handler의 이름을 다르게 정해야하므로 관리부담이 늘어나게 됩니다.
Python의 register 패턴
문제를 해결하기 위해, 파이썬의 독특한 register Decorator 패턴을 참고했습니다.
from typing import Callable
registry = dict[str, Callable]()
def register(fn: Callable):
registry[fn.__name__] = fn
return fn
파이썬에서는 위와 같이 register Decorator를 정의해서, 함수에 붙이는 것으로 registry에 해당 함수를 등록할 수 있도록하는 패턴이 있습니다. 이 패턴은 등록해야하는 함수가 매우 많거나, 서로 멀리 떨어져 있는 경우에 유용하게 쓰일 수 있습니다.
const registry = new Map<string, any>();
export const register = (name: string, fn: any) => {
registry.set(name, fn);
return fn;
};하지만 typescript에서 decorator는 python과 동일하게 작동하지 않기 때문에, HOF(High Order Function)을 사용하는 방법으로 구현할 수 있습니다.
Tree Shaking 방지
export 할 필요도 import 할 필요도 사라졌습니다. 하지만 현대의 bundler는 export import하지 않은 대상은 Tree Shaking 하여 bundle에서 제거해버리기 때문에 nodejs에서 사용하면 registry에 아무것도 등록되지 않는 문제가 발생합니다.
const registry = new Map<string, any>();
// ^^^^^^^^ 아무 것도 들어있지 않음
export const register = (name: string, fn: any) => {
registry.set(name, fn);
return fn;
};이 문제는 dynamic import를 사용하여 해결할 수 있습니다. bundler는 dynamic import하는 대상은 Tree Shaking 하지 않으므로, 안전하게 모든 내용을 가져올 수 있습니다.
async function loadHandlers() {
await import("./a_handlers");
await import("./b_handlers");
await import("./c_handlers");
await import("./d_handlers");
...
return registry;
}이제
await loadHandlers()마무리
초기에 생각했던 모든 문제점을 해결하지는 못했습니다. 특히 관리부담이 handler 각각에서 dynamic import로 옮겨간 점은, 이 방법이 완벽한 해결책이 될 수 없다는 점을 알 수 있습니다.
하지만 이 방법은 named export의 필요성을 줄이고, file의 존재여부를 통한 code gen 가능성을 높이는 긍정적인 면도 있습니다. 가능하다면 나중에는 bundler의 플러그인을 통해 자동으로
async function loadHandlers()