테마
TypeScript 연동
Module Federation으로 다른 서버의 코드를 런타임에 가져올 때, TypeScript 타입 정의가 없으면 개발 생산성이 크게 저하된다. Federated Types 플러그인과 tsconfig path 매핑 두 가지 전략으로 타입 안전성을 확보할 수 있다.
학습 목표
- Module Federation에서 리모트 컴포넌트의 타입 정의가 필요한 이유를 이해한다
- @module-federation/typescript 플러그인의 동작 원리와 한계를 파악한다
- tsconfig의 paths를 이용한 모노레포 내 타입 참조 방법을 구현할 수 있다
- 두 가지 타입 공유 전략의 장단점을 비교하여 프로젝트에 적합한 방식을 선택할 수 있다
1. 타입 정의 없는 리모트 모듈의 문제
Module Federation으로 리모트 컴포넌트를 import하면, TypeScript는 해당 모듈의 존재를 알 수 없어 타입 에러가 발생한다.
tsx
// @ts-ignore 없이는 타입 에러 발생
const Button = React.lazy(
() => import("componentApp/Button")
// ^^^^^^^^^^^^^^^^^^^^
// Cannot find module 'componentApp/Button'
);@ts-ignore로 회피할 때의 문제점:
tsx
// @ts-ignore 사용 - 동작은 하지만 타입 안전성 상실
// @ts-ignore
const Button = React.lazy(() => import("componentApp/Button"));
// Button의 타입이 React.ComponentType<any>가 됨
// props 자동완성, 타입 검사 모두 불가능
<Button unknownProp="값" /> // 에러를 잡지 못함2. 전략 1: @module-federation/typescript 플러그인
이 플러그인은 Remote 앱이 빌드될 때 타입 선언 파일(.d.ts)을 자동 생성하여 서버에 배포하고, Host 앱이 실행될 때 이 타입 파일을 다운로드하여 로컬에 저장하는 방식이다.
2.1 설치 및 설정
bash
# Remote와 Host 양쪽에 설치
pnpm add -D @module-federation/typescript2.2 Remote 측 설정
js
// apps/component-app/webpack.config.js
const { FederatedTypesPlugin } = require("@module-federation/typescript");
const federationConfig = {
name: "componentApp",
filename: "remoteEntry.js",
exposes: {
"./Button": "./src/components/Button",
},
shared: {
react: { singleton: true },
"react-dom": { singleton: true },
},
};
module.exports = {
plugins: [
new ModuleFederationPlugin(federationConfig),
new FederatedTypesPlugin({
federationConfig,
}),
],
};빌드 후 dist 폴더 구조:
dist/
main.js
remoteEntry.js
@mf-types/ # 자동 생성된 타입 폴더
componentApp/
compiled-types/
components/
Button.d.ts # Button 컴포넌트 타입2.3 Host 측 설정
js
// apps/main-app/webpack.config.js
const { FederatedTypesPlugin } = require("@module-federation/typescript");
module.exports = {
plugins: [
new ModuleFederationPlugin(federationConfig),
new FederatedTypesPlugin({ federationConfig }),
],
};json
// apps/main-app/tsconfig.json
{
"compilerOptions": {
"paths": {
"*": ["./@mf-types/*"]
}
}
}2.4 Federated Types의 한계
| 한계점 | 설명 |
|---|---|
| Remote 빌드 필수 | Remote를 빌드하여 서버에 올려야 타입 파일 생성 |
| Host 실행 필수 | Host를 실행해야 타입 파일 다운로드 |
| Dev 모드 제한 | Remote의 dev 모드에서는 타입 파일 미생성 |
| 네트워크 의존 | Remote 서버가 다운되면 타입 갱신 불가 |
| 별도 레포에 유효 | 모노레포에서는 과도한 방식 |
3. 전략 2: tsconfig paths 매핑 (모노레포 권장)
모노레포 환경에서는 같은 코드베이스 안에 모든 마이크로앱이 있으므로, tsconfig의 paths를 이용하여 직접 타입을 참조할 수 있다. 런타임에는 Webpack이 Module Federation으로 처리하고, 타입 수준에서만 소스 파일을 참조하는 방식이다.
3.1 tsconfig.json 설정
json
// apps/main-app/tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"componentApp/Button": [
"../component-app/src/components/Button"
],
"componentApp/Header": [
"../component-app/src/components/Header"
]
}
}
}3.2 코드에서의 사용
tsx
// apps/main-app/src/App.tsx
import React, { Suspense } from "react";
// TypeScript: ../component-app/src/components/Button 의 타입 참조
// Runtime: Webpack이 componentApp의 remoteEntry.js에서 로드
const Button = React.lazy(() => import("componentApp/Button"));
function App() {
return (
<Suspense fallback={<div>로딩 중...</div>}>
{/* props 자동완성이 정상 동작 */}
<Button
type="primary" // 자동완성: "primary" | "warning"
onClick={() => {}} // 타입: React.MouseEventHandler
>
확인
</Button>
</Suspense>
);
}3.3 동작 원리 분석
| 시점 | 처리 주체 | 동작 |
|---|---|---|
| 코드 작성 시 | TypeScript (tsconfig paths) | 소스 파일의 타입을 직접 참조하여 자동완성/타입검사 |
| 빌드 시 | Webpack (Module Federation) | import 경로를 Remote로 해석하여 런타임 로딩 코드 생성 |
| 실행 시 | 브라우저 (Webpack Runtime) | remoteEntry.js를 통해 다른 서버에서 모듈 동적 로드 |
paths 매핑은 코드를 실제로 가져오는 것이 아니다. TypeScript 레벨에서만 타입을 참조할 뿐, 런타임에는 Webpack의 Module Federation이 네트워크를 통해 모듈을 로드한다.
4. 두 전략 비교
| 비교 항목 | Federated Types 플러그인 | tsconfig paths 매핑 |
|---|---|---|
| 적합 환경 | 별도 레포지토리 | 모노레포 |
| 설정 복잡도 | 높음 (양쪽 플러그인 설치) | 낮음 (tsconfig만 수정) |
| 타입 갱신 | Remote 빌드 + Host 실행 필요 | 소스 변경 시 즉시 반영 |
| 네트워크 의존 | 있음 (서버에서 타입 다운로드) | 없음 (로컬 참조) |
| 개발 편의성 | 중간 (빌드 절차 필요) | 높음 (즉시 반영) |
| 프로덕션 실무 | 별도 레포에서 사용 | 모노레포에서 권장 |
5. 타입 선언 파일을 직접 작성하는 방법
두 전략이 모두 적합하지 않은 경우, 수동으로 타입 선언 파일을 작성할 수도 있다.
ts
// apps/main-app/src/@types/componentApp.d.ts
declare module "componentApp/Button" {
import React from "react";
interface ButtonProps {
type?: "primary" | "warning";
onClick?: React.MouseEventHandler<HTMLButtonElement>;
children: React.ReactNode;
}
const Button: React.FC<ButtonProps>;
export default Button;
}
declare module "componentApp/Header" {
import React from "react";
interface HeaderProps {
title: string;
}
const Header: React.FC<HeaderProps>;
export default Header;
}| 방법 | 장점 | 단점 |
|---|---|---|
| 수동 .d.ts | 어떤 환경에서든 사용 가능 | 리모트 변경 시 수동 동기화 필요 |
| Federated Types | 자동 생성, 동기화 | 빌드/실행 절차 복잡 |
| tsconfig paths | 즉시 반영, 간단 | 모노레포에서만 사용 가능 |
핵심 정리
| 항목 | 내용 |
|---|---|
| 타입 문제 | 리모트 모듈의 타입을 TypeScript가 알 수 없어 에러 발생 |
| @ts-ignore | 단기 해결책이나 타입 안전성 완전 상실, 비권장 |
| Federated Types | 빌드 시 .d.ts 생성하여 서버에 배포, 별도 레포에 적합 |
| tsconfig paths | 모노레포에서 소스 파일 직접 참조, 프로덕션 실무 권장 |
| 핵심 원리 | TypeScript 레벨의 타입 참조와 런타임의 모듈 로딩은 별개 |
| 수동 .d.ts | 환경 제약이 있을 때의 대안, 수동 동기화 부담 |
다음 단계
- 동적 로딩과 라우팅에서 리모트 서버 URL을 런타임에 동적으로 지정하는 방법과 React Router를 활용한 멀티 페이지 앱 통합을 학습한다