Microfrontends

Prerelease

Microfrontends는 웹 애플리케이션을 더 작고 독립적으로 개발 및 배포되어 함께 작동하는 애플리케이션으로 분해하는 아키텍처 패턴입니다.

Turborepo는 통합 프록시 서버를 통해 개발 중 수직 microfrontends(때로는 "zones"라고 함)를 로컬에서 실행하기 위한 기본 지원을 제공합니다. 프록시는 여러 애플리케이션을 조정하고 그들 간의 트래픽을 라우팅합니다.

여러 프론트엔드 애플리케이션이 있는 모노레포가 있다고 가정해 봅시다:

package.json
turbo.json

프로덕션 환경에서 이러한 애플리케이션은 개별적으로 배포되고 리버스 프록시 또는 엣지 라우팅을 사용하여 함께 구성될 수 있습니다. 하지만 개발 중에는 다음을 원합니다:

  • 단일 명령으로 모든 애플리케이션을 동시에 실행
  • 통합 URL(예: http://localhost:3024)을 통해 액세스
  • 경로 패턴을 기반으로 올바른 애플리케이션으로 요청 라우팅
  • 핫 모듈 리로딩 및 WebSocket 연결 지원
  • 포트 충돌 및 수동 조정 방지

시작하기

Turborepo는 개발 중 microfrontend 애플리케이션 간의 트래픽을 자동으로 라우팅하는 내장 프록시 서버를 제공합니다. 프록시는 microfrontends.json 설정 파일을 읽고 turbo dev를 실행할 때 시작됩니다.

npx create-turbo@latest -e with-microfrontends로 생성된 모노레포로 다음 지침을 시도할 수 있습니다.

microfrontends.json 생성

상위 애플리케이션에 microfrontends.json 파일을 생성합니다. 이 애플리케이션은 다른 애플리케이션과 일치하지 않을 때 모든 요청이 대체되는 애플리케이션입니다.

./apps/web/microfrontends.json
{
  "applications": {
    "web": {
      "development": {
        "local": 3000
      }
    },
    "docs": {
      "development": {
        "local": 3001
      },
      "routing": [
        {
          "paths": ["/docs", "/docs/:path*"]
        }
      ]
    }
  }
}

애플리케이션 포트 설정

다음으로 TURBO_MFE_PORT 환경 변수를 사용하여 애플리케이션의 개발 스크립트에서 포트를 설정합니다. 예를 들어:

./apps/web/package.json
{
  "scripts": {
    "dev": "next dev --port $TURBO_MFE_PORT"
  }
}

베이스 경로 처리

프레임워크를 사용하는 경우 프레임워크의 요구 사항에 따라 베이스 경로를 설정합니다.

./apps/my-app/next.config.ts
import type { NextConfig } from 'next';
 
const nextConfig: NextConfig = {
  basePath: '/docs',
};
 
export default nextConfig;

프레임워크별 지침은 프레임워크의 Microfrontends 섹션을 참조하세요.

turbo dev 실행

turbo dev를 실행하면 Turborepo는 다음을 수행합니다:

  1. 구성된 포트에서 프록시 서버 시작(기본값: 3024)
  2. 작업에 TURBO_MFE_PORT 환경 변수 주입하여 애플리케이션의 포트 설정
  3. 모든 구성된 애플리케이션에 대한 개발 작업 실행
  4. 경로 패턴을 기반으로 적절한 애플리케이션으로 수신 요청 라우팅

이제 http://localhost:3024에서 모든 microfrontends 애플리케이션에 액세스할 수 있습니다.

설정

applications 객체의 각 애플리케이션은 다음 속성을 가질 수 있습니다:

제공되지 않으면 애플리케이션 키가 package.json의 이름과 일치하도록 사용됩니다.

development.local

애플리케이션이 로컬에서 실행되는 포트입니다. 다음과 같이 지정할 수 있습니다:

  • 숫자: 3000
  • 포트가 있는 호스트명: "localhost:3000"
  • URL: "http://localhost:3000"
./apps/web/microfrontends.json
{
  "development": {
    "local": 3000
  }
}

development.fallback

애플리케이션이 로컬에서 실행되지 않을 때 프록시할 대상을 선택적으로 제공합니다. 이는 애플리케이션의 하위 집합만 실행하는 turbo dev --filter=web와 같은 명령을 원활하게 만들기 위해 프로덕션으로 라우팅하는 데 가장 자주 사용됩니다.

완전히 정규화된 URL이어야 합니다.

./apps/web/microfrontends.json
{
  "development": {
    "fallback": "example.com"
  }
}

routing

이 애플리케이션으로 라우팅해야 하는 경로 그룹의 배열입니다. 라우팅이 제공되지 않으면 애플리케이션은 일치하지 않는 모든 경로를 포착하는 기본 애플리케이션이 됩니다.

하나의 애플리케이션만 routing 설정을 가지지 않을 수 있습니다. 이것이 다른 애플리케이션과 일치하지 않는 모든 경로를 처리하는 "루트" 애플리케이션입니다.

./apps/web/microfrontends.json
{
  "routing": [
    {
      "paths": ["/api/:version/users/:id"]
    },
    {
      "paths": ["/docs", "/docs/:path*"]
    }
  ]
}

정확한 일치

./apps/web/microfrontends.json
{
  "paths": ["/pricing", "/about", "/contact"]
}

이러한 경로는 작성된 대로 정확하게 일치합니다.

매개변수

./apps/web/microfrontends.json
{
  "paths": ["/blog/:slug", "/users/:id/profile"]
}

:로 시작하는 세그먼트는 단일 경로 세그먼트와 일치합니다. 예를 들어, /blog/:slug/blog/hello와 일치하지만 /blog/hello/world와는 일치하지 않습니다.

와일드카드

./apps/web/microfrontends.json
{
  "paths": ["/docs/:path*", "/api/:version*"]
}

*로 끝나는 매개변수는 0개 이상의 경로 세그먼트와 일치합니다. 예를 들어, /docs/:path*/docs, /docs/intro, /docs/api/reference와 일치합니다.

그룹 레이블

더 나은 유지 관리를 위해 경로를 논리적 그룹으로 구성합니다:

./apps/web/microfrontends.json
{
  "applications": {
    "marketing": {
      "development": {
        "local": 3002
      },
      "routing": [
        {
          "group": "blog",
          "paths": ["/blog", "/blog/:slug*"]
        },
        {
          "group": "sales",
          "paths": ["/pricing", "/contact", "/demo"]
        }
      ]
    }
  }
}

group 필드는 구성 목적을 위한 것이며 라우팅 동작에 영향을 주지 않습니다.

복잡한 라우팅 패턴

중첩된 매개변수로 정교한 라우팅을 정의할 수 있습니다:

./apps/web/microfrontends.json
{
  "routing": [
    {
      "paths": [
        "/api/:version/users",
        "/api/:version/users/:id",
        "/api/:version/posts/:postId/comments/:commentId"
      ]
    }
  ]
}

packageName

워크스페이스에서 package.json의 패키지 이름을 선택적으로 사용합니다.

./apps/main/microfrontends.json
{
  "applications": {
    "main-site": {
      "packageName": "web"
    }
  }
}

options.localProxyPort

localProxyPort 옵션을 사용하여 선택적으로 프록시 포트를 변경합니다.

기본값은 3024입니다.

./apps/web/microfrontends.json
{
  "options": {
    "localProxyPort": 8080
  }
}

프로덕션과 통합

Turborepo microfrontends 프록시는 로컬 사용만을 위한 것입니다. 프로덕션 microfrontends를 구현하고 통합하는 방법은 프로덕션 인프라에 따라 다릅니다. 그러나 로컬 및 프로덕션 환경을 통합하여 환경 전반에 걸쳐 원활한 경험을 만들 수 있습니다.

시작하기 위해 Turborepo의 로컬 프록시를 Vercel의 microfrontends와 통합하도록 구축했습니다. 통합하고자 하는 모든 인프라 제공업체와 협력하기를 기대합니다.

Vercel의 Microfrontends

Vercel은 다음을 제공하는 microfrontends에 대한 기본 지원을 제공합니다:

  • 향상된 성능의 프로덕션 프록시 구현
  • 환경 전반의 대체 URL 지원
  • Vercel 툴바 통합
  • 기능 플래그 지원
  • 애셋 접두사 처리

Vercel 문서에서 자세히 알아보기.

@vercel/microfrontends로 마이그레이션

패키지에 @vercel/microfrontends를 설치하거나 워크스페이스에 추가하면 Turborepo가 내장 프록시 대신 자동으로 이를 사용합니다. 이를 통해 점진적인 마이그레이션 경로가 가능합니다.

Turborepo와 동일한 microfrontends.json 설정을 사용할 수 있습니다. Turborepo의 microfrontends.json 스키마는 Vercel 스키마의 하위 집합이므로 @vercel/microfrontends와 호환됩니다.

@vercel/microfrontends에 대해 자세히 알아보려면 npm의 패키지를 방문하세요.

문제 해결

포트가 이미 사용 중

기본적으로 microfrontends 프록시는 포트 3024를 사용하려고 시도합니다. 다른 목적으로 해당 포트를 이미 사용 중인 경우 options.localProxyPort를 사용하여 Turborepo의 포트를 변경할 수 있습니다.

CSS, 이미지 또는 기타 애셋 누락 또는 경로가 일치하지 않음

microfrontends가 routing 설정에서 일치시키는 경로에 애셋에 대한 경로가 포함되어 있는지 확인합니다. 네트워크 탭을 확인하여 예상대로 일치하거나 일치하지 않는 경로를 찾으세요.

애플리케이션 간 링크로 인한 오류

애플리케이션 간 링크에 Single-Page Application(SPA) 링크(Next.js <Link> 컴포넌트 등)를 사용하면 오류가 발생할 수 있습니다. 포트와 도메인은 동일하게 유지되지만 애플리케이션은 다릅니다. 이는 라우팅이 정의상 "단일 페이지 애플리케이션"이 아님을 의미합니다.

프록시된 포트 방문 시 리디렉션되지 않음

프록시된 포트에 여전히 직접 접근할 수 있습니다. 예를 들어, localhost:3000localhost:3042로 프록시되어 있어도 브라우저에서 여전히 localhost:3000을 방문할 수 있습니다.

localhost:3000localhost:3024로 리디렉션되도록 하려면 애플리케이션에서 수동으로 설정해야 합니다.

애플리케이션이 시작되지 않음

다음을 확인하세요:

  1. packageName이 실제 패키지 이름과 일치하는지
  2. 지정된 task가 패키지의 package.json에 존재하는지
  3. 각 애플리케이션의 포트가 사용 가능한지
  4. 모든 종속성이 설치되어 있는지

전체 예제

다음은 전자 상거래 플랫폼을 위한 microfrontends 설정의 전체 예제입니다:

./apps/web/microfrontends.json
{
  "options": {
    "localProxyPort": 3024
  },
  "applications": {
    "web": {
      "packageName": "web",
      "development": {
        "local": 3000
      }
    },
    "docs": {
      "packageName": "documentation",
      "development": {
        "local": 3001
      },
      "routing": [
        {
          "group": "documentation",
          "paths": [
            "/docs",
            "/docs/:path*",
            "/api-reference",
            "/api-reference/:path*"
          ]
        }
      ]
    },
    "blog": {
      "development": {
        "local": 3002
      },
      "routing": [
        {
          "group": "content",
          "paths": [
            "/blog",
            "/blog/:slug",
            "/blog/category/:category",
            "/authors/:author"
          ]
        }
      ]
    },
    "shop": {
      "development": {
        "local": 3003
      },
      "routing": [
        {
          "group": "commerce",
          "paths": [
            "/products",
            "/products/:id",
            "/cart",
            "/checkout",
            "/orders/:orderId"
          ]
        }
      ]
    }
  }
}

이 설정을 사용하면:

  • web 앱은 홈페이지 및 일치하지 않는 모든 경로를 처리합니다
  • docs 앱은 모든 문서를 처리합니다
  • blog 앱은 블로그 게시물 및 저자 페이지를 처리합니다
  • shop 앱은 전자 상거래 기능을 처리합니다