Logo
Bason

Microfrontends with Turborepo: Build to Runtime Sharing

Turborepo 사용자를 위한 마이크로 프론트엔드 완전 정복: 빌드타임 공유에서 런타임 공유로

이성훈
이성훈May 13, 2026 · 0 views

Turborepo 사용자를 위한 마이크로 프론트엔드 완전 정복: 빌드타임 공유에서 런타임 공유로

모노레포를 운영하다 보면, 어느 순간 조용히 벽에 부딪히는 경험을 하게 됩니다.

처음엔 모든 게 순조롭습니다. Turborepo는 빠르게 돌아가고, 공유 패키지는 깔끔하게 버전 관리되고, 빌드 캐시는 제대로 작동합니다. 그런데 제품이 커지고 팀이 늘어나면서, 그 아름다웠던 모노레포가 슬슬 발목을 잡기 시작하죠.

공유 컴포넌트 라이브러리 하나 고쳤을 뿐인데 배포가 줄줄이 엮이고, 누가 이 패키지 소유자냐는 논쟁이 시작되고, 한 팀의 변경이 다른 팀의 릴리즈를 막는 상황이 반복됩니다.

이 글은 바로 그 지점에서 시작합니다. Turborepo를 실제로 사용하는 개발자 입장에서, 마이크로 프론트엔드(MFE) 아키텍처가 왜 현실적인 선택지가 되는지, 그리고 어떻게 접근해야 하는지를 구체적으로 풀어봅니다. 이론이 아니라, 실제로 마주치는 문제들을 중심으로요.


Turborepo가 잘하는 것, 그리고 한계가 오는 순간

런타임 공유의 장점을 이해하기 전에, 먼저 빌드타임 패키지 공유가 왜 그토록 매력적인지 — 그리고 왜 한계가 오는지 — 를 짚고 넘어가야 합니다.

Turborepo의 핵심 강점

Turborepo는 JavaScript/TypeScript 모노레포를 위한 고성능 빌드 시스템입니다. 핵심 가치는 단순하지만 강력합니다. 여러 패키지를 하나의 저장소에서 관리하면서, 지능적인 캐싱과 병렬 태스크 실행, 명확한 의존성 그래프를 제공하는 것이죠.

실무적으로는 이런 게 가능해집니다:

  • ui-components 패키지를 다섯 개의 앱에서 코드 중복 없이 공유
  • turbo build 한 번으로 변경되지 않은 패키지는 자동으로 스킵
  • 디자인 시스템, 유틸 함수, API 클라이언트를 전체 제품 라인업에서 일관되게 유지

소규모에서 중규모 팀에게는 이 조합이 정말 강력합니다. 런타임 복잡성 없이 코드 재사용을 달성할 수 있고, 개발자 경험도 훌륭합니다.

균열이 시작되는 지점

문제는 빌드타임 공유의 본질적인 특성에 있습니다. 공유 의존성이 변경될 때마다 배포가 함께 엮입니다.

버튼 컴포넌트 하나를 수정했나요? 그 패키지를 사용하는 모든 앱이 다시 빌드되고 배포되어야 합니다. 조직이 커질수록 이 구조는 실질적인 병목이 됩니다:

  • 배포 결합(Deployment Coupling): 팀들이 독립적으로 배포할 수 없습니다. 공유 패키지 하나의 변경이 여러 제품 영역의 릴리즈를 막습니다.
  • 번들 중복: 각 앱이 공유 코드를 자체 번들에 포함시킵니다. 사용자 입장에서는 React 같은 라이브러리를 여러 번 내려받게 됩니다.
  • 버전 드리프트: 팀마다 내부 패키지의 서로 다른 버전을 고정(pin)하면서 "내 로컬에서는 됩니다" 문제가 반복됩니다.
  • 거버넌스 오버헤드: 도움이 되려던 공유 라이브러리가 서서히 소유권 논쟁의 대상이 됩니다. 누가 PR을 리뷰하고, 누가 API를 결정하는가?

이것들은 가상의 시나리오가 아닙니다. Turborepo를 실제로 운영하는 팀들이 일정 규모 이상이 되면 반드시 마주치는 마찰 지점들입니다.


패키지 공유 vs 런타임 공유: 핵심 차이 이해하기

이 개념적 전환이 모든 것을 바꿉니다. 실무에서 경계가 흐릿하게 느껴지는 경우가 많으니, 여기서 시간을 충분히 투자할 가치가 있습니다.

빌드타임(패키지) 공유의 작동 방식

Turborepo 환경에서 공유 패키지는 빌드 시점에 소비됩니다. App A가 @company/ui-button을 임포트하면, 그 컴포넌트의 코드는 App A의 최종 번들에 직접 포함됩니다. 코드가 브라우저에 도달하기 전에 공유가 완료되는 방식입니다.

레시피 카드를 인쇄하는 것과 비슷합니다. 모든 주방(앱)이 레시피의 물리적인 사본을 각자 갖습니다. 레시피가 바뀌면? 모든 카드를 다시 인쇄해서 배포해야 합니다.

빌드타임 공유:
@company/ui → App A 번들 (ui 코드 포함)
@company/ui → App B 번들 (ui 코드 포함)
@company/ui → App C 번들 (ui 코드 포함)

각 앱은 자기완결적입니다. 배포는 단순하지만, 업데이트마다 전체 재빌드가 필요합니다.

런타임 공유의 작동 방식

런타임 공유 — 현대 MFE의 핵심 메커니즘 — 는 이 모델을 뒤집습니다. 공유 코드가 필요한 시점에, 브라우저에서 동적으로 로드됩니다.

현재 가장 검증된 구현체는 Webpack Module Federation(Webpack 5에서 도입)이며, Vite FederationNative ESM도 주목받고 있습니다.

런타임 공유:
App A → 런타임에 CDN에서 ui@latest 가져옴
App B → 런타임에 CDN에서 ui@latest 가져옴
App C → 런타임에 CDN에서 ui@latest 가져옴

UI 라이브러리를 업데이트했나요? 한 번만 배포하면 됩니다. 소비하는 모든 앱이 다음 로드 시 자동으로 새 버전을 받습니다. 재빌드가 필요 없습니다.

트레이드오프 한눈에 보기

항목패키지 공유 (Turborepo)런타임 공유 (MFE)
배포 결합도높음낮음
번들 크기큼 (중복)작음 (런타임 공유)
타입 안전성우수추가 도구 필요
운영 복잡성낮음높음
팀 자율성중간높음
초기 설정 비용낮음높음

어느 쪽이 무조건 낫다는 건 없습니다. 팀 규모, 배포 빈도, 운영 복잡성에 대한 허용 범위에 따라 정답이 달라집니다.


Module Federation으로 런타임 공유 구현하기

런타임 공유가 목적지라면, Module Federation은 현재 가장 실전에서 검증된 이동 수단입니다. 실제로 어떻게 작동하는지, 그리고 왜 대부분의 입문 자료가 설명하는 것보다 훨씬 미묘한지를 살펴보겠습니다.

기본 멘탈 모델

Module Federation은 JavaScript 애플리케이션이 특정 모듈을 **노출(expose)**하고, 다른 애플리케이션의 모듈을 **소비(consume)**할 수 있게 해줍니다 — 모두 런타임에. Webpack이 로딩, 버전 협상, 폴백 동작을 처리합니다.

앱은 두 가지 역할로 나뉩니다:

  • Host(Shell): 다른 마이크로 프론트엔드를 조율하는 메인 애플리케이션
  • Remote: 특정 컴포넌트나 페이지를 노출하는, 독립적으로 배포되는 앱

Remote 앱의 최소 Webpack 설정은 다음과 같습니다:

// webpack.config.js (Remote App)
new ModuleFederationPlugin({
  name: 'productApp',
  filename: 'remoteEntry.js',
  exposes: {
    './ProductList': './src/components/ProductList',
    './ProductDetail': './src/components/ProductDetail',
  },
  shared: {
    react: { singleton: true, requiredVersion: '^18.0.0' },
    'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
  },
})

이를 소비하는 Host 앱:

// webpack.config.js (Host App)
new ModuleFederationPlugin({
  name: 'shell',
  remotes: {
    productApp: 'productApp@https://products.myapp.com/remoteEntry.js',
  },
  shared: {
    react: { singleton: true },
    'react-dom': { singleton: true },
  },
})

shared 설정이 핵심이자 함정인 이유

shared 키는 Module Federation에서 가장 중요하면서도 가장 오해를 많이 받는 부분입니다. 어떤 의존성을 런타임에 공유할지 선언하는 곳입니다.

singleton: true 플래그는 React처럼 두 인스턴스가 동시에 실행되면 절대 안 되는 라이브러리에 필수입니다. Module Federation이 이를 보장하지만, Host와 Remote 간의 꼼꼼한 버전 협상이 필요합니다.

이걸 잘못 설정하면 커리어에서 가장 난해한 디버깅 세션을 경험하게 됩니다.

Turborepo + Module Federation 함께 쓰기

여기서 Turborepo 사용자에게 중요한 포인트가 나옵니다. 두 접근법은 상호 배타적이지 않습니다. 성숙한 팀 중 다수가 Turborepo로 MFE 모노레포를 관리하면서, Module Federation으로 런타임 공유를 구현합니다.

아키텍처는 이런 형태가 됩니다:

turborepo/
├── apps/
│   ├── shell/          # Host 애플리케이션
│   ├── products/       # Remote MFE
│   ├── checkout/       # Remote MFE
│   └── account/        # Remote MFE
├── packages/
│   ├── ui/             # 빌드타임 공유 (디자인 토큰, 타입)
│   ├── utils/          # 빌드타임 공유 (순수 함수)
│   └── mfe-config/     # 공유 Module Federation 설정

Turborepo가 빌드 파이프라인, 캐싱, 툴링 설정을 담당합니다. Module Federation이 런타임 구성을 담당합니다. 모노레포의 개발 경험과 마이크로서비스의 배포 독립성, 둘 다 가져가는 구조입니다.


문서에 안 나오는 실전 함정들

이론은 이론입니다. 실제로 MFE 시스템을 프로덕션에 배포하는 건 전혀 다른 이야기입니다. 대부분의 팀이 이 전환 과정에서 예상치 못하게 마주치는 네 가지 문제를 정리했습니다.

1. 모듈 경계를 넘는 TypeScript 타입

가장 흔한 고통 포인트입니다. Module Federation Remote에서 컴포넌트를 임포트할 때, TypeScript는

지원 문의