모노레포에서 무엇을 공통으로 빼야하나
2026년 7월 3일
모노레포 공용 경계: 무엇을 공통으로 뺄지 정하는 기준
1. 우리 모노레포의 구조적 제약
apps/
web/ Next 데스크톱 웹 (tsconfig: @/* → ./app/*)
mobile/ Next 모바일 웹뷰 (tsconfig: @/* → ./app/*)
packages/
tokens/ @plick/tokens — theme.css (색·간격·글자·라운드)
ui/ @plick/ui — 앱 중립 컴포넌트·아이콘
eslint-config/·typescript-config/ 도구 설정
세 가지 제약이 판단의 전제다:
@/alias는 앱마다 자기app/을 가리킨다. 즉@plick/ui의 코드는@/_lib/constants(팀 레지스트리)·@/_lib/format같은 앱 로컬 모듈에 절대 닿을 수 없다. 패키지가 앱을 import하면 의존이 역류한다(패키지→앱).- 두 앱은 같은 제품이지만 다른 매체다. 모바일은 웹뷰(터치, 좁은 폭, 0.55배 시안), 웹은 데스크톱(hover/focus 링, 넓은 폭, 0.45배 시안). "같은 것을 보여줘도" 스케일·인터랙션·레이아웃이 갈린다.
- BE 이전이라 도메인 데이터가 mock이다. 타입 shape는 계약이지만, mock 콘텐츠는 앱별 샘플이라 자유롭게 다르다.
이 세 제약이 아래 기준을 강제한다.
2. 결정 — "공통으로 뺀다"의 게이트
한 코드 조각을 packages/*로 올릴지는 세 관문을 모두 통과해야 한다. 하나라도 막히면 앱별로 둔다.
게이트 A — 앱을 역참조하지 않는가 (Hard, 절대조건)
패키지는 앱을 몰라야 한다. 앱 로컬 레지스트리·유틸(TEAMS, formatCount, 라우트 상수)을 내부에서 조회하면 실격.
→ 통과시키려면 결합을 prop 주입으로 끊는다(ADR 0005 §2-1의 MediaThumb: team: TeamCode 조회 → colorVar: string 주입). 못 끊으면 승격 불가.
게이트 B — 스케일·인터랙션·레이아웃이 두 앱에서 동일한가
- 동일 → 승격 후보. (예:
MediaThumb·ReporterTierBadge·icons— 토큰/currentColor만, 형태가 하나.) - 갈림 → 앱별. variant prop을 잔뜩 달아 하나로 만들면 두 파일보다 읽기 어렵고, 한쪽 디자인 변경이 공유 API와 반대편 앱을 동시에 흔든다.
게이트 C — 지금 안정적인가 (성숙도)
두 앱에 실제로 두 번째 사용처가 생겼고, 모양이 굳었을 때 올린다. 화면이 아직 진화 중이면 조기 추상화 비용이 크다(§4-3). "언젠가 공유하겠지"로 미리 올리지 않는다.
결과: 종류별 기본 위치
| 종류 | 기본 위치 | 근거 |
|---|---|---|
| 색·간격·글자·라운드 값 | @plick/tokens | 매체 무관, 네임스페이스 하나. 반복 시맨틱 값은 웹 전용 CSS에 숨기지 않음(ADR 0005 §3) |
순수 유틸(formatCount·avatarInitials) | 후보(현재 앱별 복제) | 토큰·JSX 없음 → A·B 무조건 통과. C(중복 실증)만 서면 승격 가치 |
도메인 타입(FeedPost·Comment) | 현재 앱별 + 수동 동기화(§3) | shape는 계약이나, 공유 패키지화는 아직 보류(트레이드오프 명시) |
| 원자 컴포넌트(아이콘·썸네일·배지) | @plick/ui | A를 prop로 끊고 B가 동일 → 이미 다수 승격 |
화면 조립체(ArticleBody·추천 리스트·CommentThread 래퍼) | 앱별 _components/ | B가 갈림. 아래 §5 참조 |
3. 도메인 타입: "공유 패키지" 대신 "수동 동기화 복제"를 택한 이유
FeedPost·Comment 등은 두 앱 _lib/types.ts에 같은 shape로 복제돼 있고, web 파일 헤더에 "모바일과 같은 API를 소비하므로 모양을 항상 동일하게 유지(BE 연동 전까지 수동 동기화)" 라고 못박아 뒀다. 왜 @plick/types로 안 올렸나:
- shape는 이미 계약이라 공유해도 되지만, 타입만을 위한 패키지는 지금 비용 대비 이득이 얕다 — mock·화면이 매주 바뀌는 단계에서 타입 한 필드 추가마다 패키지 버전을 오가는 마찰이 생긴다.
- 드리프트의 실체는 타입이 아니라 mock 데이터였다. 예: 웹 mock은 기자명이 한국어·
body채움, 모바일 mock은 영어·body비움(KAN-243 전). 이건 의도된 앱별 샘플 차이지 타입 불일치가 아니다. 타입 shape(body?: string[])는 양쪽 동일했다. - 따라서 지금의 리스크는 "shape가 몰래 갈라지는 것" 하나뿐이고, 그건 파일 헤더 주석 + 코드리뷰로 관리한다.
트레이드오프(명시): 이 선택의 대가는 shape 동기화를 사람이 책임진다는 것. BE 계약이 확정되거나 타입이 안정되면 §2 게이트 C가 서므로 그때
@plick/types(또는@plick/ui/types)로 승격을 재검토한다. 지금은 "수동 동기화 + 주석 계약"이 더 싸다.
4. 막을 수 있는 실패들
기준을 이렇게 잡은 이유는 각 관문이 구체적 사고를 하나씩 막기 때문이다.
4-1. 게이트 A가 막는 것 — 의존 역류
패키지가 @/_lib/constants를 import하면 @plick/ui → apps/mobile 의존이 생긴다. 그러면 웹이 그 컴포넌트를 쓸 때 모바일 앱을 통째로 끌고 오거나 빌드가 깨진다. ADR 0005에서 MediaThumb이 TEAMS[team]을 내부 조회하던 걸 colorVar 주입으로 뒤집은 게 바로 이 방지책.
4-2. 게이트 B가 막는 것 — variant prop 폭발과 양방향 결합
ArticleBody를 억지로 공유하면 size·interaction·layout·titleScale… prop이 붙는다. 결과:
- 한쪽 앱의 사소한 디자인 조정이 공유 컴포넌트 API를 바꾸고 반대편 앱을 회귀 위험에 넣는다.
- 파일 하나가 두 매체의 분기를 다 안아 읽기·수정이 두 파일보다 어려워진다.
실제로
CommentThread는 웹(답글 접기 토글, client)·모바일(인라인, server)로 일부러 갈라 뒀다(ADR 0004·0009·0010). 추천 기사도 웹은 스크림 카드, 모바일은 리스트 행이라 다른 컴포넌트다.
4-3. 게이트 C가 막는 것 — 조기 추상화
두 번째 사용처가 없는데 미리 공유로 만들면, 유일 사용처의 요구가 바뀔 때마다 공유 API를 흔든다. "지금 확실히 중복이고 모양이 굳었을 때"만 올린다.
4-4. 승격 자체가 부르는 함정(→ 절차는 ADR 0005)
올리기로 정해도 대가가 있다. 0005에서 실제로 밟은 것들:
- Tailwind
@source누락:@plick/ui클래스는node_modules취급이라 앱이 자동 스캔 못 함 → 빌드는 되는데 스타일만 실종. 양쪽globals.css에@source. - 토큰 이름 충돌:
--text-nav가 색 토큰--color-nav와 네임스페이스를 공유해 활성 링크 색이 죽음 → 폰트 토큰 작명 시 동명--color-*확인. - stale 캐시 재발: 공유 패키지(tokens) 변경은 dev 캐시가 끈질김 →
.next삭제.
이 비용 때문에라도 "공유가 기본"이 아니라 "게이트 통과 시에만 공유" 가 맞다.
5. 안티패턴 — 이렇게는 올리지 않는다
- ❌ "web·mobile 둘 다 쓰니까" 만으로 승격. → 게이트 B(동일성) 없이는 무의미.
- ❌ 화면 조립체(문단·액션·댓글을 다 담은
ArticleBody류)를 통짜 공유. → variant 폭발(§4-2). - ❌ 승격하면서 원본을 복사만 하고 남겨 사본 2개 방치. → ADR 0005 §2-2대로 이동+import 교체를 한 PR에. 남은 사본은
/audit(code-audit 스킬) 대상. - ❌ 앱 로컬 값을 패키지 내부에서 조회. → 게이트 A 위반, prop으로 주입.
6. 실무 체크리스트 (승격 결정 전 5문)
- A 이 조각이 앱 로컬(
@/_lib/…, 라우트,TEAMS)을 내부에서 참조하나? → 그렇다면 prop로 끊을 수 있나? 못 끊으면 중단. - B 두 앱에서 스케일·인터랙션(hover vs active)·레이아웃이 정말 동일한가? 아니면 앱별.
- C 두 번째 실사용처가 이미 있고 모양이 굳었나? 아니면 대기.
- 올린 뒤 양쪽 앱
globals.css@source·에셋(public/*)·Tailwind 토큰 충돌을 점검하나?(0005) - 원본 이동 + import 교체 + 사본 제거를 같은 PR에 담나?
모두 예 → @plick/ui(또는 @plick/tokens). 하나라도 아니오 → 앱별 _components/.