유지보수 가능한 Tailwind와 shadcn/ui · 튜토리얼

유지보수 가능한 Tailwind와 shadcn/ui 기능 만들기

기존 primitive와 semantic token을 사용해 상태 안내 panel을 만드는 학습 과정을 안내합니다.

검증일 근거 자료

학습 결과

기존 CardButton을 조합해 theme 변경과 제품 문구 변경이 서로 다른 파일에서 처리되는 작은 기능을 완성함

이 튜토리얼은 jongminchung 저장소에서 읽기 전용 상태 안내 panel을 만든다고 가정한다. 실제 제품 동작보다 변경 owner를 나누는 감각을 익히는 것이 목표다.

준비

  • 저장소 root에서 작업함
  • apps/web/components.jsonDESIGN_SYSTEM.md를 먼저 확인함
  • 새 registry component를 설치하지 않고 기존 CardButton을 사용함
  • 예시는 Server Component로 유지함
  1. 현재 component API를 확인함

    공용 primitive의 실제 export와 variant를 source에서 확인함

    rg -n "export.*Card|buttonVariants" packages/ui/src/components
    pnpm exec shadcn info --json -c apps/web

    이 단계의 목적은 기억에 의존해 새 wrapper나 존재하지 않는 variant를 만들지 않는 데 있음

  2. 제품 composition을 앱에 만듦

    apps/web이 문구와 상태를 소유하고 공용 primitive는 범용 외형과 접근성을 유지하게 함

    import { buttonVariants } from "@jongminchung/ui/components/button";
    import {
      Card,
      CardDescription,
      CardFooter,
      CardHeader,
      CardTitle,
    } from "@jongminchung/ui/components/card";
    import Link from "next/link";
    import type { ComponentProps } from "react";
    
    export function DocumentationHealthPanel({
      className,
      ...props
    }: ComponentProps<typeof Card>) {
      return (
        <Card {...props} className={className}>
          <CardHeader>
            <CardTitle>문서 계약이 최신임</CardTitle>
            <CardDescription>
              locale pair와 내부 링크가 마지막 build에서 검증됨
            </CardDescription>
          </CardHeader>
          <CardFooter>
            <Link
              className={buttonVariants({ variant: "outline", size: "sm" })}
              href="/ko/series"
            >
              시리즈 확인
            </Link>
          </CardFooter>
        </Card>
      );
    }

    composition은 제품 문구를 소유하고 CardHeader·CardFooter는 내부 spacing을 소유함. button의 focus·size·색상은 primitive가 소유함

  3. semantic token으로 시각 의미를 표현함

    상태 설명에는 text-muted-foreground, 주 동작에는 기존 button variant를 사용함. text-gray-500, bg-white dark:bg-*text-white를 추가하지 않음

    새 색이 필요해 보여도 먼저 기존 역할로 의미를 표현할 수 있는지 확인함. 제품 전체에서 반복되는 새 역할일 때만 light·dark provider, foreground pair와 adapter를 함께 추가함

  4. page는 배치만 결정함

    호출 위치에서는 panel의 외부 너비와 grid 위치만 지정함

    <DocumentationHealthPanel className="max-w-xl lg:col-span-2" />

    실제 component가 className을 받는다면 내부 색상이나 padding을 호출부에서 덮어쓰지 않도록 API 설명과 review 기준을 유지함

  5. 가까운 검사부터 실행함

    pnpm run fmt:check
    pnpm --filter @jongminchung/web run typecheck
    pnpm --filter @jongminchung/web run test
    pnpm --filter @jongminchung/web run build

    화면이 달라졌다면 light·dark와 mobile·wide viewport를 확인함. 공용 primitive 자체를 수정했다면 UI package test와 다른 consumer build까지 범위를 넓힘

완료 상태를 확인함

  • 제품 문구와 흐름이 apps/web에 남아 있음
  • 공용 primitive에 제품 이름의 variant가 추가되지 않음
  • raw palette와 동적 class fragment가 없음
  • styling만을 이유로 "use client"가 추가되지 않음
  • formatter, typecheck, 관련 test와 build가 통과함

다음 학습

이미 override와 raw color가 퍼진 화면을 정리해야 한다면 기존 화면 감사 방법으로 이동함