들어가면서
이전 글에서 했던 결론은 이거였다.
공유 UI 패키지가 진짜로 공유한 건 컴포넌트가 아니라 동작·로깅·약속이었다.
그 글을 쓰고 나서 며칠, 같은 패키지를 다시 열어 호출 지점을 세고 부품을 비교해 봤다. 합쳐도 깨끗해 보이는 짝이 한두 개 있었다. 그리고 결국, 하나도 합치지 않았다.
이번 글은 그 데이터와, 합치지 않기로 한 이유에 대한 글이다.
가명으로 두자. promo-nudging-ui 라는 작은 공유 UI 패키지가 있고, 외부로 export 하는 메인 컴포넌트가 8개(N1 ~ N8) 있다고 하자. 호출 화면도 A ~ E 다섯 곳이라고 하자.
데이터 (1) — 호출 지점 분포
8개 컴포넌트가 앱/패키지 외부에서 import 되는 위치 수를 그대로 세었다.
| 컴포넌트 | 외부 호출 지점 수 | 사용 화면 |
|---|---|---|
| N1 (이미지 + 텍스트 + CTA, 큰 카드) | 2 (같은 화면 안) | A |
| N2 (한 줄 컴팩트) | 1 | B |
| N3 (인라인 행, 분기 두 가지) | 1 (한 파일에서 분기 2회) | B |
| N4 (우측 적용가, 두 줄) | 1 | C |
| N5 (모션 + 한 줄, 데스크톱 분기) | 1 | D |
| N6 (모션 + 비율 + 가격 강조) | 1 | D |
| N7 (요약, 모션) | 1 | E |
| N8 (요약 + CTA) | 1 | E |
핵심 관찰은 셋이다.
- 8개 컴포넌트 → 외부 호출 파일은 5개.
- 한 컴포넌트가 2개 이상의 서로 다른 화면에서 재사용된 사례: 0건.
- 같은 화면 안의 인접 분기에 함께 쓰이는 케이스만 일부.
이 패키지가 “여러 화면에서 재사용” 한 횟수는 사실상 0회 였다.
데이터 (2) — 무엇이 공유되고, 무엇이 갈라졌나
같은 8개 컴포넌트의 본체를 한 줄씩 줄여 비교해 보면, 공유된 결과 갈라진 결이 분명히 갈린다.
✅ 공유된 것 (실질적 재사용)
| 공유 자산 | 사용 컴포넌트 수 | 결 |
|---|---|---|
| 라우팅 + 클릭 트래킹 컨테이너 | 6 / 8 | 동작 |
| 임프레션 / 클릭 로깅 훅 | 다수 | 로깅 |
placement data-attribute 부착 |
7 / 8 | 약속(placement) |
| 식별 코드 → 이름/이미지 매핑 | 7 / 8 | 데이터 매핑 |
| 금액 포매팅 함수 | 6 / 8 | 포매팅 약속 |
| 렌더 가드 (값 ≤ 0 이면 null) | 5 / 8 | 렌더 가드 |
| 색상 / 배경 토큰 | 4 / 8 | 토큰 |
❌ 공유되지 못한 것 (시각·구성)
- 레이아웃: 한 줄 / 두 줄 / 우측 배치 / 이미지+텍스트+버튼 / 모션 / 데스크톱 분기 — 6가지
- 이미지: 없음 / 페어 모션 / 직사각형 / 원형 — 4가지
- 텍스트 위계:
m,s,l,*-medium,*-bold조합 — 6가지 - 외곽 스타일: 라운드 4 / 라운드 6 / 그라데이션 보더 / 하단 보더 — 4가지
- 액션: 없음 / Chevron / 두 가지 라벨의 텍스트 버튼 / “바로 적용” — 5가지
- 분기 props: 모드 플래그, 적용 가능 여부, 총가, 포인트, 비율, 비활성, 최소 금액 — 각자 다른 슬롯
8개 모두가 공유하는 props 는 단 3개뿐이었다 — 식별 코드, placement, className. 나머지는 컴포넌트마다 자기 모양이었다.
데이터 (3) — 컴포넌트 간 유사도
여기서 한 단계 더 들어가, 각 컴포넌트가 어떤 부품을 가지고 있는지를 매트릭스로 정리해 봤다.
| 부품 / 컴포넌트 | N1 | N2 | N3 | N4 | N5 | N6 | N7 | N8 |
|---|---|---|---|---|---|---|---|---|
| 라우팅 컨테이너 (이동) | ✅ | ✅ | ✅ | ✅ | ||||
| 페어 모션 | ✅ | ✅ | ✅ | |||||
| 직사각형 카드 이미지 | ✅ | ✅ | ||||||
| 원형(소) 카드 이미지 | ✅ | |||||||
| 우측 Chevron | ✅ | ✅ | ✅ | ✅ | ||||
| Action 버튼 | ✅ | (분기) | ✅ | |||||
| 비율(%) 표시 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ||
| 할인액 표시 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
| 우측 적용가 박스 | ✅ | |||||||
| 데스크톱 분기 | ✅ |
이 매트릭스로 어림 짝-유사도(부품 일치 비율)를 매겨 보면, 가장 닮은 짝은 다음과 같다.
| 짝 | 유사도 | 차이 |
|---|---|---|
| N2 ↔ N5 | ★★★★★ | 컨테이너 분기 · 텍스트 크기 · 패딩 정도 |
| N7 ↔ N8 | ★★★★ | Action 버튼 유무 · 데스크톱 분기 유무 |
| N5 ↔ N6 | ★★★ | 모션 강도(간단 vs shimmer) |
| N1 ↔ N8 | ★★ | Action 버튼은 공유, 시각 다름 |
| N3 ↔ N4 | ★ | 같은 흐름의 한 줄짜리지만 본질 다름 |
보였지만 안 합쳤다 — 그리고 그게 맞았다
여기까지 보면 1·2 순위 짝(N2↔N5, N7↔N8)은 합쳐도 깨끗해 보였다. 사실 합쳤어야 했나 싶기도 했다. 그런데 시간이 지난 지금, 하나도 합치지 않았다. 처음에는 “시간이 없어서” 라고 생각했는데, 한참 뒤에 다시 보니 이유는 세 개였다.
1) 합칠 시간을 따로 빼기 어렵다
새 화면, 새 분기, 새 실험은 계속 들어온다. 합치는 작업은 “지금 안 해도 죽지 않는” 일이라 항상 뒤로 밀린다. 한 번 밀리고 나면, 그 사이에 한쪽에만 작은 분기 prop 이 또 들어가서 합치는 비용은 더 커져 있다. 밀린 리팩토링은, 다음 주가 되면 같은 일이 아니다.
2) 합쳐도, 한쪽만 바뀌면 다시 갈라야 한다
N2 ↔ N5 를 합쳤다고 치자. 그 다음 분기 중 하나만 디자인 정책이 살짝 바뀌면 — 모션이 바뀌거나, 버튼 라벨이 한쪽만 바뀌거나, 패딩이 어긋나면 — variant prop 이 둘에서 셋, 셋에서 넷으로 늘어나거나, 결국 다시 두 개로 갈라야 한다.
합치는 비용은 한 번이 아니라 두 번 든다 — 합칠 때 한 번, 갈라낼 때 또 한 번.
그리고 보통 갈라낼 때 더 비싸다. 이미 합쳐진 props 와 분기 로직을 다시 풀어 두 컴포넌트로 되돌리는 일은, 처음부터 따로 두는 것보다 항상 손이 더 간다.
3) 호출 지점이 작아 합친 효과도 작다
호출 지점이 5개다. 8 → 6 으로 줄어들어도, 호출 쪽 코드가 짧아지는 효과는 본질적으로 작다. 공유의 ROI 는 호출 지점 수에 비례한다. N=1 인 영역에서 합친다고 해 봐야 패키지 안의 이름 두 개가 하나로 되는 정도다 — 호출하는 사람 입장에서 달라지는 건 거의 없다.
그래서, UI 추상화는 쓸데 없는 일이었나
며칠 전 이 패키지를 다시 보면서 — “UI 추상화는 정말 쓸데 없다” 는 생각이 잠깐 들었다. 데이터를 한 번 더 보고 나니, 정확한 표현은 이쪽이었다.
쓸데 없는 게 아니라, 보통 너무 일찍 한다. 그리고 UI 가 아니라 동작을 추상화했어야 했다.
이 패키지가 6개월간 진짜로 절약해 준 건 — 데이터 (2) 의 “공유된 것” 표 — 컴포넌트가 아니라 동작·로깅·약속이었다.
- 라우팅 + 클릭 트래킹 컨테이너
- 임프레션 / 클릭 로깅 훅
- 식별 코드 → 이름·이미지 매핑
- 금액 포매팅 함수
- 렌더 가드
이 자산들은 6개월간 화면이 변해도 같이 변하지 않았다. 새 화면이 추가될 때마다 useMoveTo* 와 useTracking* 가 그대로 다시 쓰였다. 이쪽은 추상화가 잘 된 사례다.
반대로 합쳐 둔 UI 는 — 합쳤다면 — 6개월 동안 분기 props 만 늘어 있었을 가능성이 더 크다. 결과적으로 UI 는 합치지 않은 것이 옳았다.
추상화 전 체크리스트 (개정판)
다음에 같은 결의 영역을 만나면, 이 표를 한 장 떠올려 보려 한다.
| 질문 | “예” 면 | “아니오” 면 |
|---|---|---|
| 정말로 동일한 한 가지 모양이 3 화면 이상에서 쓰이는가? | UI 컴포넌트로 끌어 올린다 | UI 는 화면에 둔다 |
| 디자인 정책이 향후 6 ~ 12 개월 한 가지 모양으로 굳어 있을 가능성이 높은가? | 컴포넌트로 둔다 | 동작만 분리, UI 는 화면에 |
| 진짜 공유 가치가 뷰가 아니라 동작·로깅인가? | hook 으로 분리한다 | 컴포넌트 통째로 공유 |
| 합친 뒤 한쪽만 변할 가능성이 있는가? | 합치지 않는다 (갈라낼 비용이 더 비싸다) | 합쳐도 안전하다 |
| 호출 지점이 3개 이상인가? | 합친 효과가 있다 | ROI 가 거의 없다 |
| 변경 시 패키지만 수정하고 앱은 안 건드릴 만큼 격리 되는가? | 외부 패키지로 둘 가치가 있다 | 같은 워크스페이스에 둔다 |
마치며 — 한 줄
변하는 건 화면에 두고, 변하지 않는 건 패키지에 둔다.
N = 1 인 컴포넌트는 합치지 말고(만들지도 말고), 그 안의 동작만 hook 으로 분리한다.
UI 를 추상화하지 않는 결정은, 추상화 능력의 부재가 아니다.
오히려 그 결정 옆에 useMoveTo..., useTracking..., useFormat... 같은 작은 훅을 정확히 빼 두었느냐 — 그게 진짜 능력이다.
이번에 배운 한 줄을 적어 두자.
UI 추상화는 쓸데 없는 게 아니라, 보통 너무 일찍 한다. 그리고 한 번 늦게 시작해도, 보통 늦은 게 아니다.