shadcn/ui 대규모 도입 시 파일럿 1페이지 검증 패턴 + 모바일 44px 터치 영역 함정¶
맥락¶
기존 프로젝트에 shadcn/ui를 전면 도입하는 마이그레이션 프로젝트(수십~수백 페이지 규모)의 초기 단계에서, 기본 컴포넌트(Button, Input, Card 등)를 설치한 직후 "어느 페이지부터 적용할지·얼마나 넓게 손댈지"를 판단해야 한다. 범위를 너무 넓히면 회귀 위험이 폭증하고, 너무 좁히면 검증이 의미 없어진다.
또한 shadcn Button의 기본 size 프리셋이 모바일 터치 영역 기준(44px)을 미달하는 숨은 함정이 있다. 기존 코드가 명시했던 min-h-[44px]가 교체 과정에서 사라지면 조용한 접근성 회귀가 발생한다.
이 문서는 다음 두 가지를 함께 다룬다: 1. 파일럿 페이지 선정·검증 워크플로우 2. shadcn Button size="lg" = h-10(40px) 함정과 오버라이드 패턴
가이드 ① 파일럿 1페이지 검증 워크플로우¶
1.1 파일럿 페이지 선정 기준 (4가지 필수)¶
파일럿은 다음을 모두 충족해야 한다:
| 기준 | 이유 |
|---|---|
| 라인 수 300줄 이하 | 변경 범위 파악이 빠르고 diff 리뷰가 쉬움 |
| 폼·CRUD 없음 (읽기 전용) | react-hook-form·zod·Controller 복합 이슈를 초기에 배제 |
| 교체 대상 요소 5개 이하 (button·input·select 합계) | 한 번에 하나씩 교체하며 결과 확인 가능 |
| 반응형·motion 사용 | 기존 애니메이션·훅이 보존되는지 실전 검증 |
자산관리 프로젝트는 LogsPage.tsx(273줄, button 3·input 1·폼 없음·framer-motion 사용)가 이 기준을 모두 충족하여 파일럿으로 선정됐다. 결과적으로 사전 분석 함정 11건 중 실제로 발생한 것은 0건이었다. 파일럿이 작은 덕분에 모든 함정을 미리 걸러낼 수 있었다.
1.2 파일럿 원칙 3개 (엄수)¶
- 파일럿 외 페이지 일체 수정 금지. "SkeletonTable을 내부까지 리뉴얼하면 모든 페이지에 자동 적용되겠네" 같은 유혹은 전부 거부. SkeletonTable은 다른 페이지 N곳에서 쓰이므로 회귀 영향이 파일럿 범위를 초과한다.
- 공용 컴포넌트 파일 자체 수정 금지.
src/components/common/*는 이번 Phase 범위 외. 파일럿 페이지 내부에서<SkeletonTable />로 사용만 하고, 파일 내부 로직은 건드리지 않는다. - 하드코딩 색상 교체는 교체 대상 요소에만 한정. 파일럿 페이지에
bg-blue-600이 20곳 있어도, 이번에 교체하는 것은 button·input 4곳뿐이면 나머지 16곳은 그대로 둔다. 나머지는 색상 일괄 교체 Phase에서 처리.
1.3 파일럿 성공 기준 (6가지)¶
- 파일럿 페이지의 교체 대상 요소가 새 컴포넌트로 100% 교체
-
npm run buildTypeScript 에러 0건 - 기존 framer-motion variants·반응형 분기 전부 보존
- 파일럿 외 페이지·훅·서비스·공용 컴포넌트 수정 0건 (git diff로 검증)
- import 경로가 새 alias(
@/components/ui/*)로 동작 - 기존
<label htmlFor>연결·aria-*·type="submit"같은 접근성 속성 보존
이 중 하나라도 미충족이면 파일럿이 실패다. 파일럿 실패 = git revert 후 범위 재조정.
가이드 ② shadcn Button 44px 터치 영역 함정¶
2.1 문제¶
shadcn CLI가 설치하는 src/components/ui/button.tsx의 size variants는 다음과 같다 (2026-04 기준 new-york 스타일):
size: {
default: "h-9 px-4 py-2", // 36px
sm: "h-8 rounded-md px-3", // 32px
lg: "h-10 rounded-md px-6", // 40px ← 44px 미달
icon: "h-9 w-9", // 36px
}
모든 size가 44px 미달이다. iOS HIG·Material Design·WCAG 2.5.5(Target Size)가 권장하는 44×44dp 최소 터치 영역을 기본으로 충족하지 않는다. 데스크탑 전용 앱이면 무시할 수 있지만, 모바일 대응 프로젝트에서는 조용한 접근성 회귀가 발생한다.
특히 기존 코드가 <button className="... min-h-[44px]"> 로 명시했던 경우, shadcn Button 교체 과정에서 이 className이 사라지면 교체 전후로 터치 영역이 축소된다. evaluator 없이는 빌드·린트가 전부 통과하므로 조용히 넘어간다.
2.2 해결 패턴 (3가지 옵션)¶
옵션 A — className 오버라이드 (권장 · 국소 수정)¶
- 장점: 해당 사용처만 수정. 다른 Button 사용처·다른 Phase에 영향 없음.
- 단점: 44px이 필요한 Button마다 반복 작성 필요.
- 언제 쓰나: 파일럿 Phase, 또는 "모바일 접근이 가능한 버튼"이 소수일 때.
옵션 B — button.tsx 자체 수정 (전역 변경)¶
size: {
default: "h-11 px-4 py-2", // 44px
sm: "h-9 rounded-md px-3", // 36px (여전히 미달 — 44px 필요한 경우 lg 사용)
lg: "h-11 rounded-md px-6", // 44px
icon: "h-11 w-11", // 44px
}
- 장점: 모든 Button 사용처가 자동 준수.
- 단점: 파일럿 원칙 위반. 다른 Phase에 영향. shadcn CLI가 재설치 시 되돌아감.
- 언제 쓰나: Phase 9 마지막(9-H 같은 마무리 단계)에서 프로젝트 컨벤션으로 고정할 때.
옵션 C — 커스텀 variant 추가¶
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3",
lg: "h-10 rounded-md px-6",
mobile: "h-11 rounded-md px-6 min-h-[44px]", // 신규
icon: "h-9 w-9",
}
- 장점: 전역 변경 없이 "모바일 대응" 의도를 이름으로 표현.
- 단점: shadcn 표준에서 벗어남. CLI 업데이트 시 충돌 위험.
- 언제 쓰나: 프로젝트 고유 variant 컨벤션을 세우고 싶을 때.
2.3 이 프로젝트의 선택¶
옵션 A 채택. 근거: - 파일럿 1페이지 원칙 유지 — button.tsx를 건드리면 아직 shadcn을 적용하지 않은 페이지들도 암묵적으로 영향을 받게 되어, 향후 Phase에서 "왜 갑자기 버튼이 커졌지?" 하는 혼란을 피할 수 있다. - 파일럿 LogsPage에서 44px 필요한 버튼이 2개(묶음 지급·검색)로 적어 className 반복 비용이 낮다. - Phase 9-H(마무리)에 와서 전체 통계를 보고 옵션 B 또는 C로 승격할지 결정.
2.4 buttonVariants 기반 컴포넌트의 2차 충돌 함정 (9-C에서 발견 — 재발 1)¶
shadcn AlertDialogAction, AlertDialogCancel처럼 내부적으로 buttonVariants()를 적용하는 컴포넌트에서는 옵션 A의 단순 min-h-[44px] 추가가 의도 불명확 상태를 만든다.
문제 재현:
// ❌ 의도 불명확 — h-9(36px)와 min-h-[44px]이 공존
<AlertDialogAction className="min-h-[44px]">
삭제
</AlertDialogAction>
내부적으로 buttonVariants({ size: "default" })가 h-9(36px)를 주입하므로, CSS 결과는 height: 36px; min-height: 44px;가 된다. 브라우저는 min-height가 우선하여 44px를 렌더링하지만, height와 min-height의 충돌이 명시적 의도를 가려 evaluator 리뷰에서 "의도 불명확"으로 지적된다. 또한 Tailwind 버전 변경이나 button.tsx 수정 시 silent regression 위험이 있다.
해결: h-auto로 height 충돌 제거
cn() + tailwind-merge의 병합 규칙을 활용하여 className 후행 적용 시 h-9를 h-auto로 대체한다.
// ✅ h-auto로 h-9 대체 → min-h-[44px]이 하한 역할을 명확히 담당
<AlertDialogAction className="h-auto min-h-[44px] py-2">
삭제
</AlertDialogAction>
<AlertDialogCancel className="h-auto min-h-[44px] py-2">
취소
</AlertDialogCancel>
tailwind-merge가 h-9 → h-auto로 자동 대체하고, min-h-[44px]가 명시적으로 44px 하한을 담당하며, py-2가 수직 여백을 보강한다. 의도가 코드에서 명확히 읽힌다.
적용 범위: AlertDialogAction, AlertDialogCancel, buttonVariants를 내부에서 직접 호출하는 모든 shadcn 래퍼 컴포넌트. 일반 <Button> className 오버라이드에는 해당 없음(외부 오버라이드라 tailwind-merge가 정상 처리).
왜 옵션 A의 보강이지 신규 옵션이 아닌가:
이 함정은 옵션 A의 동일 원칙(국소 className 오버라이드)을 적용하되, buttonVariants 래퍼 컴포넌트라는 특수 케이스에서 h-9 충돌이 추가로 발생한 것이다. 옵션 B/C는 여전히 파일럿 원칙을 벗어나는 전역 변경이므로 채택하지 않는다.
- 실제 적용 커밋: AssetManagement/Frontend
cf9a2ef(Phase 9-C) - 적용 파일:
src/components/common/ConfirmDialog/ConfirmDialog.tsx - evaluator 1차 리뷰: "min-h-[44px]만 추가 시 h-9와 충돌하여 의도 불명확" WARNING 지적 → h-auto 추가로 해결
- 재발 단계: SOFT → MEDIUM (개발_규칙_FE.md 9.6에 등재됨)
2.5 buttonVariants 기반 — Pagination 3차 적용 (9-D1 실증)¶
shadcn PaginationPrevious, PaginationNext, PaginationLink 3종은 내부적으로 buttonVariants()를 호출한다. 2.4의 AlertDialogAction과 동일한 충돌 패턴이 발생한다.
해결: h-auto min-h-[44px] py-2 3개 클래스를 className에 적용
<PaginationPrevious
className="h-auto min-h-[44px] py-2"
onClick={...}
/>
<PaginationNext
className="h-auto min-h-[44px] py-2"
onClick={...}
/>
<PaginationLink
className="h-auto min-h-[44px] py-2"
isActive={currentPage === p}
onClick={...}
>
{p}
</PaginationLink>
h-auto: buttonVariants 기본h-9(36px) 제거min-h-[44px]: 44px 하한 명시py-2: 수직 여백 보강
적용 커밋: e827b9a (Phase 9-D1)
2.6 shadcn 함수명 충돌 — alias import 패턴¶
문제¶
기존 프로젝트에 이미 Pagination이라는 이름의 공용 컴포넌트가 존재할 때, shadcn pagination.tsx를 설치하면 같은 이름의 export가 두 곳에 생긴다.
// src/components/ui/pagination.tsx (shadcn 설치 후)
export function Pagination(...) { ... } // shadcn Pagination
// src/components/common/Pagination/Pagination.tsx (기존)
export default Pagination; // 기존 래퍼
기존 래퍼 내부에서 shadcn을 import하려 하면 이름 충돌이 발생한다:
// ❌ 컴파일 에러 — Identifier 'Pagination' has already been declared
import { Pagination } from "@/components/ui/pagination";
해결: alias import¶
// ✅ shadcn 컴포넌트에 alias 적용
import {
Pagination as ShadcnPagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
// 기존 래퍼 함수명·export 유지
function Pagination({ pagination, onPageChange, compact }: PaginationProps) {
return (
<ShadcnPagination>
...
</ShadcnPagination>
);
}
export default Pagination; // 외부 API 완전 보존
API 완전 보존 검증¶
결과가 0건이면 기존 사용처(src/pages/ 내 12개 파일)가 일체 수정되지 않은 것. 래퍼의 Props 인터페이스(pagination, onPageChange, compact?)가 변경되지 않는 한 외부에서 import Pagination from "@/components/common/Pagination/Pagination" 형태로 쓰는 코드는 전혀 건드릴 필요가 없다.
적용 커밋: e827b9a (Phase 9-D1)
왜 중요한가¶
파일럿 워크플로우가 중요한 이유¶
대규모 UI 마이그레이션은 "한 번에 끝내고 싶은 유혹"과의 싸움이다. "어차피 다 바꿀 건데 지금 같이 하지"는 함정이다. 한 파일만 수정했을 때 발생할 수 있는 에러는 하나의 파일 범위에서 해결 가능하지만, 50개 파일을 한 번에 수정하면 에러의 출처를 추적하는 데만 몇 시간이 걸린다. 파일럿 원칙 = 디버깅 비용의 선불 지불.
44px 함정이 중요한 이유¶
shadcn은 "예쁘고 세련된 기본값"을 추구하지만, 그 기본값이 접근성 최소 기준을 충족하지는 않는다. 빌드 에러·린트 경고·TypeScript 에러 어느 것도 이 문제를 잡아주지 못한다. 조용히 넘어가는 회귀는 리뷰어가 명시적으로 확인해야만 발견된다. 이 문서는 리뷰 체크리스트에 영구 항목으로 추가되어야 한다.
적용 시기¶
파일럿 워크플로우¶
- 적용: 기존 프로젝트에 shadcn/ui(또는 다른 UI 라이브러리)를 전면 도입할 때 첫 번째 페이지 작업.
- 적용: 대형 리팩터링에서 "새 패턴의 실전 검증"이 필요할 때.
- 적용: 신규 기능을 도입할 때 "한 페이지로 증명 후 확장".
- 비적용: 새 프로젝트를 백지에서 시작할 때 (파일럿 자체가 무의미 — 처음부터 전부 새 패턴).
- 비적용: 단일 컴포넌트 버그 수정 같은 소규모 작업.
44px 함정¶
- 적용: 모바일 반응형을 지원하는 모든 shadcn 프로젝트.
- 적용: iOS/Android 웹앱, PWA, 사내 모바일 도구.
- 비적용: 완전 데스크탑 전용 도구 (사내 관리자 화면이 1920px 이상 고정일 때). 단, 이 경우에도 나중에 "태블릿에서도 써야겠다"는 요청이 올 가능성이 있으므로 선제 대응 권장.
관련 자료¶
- 실제 적용 커밋: AssetManagement/Frontend
40bf5ee(Phase 9-B),cf9a2ef(Phase 9-C — 2.4 섹션 추가 계기) - 선행 기록: 2026-04-09-tailwind-v4-shadcn-theme-inline-도입-패턴 — 9-A OKLCH·@theme inline 패턴
- 후속 기록: 2026-04-10-shadcn-dialog-ispending-닫기가드-3중-패턴 — 9-C에서 발견된 isPending 가드 패턴
- 후속 기록: 2026-04-10-shadcn-점진적-모달-교체-animatepresence-래퍼-보존-패턴 — 9-C에서 발견된 점진 마이그레이션 패턴
- 관련 ADR: ADR-007
- 작업지시서: Planning/작업_지시서_Phase9B_기본컴포넌트_도입, Planning/작업_지시서_Phase9C_Dialog_모달_교체
- WCAG 2.5.5 Target Size: https://www.w3.org/WAI/WCAG21/Understanding/target-size.html
- 9-D1 커밋 e827b9a — 2.5 Pagination 3차 적용 + 2.6 alias import 패턴 추가. Planning/작업_지시서_Phase9D1_Table_Tabs_Pagination