Skip to content

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/typescript

2.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={() => {&#125;&#125;      // 타입: 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를 활용한 멀티 페이지 앱 통합을 학습한다