profile

이아현

Frontend Engineer

githublinked-in

Next.js Multi-Zones로 공통 기능 분리하기

6/25/2026FE

서비스를 운영하다 보면 처음에는 한 곳에만 필요했던 기능이, 어느 순간 다른 서비스에서도 필요해지는 일이 생깁니다.
이번에 마주한 상황이 딱 그랬습니다.


기존에 A 서비스에 강하게 결합되어 있던 B 기능을, 새롭게 C 서비스에도 제공해야 하는 요구사항이 생겼습니다.
A에만 붙어 있던 기능을 C에서도 똑같이 쓰려면, 결국 B 기능을 독립적으로 떼어내는 것이 가장 깔끔한 방향이었습니다.


그래서 B 기능을 모노레포의 신규 워크스페이스로 분리하고, 이를 A와 C 두 서비스에 제공하는 구조를 설계하게 되었습니다.
이 글은 특정 서비스의 구현 디테일보다는, 그 과정에서 차용한 개념과 부딪혔던 트러블슈팅 경험에 초점을 맞춰 적어보려 합니다.


어떤 구조가 필요했을까

먼저 우리가 풀어야 할 문제를 정리해보겠습니다.


  • B 기능은 독립적으로 개발 / 배포되어야 합니다. (A, C 어느 쪽의 배포 사이클에도 묶이면 안 됩니다.)
  • 동시에 A, C 입장에서는 마치 자기 서비스의 일부인 것처럼 B 기능이 보여야 합니다.
  • 사용자 입장에서는 도메인이 바뀌거나 새 창이 뜨는 등의 어색한 경험 없이, 자연스럽게 이어져야 합니다.

이 조건들을 보면, 단순히 B를 별도 도메인에 배포하고 링크로 연결하는 방식으로는 부족했습니다.
"여러 개의 독립된 앱을, 사용자에게는 하나의 앱처럼" 제공해야 했고, 이 지점에서 Next.js의 Multi-Zones 개념을 차용하게 되었습니다.


Multi-Zones란

Multi-Zones1는 하나의 큰 애플리케이션을 여러 개의 작은 Next.js 앱(zone)으로 나누고, 이들을 하나의 도메인 아래에서 동작하는 것처럼 묶는 Next.js의 접근 방식입니다.


핵심은 경로(path) 기준으로 요청을 각 zone에 분배한다는 것입니다.
예를 들어 아래와 같은 형태입니다.

경로 담당 zone
/, ... 메인 앱 (A 서비스)
/{B-기능-경로}/* B 기능 앱

사용자는 domain.com이라는 하나의 도메인 안에서 움직이지만, 실제로는 특정 경로에 진입하는 순간 별도로 배포된 B 기능 앱이 응답을 내려주는 구조입니다.
덕분에 각 앱은 독립적으로 배포되면서도, 사용자에게는 끊김 없는 하나의 서비스로 보이게 됩니다.


이 개념을 우리 상황에 대입하면, A와 C는 각각 자신의 메인 앱을 그대로 유지하고, B 기능 앱을 특정 경로에 꽂아 넣는 형태로 제공할 수 있게 됩니다.


그럼 이 경로 기준 분배를 실제로 누가, 어떻게 처리하느냐가 관건이 됩니다.
바로 이 지점에서 선택지가 갈렸습니다.


1차 선택 : @vercel/microfrontends

A, C 서비스는 모두 배포에 Vercel을 사용하고 있습니다.
그래서 가장 먼저 검토한 것은 Vercel이 제공하는 @vercel/microfrontends2였습니다.


이 방식을 최우선으로 둔 이유는 명확했습니다.
공식 문서를 읽었을 때, 라우팅이 Vercel 네트워크 인프라에서 직접 일어난다는 점이 굉장히 인상적이었기 때문입니다.



요약하면, 프록시 없이, 추가 network hop 없이 인프라에서 운반한다는 점이 가장 매력적이었습니다.
우리가 직접 관리해야 할 인프라가 줄어든다는 건 곧 운영 부담이 줄어든다는 의미였으니까요.


여기에 더해, 한 가지 더 마음에 드는 부분이 있었습니다.
보통 zone 간 이동은 하드 네비게이션으로 일어나는데, 이 방식의 단점은 Next.js의 prefetch, prerender를 활용할 수 없습니다.
그런데 @vercel/microfrontends는 zone 경계를 넘어서도 prefetch / prerender를 사용할 수 있는 로직을 제공하고 있었습니다.
인프라 라우팅 + 하드 네비게이션의 단점까지 보완해주기 때문에 가능하다면 이 방식을 사용하는 게 베스트라고 생각했습니다.


그래서 이 방식을 팀에 공유하고, 합의를 거쳐 실제로 적용했습니다.
구조적으로도 깔끔했고, 기대했던 대로 동작했습니다.


부딪힌 벽 : 비용

문제는 예상하지 못했던 곳에서 나왔습니다.
바로 비용이었습니다.


가장 큰 문제는 microfrontends group에 묶을 수 있는 프로젝트 개수였습니다.



그런데 우리 서비스의 Vercel 프로젝트 구성이 발목을 잡았습니다.


  • A 서비스dev, stg, prod 각 환경이 별도 프로젝트로 나뉘어 있습니다.
  • C 서비스는 모든 환경을 하나의 프로젝트에서 관리하고 있습니다.

여기에 분리한 B 서비스 프로젝트를 A, C에 각각 group으로 묶으려고 하니, 무료 범위인 2개 프로젝트를 금세 넘어서게 됩니다.
A 서비스의 모든 환경을 C처럼 하나의 프로젝트로 마이그레이션한다고 가정해도, 결국 추가되는 비용을 피할 수 없는 구조였습니다.


여기에 더해, 요청이 특정 횟수를 초과하면 그만큼 또 비용이 붙습니다.
즉, 프로젝트 개수와 트래픽 양쪽에서 비용이 누적되는 구조였던 거죠.


결국 기술적으로는 최선이지만, 우리 프로젝트 구성과 비용 측면에서는 지속 가능하지 않다는 결론에 도달했습니다.


여기서 한 가지 배운 점이 있습니다.
기술 선택에서 가장 깔끔하고 좋은 방식이 꼭 정답은 아니라는 것입니다.
운영 부담, 비용, 프로젝트 구성 같은 현실적인 제약을 함께 저울질해야 비로소 답이 나온다는 걸 체감했습니다.


2차 선택 : Next.js rewrites

그래서 차선책으로 눈을 돌린 것이 Next.js의 기본 기능인 rewrites3였습니다.


rewrites는 들어온 요청 경로를 다른 목적지로 내부적으로 다시 매핑해주는 기능입니다.
URL은 그대로 유지한 채, 실제 응답은 다른 곳에서 가져올 수 있습니다.

// next.config.js (A, C 메인 앱)
module.exports = {
  async rewrites() {
    return [
      {
        source: '/{B-기능-경로}/:path*',
        destination: 'https://{B-기능-앱-주소}/{B-기능-경로}/:path*',
      },
    ];
  },
};

위처럼 설정하면, 사용자가 domain.com/{B-기능-경로}/...로 접근했을 때 메인 앱이 그 요청을 B 기능 앱으로 다시 흘려보냅니다.
Multi-Zones에서 우리가 원했던 경로 기준으로 zone을 분배한다는 개념을, Next.js의 기본 기능만으로 구현하는 셈입니다.


별도의 microfrontends 과금 구조를 타지 않고 Next.js 표준 기능으로 처리하기 때문에, 비용 문제에서 비교적 자유로워졌습니다.


두 방식 비교

직접 둘 다 적용해보며 느낀 차이를 정리하면 아래와 같습니다.

구분 @vercel/microfrontends Next.js rewrites
라우팅 처리 Vercel 네트워크 인프라에서 같은 요청 내 처리 메인 앱이 요청을 목적지로 다시 매핑
network hop 추가 hop 없음 메인 앱을 한 번 거침
prefetch 등 zone 경계를 넘어서도 지원 하드 네비게이션 기반, 별도 보완 필요
비용 무료는 프로젝트 2개까지, 이후 개수당 과금 + 요청 초과분 Next.js 표준 기능이라 상대적으로 자유로움

물론 rewrites로 전환하는 과정도 마냥 매끄럽지만은 않았습니다.


트러블슈팅 : 정적 에셋과 basePath

가장 신경 써야 했던 부분은 정적 에셋 경로였습니다.


메인 앱(A, C)과 B 서비스가 같은 도메인 아래에서 동작하다 보니, 정적 에셋 경로가 서로 충돌할 여지가 있었습니다.
또 rewrites로 경로를 매핑할 때, 어떤 경로가 어느 앱으로 가야 하는지 헷갈리지 않도록 명확한 경계를 두는 것도 중요했습니다.


그래서 B 서비스에 basePath를 설정했습니다.

// next.config.js (B 서비스)
module.exports = {
  basePath: '/{B-기능-경로}',
};

basePath를 지정하면 B 서비스의 페이지 경로뿐 아니라 _next/static 같은 정적 에셋 경로에도 prefix가 붙기 때문에, 메인 앱의 에셋과 충돌할 가능성을 자연스럽게 줄일 수 있었습니다.
rewrites의 source / destination 경로 역시 이 basePath 기준으로 맞추니 매핑도 한결 명확해졌고요.


다만 한 가지 더 챙겨야 할 부분이 있었습니다.
바로 public 디렉터리의 에셋을 직접 참조할 때입니다.
basePath는 코드상의 라우팅/번들 에셋에는 자동으로 반영되지만, public 에셋을 직접 경로로 가리킬 때는 prefix가 자동으로 붙지 않습니다.
그래서 B 서비스의 public 에셋을 사용할 때도 basePath를 인지하도록, 경로를 공통화해두었습니다.
(매번 손으로 prefix를 붙이다 보면 분명 어딘가에서 실수하게 되니까요 😅)


CORS나 웹 스토리지 공유 같은 사항은 rewrites는 브라우저 입장에서 보면 같은 도메인 내부의 요청으로 처리되기 때문에, 큰 이슈가 없었습니다.


그래서, 느리지 않았을까?

rewrites는 메인 앱을 한 번 거쳐 B 서비스로 요청을 흘려보내는 구조이다 보니, "그럼 느려지는 거 아니야?"라는 의문이 자연스럽게 따라옵니다.
사실 저도 이 점을 가장 걱정했습니다.


전략은 이랬습니다.
만약 rewrites가 체감될 만큼 많이 느리다면, 그걸 근거로 삼아 vercel/microfrontends 도입을 제안드려보자는 것이었죠.


그런데 실제로 배포해서 써보니, 살짝 느리긴 해도 미칠 듯이 느리다고 할 정도는 아니었습니다. (분리한 B 서비스 앱 자체가 작아서 그런 영향도 있는 것 같습니다)
결국 체감 성능이 충분히 납득할 만한 수준이라, rewrites 구조로 가기로 결정했습니다.


마무리

이번 경험을 한 문장으로 정리하면, 개념은 그대로 두되, 그것을 실현하는 수단은 현실에 맞게 바꿀 수 있다는 것이었습니다.


우리가 진짜로 원했던 것은 Multi-Zones라는 개념 — 즉 "독립적으로 배포되는 여러 앱을 하나의 서비스처럼 경로 단위로 묶는 것"이었습니다.
이 개념을 구현하는 수단으로 처음에는 @vercel/microfrontends를 골랐고,
비용이라는 제약을 만난 뒤에는 Next.js rewrites로 갈아탔지만,
달성하려던 목표 자체는 변하지 않았습니다.


기술 선택에서 가장 좋은 방식이 늘 정답이 아니라는 것, 그리고 제약을 만났을 때 개념과 수단을 분리해서 생각하면 차선책을 훨씬 빠르게 찾을 수 있다는 것을 배운 값진 경험이었습니다 :-)

Footnotes

  1. https://nextjs.org/docs/app/guides/multi-zones

  2. https://vercel.com/docs/microfrontends

  3. https://nextjs.org/docs/app/api-reference/config/next-config-js/rewrites

Next.js Multi-Zones로 공통 기능 분리하기 : 🐢