프론트엔드 모노레포에서 shared package를 어떻게 나눌까?
이 글은 프론트엔드 모노레포 아키텍처 시리즈 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가 같이 들어가고common과shared의 차이를 아무도 설명하지 못하게 됩니다
즉, 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 같은 도메인 패키지를 어떻게 설계하면 좋은지를 정리합니다.
