프론트엔드 모노레포에서 shared package를 어떻게 나눌까?

Frontend

이 글은 프론트엔드 모노레포 아키텍처 시리즈 1편입니다.

프론트엔드 모노레포를 운영하다 보면 가장 먼저 나오는 질문 중 하나가 이것입니다.

  • 무엇을 shared package로 빼야 할까
  • 어디까지 공통화해야 할까
  • shared, common, utils 같은 패키지를 만들어도 괜찮을까

처음에는 공통화가 좋아 보입니다. 중복 코드를 줄이고, 앱 간 일관성을 맞추고, shared UI를 한 곳에서 관리할 수 있기 때문입니다.

하지만 실무에서는 반대 비용도 같이 따라옵니다.

  • 공통 패키지 하나를 바꿨는데 모든 앱이 영향 받거나
  • package 수는 많아졌는데 경계는 오히려 더 흐려지거나
  • shared가 사실상 아무 코드나 들어가는 폴더가 되기도 합니다

즉, shared package 설계는 재사용성 문제이기도 하지만, 더 크게 보면 변경 영향 반경을 어디까지 허용할 것인가의 문제입니다.

이 글에서는 프론트엔드 모노레포에서 shared package를 어떤 기준으로 나누면 좋은지 실무 관점에서 정리해보겠습니다.

한눈에 보면

먼저 짧게 정리하면 이렇습니다.

  • shared package는 "중복이 보인다"만으로 빼기보다 책임이 명확한 공통성이 있을 때 빼는 편이 좋습니다.
  • 공통화는 생산성을 높이지만, 동시에 dependency fan-out과 cache invalidation 범위도 넓힙니다.
  • 이름이 추상적인 shared, common, lib보다 ui, api, config-eslint처럼 책임이 드러나는 패키지가 더 안정적입니다.

조금 더 자세히 보면 아래처럼 볼 수 있습니다.

패턴 보통 결과
책임이 드러나는 공통 패키지 경계가 비교적 안정적이고 이해가 쉬움
추상적인 shared/common 패키지 시간이 갈수록 범위가 커지고 경계가 흐려짐
공통화를 너무 빨리 진행 영향 범위가 넓어지고 변경 비용이 커짐
공통화를 늦추고 실제 수요를 본 뒤 분리 경계가 더 명확해짐

빠르게 정리하면, shared package는 재사용성보다 책임과 영향 범위를 기준으로 나누는 편이 더 안정적입니다.

왜 shared package가 금방 문제를 만들까?

처음에는 대체로 이런 구조에서 시작합니다.

repo/
  apps/
    web/
    admin/
  packages/
    ui/

이때 공통 코드가 조금씩 늘어나면 아래처럼 패키지를 계속 만들고 싶어집니다.

packages/
  ui/
  shared/
  common/
  utils/
  hooks/
  lib/

문제는 package 수가 늘어나는 것 자체가 아니라, 각 package의 책임이 점점 설명되지 않게 되는 것입니다.

예를 들어:

  • utils 안에 도메인 코드가 들어가고
  • shared 안에 UI와 API helper가 같이 들어가고
  • commonshared의 차이를 아무도 설명하지 못하게 됩니다

즉, shared package 문제는 보통 도구 문제가 아니라 이름과 책임 설계 문제에서 시작합니다.

shared package를 만들기 전에 먼저 물어야 할 것

실무에서는 아래 질문이 꽤 중요합니다.

1. 이 코드는 정말 여러 앱이 같이 써야 하는가?

예를 들어 apps/web에서만 쓰는 hook이라면, 아직 package로 뺄 이유가 약할 수 있습니다.

2. 이 코드는 범용적인가, 아니면 특정 도메인에 묶여 있는가?

예를 들어 인증 토큰 처리 로직은 공통으로 보이지만, 실제로는 auth 도메인에 더 가깝습니다.

3. 이 코드를 분리하면 변경 영향 범위를 감당할 수 있는가?

공통 패키지로 올리면 재사용은 쉬워지지만, 바꿨을 때 영향 받는 앱 수는 늘어납니다.

즉, package 분리는 "편하니까"보다 공통화했을 때의 비용을 설명할 수 있는가가 더 중요합니다.

어떤 shared package는 비교적 안정적인가?

보통 아래 패키지는 책임이 비교적 명확해서 안정적으로 가져가기 좋습니다.

1. ui

packages/
  ui/
    src/
      button.tsx
      input.tsx
      dialog.tsx
      index.ts

버튼, 입력창, 다이얼로그처럼 공통 UI는 shared package로 가져가기 쉽습니다.

// packages/ui/src/index.ts
export * from './button';
export * from './input';
export * from './dialog';

다만 여기서도 주의할 점은 있습니다.

  • ui 안에 auth 비즈니스 로직을 넣지 않기
  • 앱 전용 variant를 너무 많이 넣지 않기

즉, ui는 공통 컴포넌트 package이지, 모든 화면 요구사항을 다 담는 package가 되면 안 됩니다.

2. config-eslint

packages/
  config-eslint/
    base.js
    next.js

공통 lint 설정은 shared package로 두기 좋습니다.

// packages/config-eslint/next.js
module.exports = {
  extends: ['next/core-web-vitals', './base.js'],
};

다만 이 패키지는 fan-out이 넓어지기 쉽기 때문에, 변경이 자주 일어나는 규칙까지 전부 몰아넣는 것은 조심해야 합니다.

3. config-typescript

{
  "extends": "@repo/config-typescript/base.json"
}

공통 TS 설정도 비교적 안정적입니다. 다만 이 역시 작은 수정이 전체 앱 타입 체크와 build에 영향을 줄 수 있다는 점은 같이 봐야 합니다.

어떤 shared package는 쉽게 비대해질까?

1. shared

packages/
  shared/
    button.tsx
    formatDate.ts
    useAuth.ts
    fetchUser.ts

이 구조는 처음에는 편하지만, 시간이 지나면 책임이 거의 설명되지 않게 됩니다.

2. common

common도 마찬가지입니다. 이름만 보면 어디까지 common인지 설명하기 어렵습니다.

3. utils

범용 함수만 있어야 할 것 같지만, 실제로는 아래처럼 흐르기 쉽습니다.

// packages/utils/auth.ts
export function isAdminRole(role: string) {
  return role === 'admin';
}

이 코드는 범용 util이라기보다 auth 도메인 규칙에 가깝습니다.

즉, 추상적인 이름을 가진 package는 시간이 갈수록 경계 붕괴를 숨기는 역할을 하기 쉽습니다.

실제로는 어떻게 시작하는 게 좋을까?

처음에는 아래 정도가 더 안정적입니다.

packages/
  ui/
  api/
  config-eslint/
  config-typescript/

이 구조에서 시작하면:

  • ui
    • 공통 UI
  • api
    • 공통 HTTP client 혹은 fetch wrapper
  • config-eslint
    • 공통 lint 설정
  • config-typescript
    • 공통 TS 설정

정도로 책임을 비교적 분명하게 설명할 수 있습니다.

그리고 나서 실제로 반복이 쌓일 때만 분리하는 편이 좋습니다.

예를 들어:

packages/
  ui/
  auth/
  billing/
  api/

이런 구조는 "도메인 반복이 실제로 생겼을 때" 더 자연스럽게 나옵니다.

package를 늘리기 전에 보는 체크리스트

shared package를 하나 더 만들기 전에 보통 아래를 확인하는 편이 좋습니다.

  • 이 코드는 최소 2개 이상의 앱/패키지에서 안정적으로 재사용되는가
  • package 이름만 보고 책임이 드러나는가
  • 바뀌었을 때 영향 받는 앱을 설명할 수 있는가
  • 이 코드는 범용적인가, 아니면 특정 도메인인가
  • 앱 내부에 두는 편이 더 단순하지는 않은가

이 체크리스트를 통과하지 못하면, package로 빼는 시점이 아직 아닌 경우가 많습니다.

공통화는 왜 생산성이면서 동시에 비용일까?

shared package를 잘 쓰면:

  • 중복이 줄고
  • 앱 간 일관성이 생기고
  • 팀 생산성이 올라갈 수 있습니다

하지만 동시에:

  • 한 package 수정이 여러 앱에 영향을 주고
  • 캐시 무효화 범위가 넓어지며
  • CI 시간이 길어질 수 있습니다

즉, 공통화는 항상 좋은 일이 아니라 생산성과 변경 반경 사이의 교환에 가깝습니다.

정리하면

프론트엔드 모노레포에서 shared package를 나누는 기준은 "중복이 보이느냐" 하나로 결정하기 어렵습니다.

실무 기준으로 정리하면:

  • 책임이 드러나는 package부터 시작하고
  • 추상적인 shared, common, utils는 가능한 늦게 만들고
  • 재사용성보다 변경 영향 범위를 함께 보고
  • 실제 반복이 생길 때 package를 늘리는 편이 더 안정적입니다

중요한 것은 package 수를 늘리는 것이 아니라, 레포 안에서 어떤 코드가 어디에 있어야 하는지 팀이 설명할 수 있는 상태를 만드는 것입니다. 그 상태가 되면 shared package는 생산성이 되지만, 그렇지 않으면 오히려 운영 비용이 됩니다.

다음 글에서 이어서 보기

여기까지는 shared package를 어떤 기준으로 나누면 좋은지 정리했습니다. 다음 글에서는 한 단계 더 들어가서, 프론트엔드 모노레포에서 auth, api, billing 같은 도메인 패키지를 어떻게 설계하면 좋은지를 정리합니다.

같이 보면 좋은 글