---
name: Component Design
description: 컴포넌트 설계 원칙. 스타일 표현 규칙(Tailwind), 인라인 스타일 금지, 프로젝트 공용 컴포넌트 우선, 시맨틱 HTML, boolean props 지양, 컴포넌트 중첩 금지.
type: coding-standard
category: component-design
---

> 출처: 팀 공통 원칙 문서 `principles/` 2026-07-03 발췌. 원칙 개정은 원본(팀 공통 문서) 먼저, 이 사본은 따라간다. P-004~P-007은 원본(styled-components 기준)을 본 보일러플레이트 스택(Tailwind + shadcn/ui)에 맞게 번역했다.

# 컴포넌트 설계

## P-004 스타일 선언과 로직 분리

반복되는 긴 className 조합을 JSX에 그대로 늘어놓지 않는다. 변형이 있으면 `cva`(class-variance-authority)로, 단순 반복이면 상수/컴포넌트로 추출해 스타일 선언과 렌더 로직을 분리한다.

```tsx
// ❌ 같은 40자 className이 파일 곳곳에 복붙
<li className="flex items-center gap-2 rounded-md border px-3 py-2 text-sm hover:bg-muted">...</li>
<li className="flex items-center gap-2 rounded-md border px-3 py-2 text-sm hover:bg-muted">...</li>

// ✅ 스타일 선언을 한 곳으로
const listItemClass = 'flex items-center gap-2 rounded-md border px-3 py-2 text-sm hover:bg-muted';
<li className={listItemClass}>...</li>

// ✅ 변형이 있으면 cva
const badge = cva('inline-flex items-center rounded-full px-2 py-0.5 text-xs', {
  variants: { tone: { neutral: 'bg-muted', danger: 'bg-destructive text-white' } },
});
```

## P-005 인라인 스타일 금지

`style={{ ... }}` 인라인 스타일 금지. Tailwind className으로 적는다. 동적 값이 정말 필요하면 CSS 변수로 넘긴다.

```tsx
// ❌
<div style={{ padding: 16, backgroundColor: 'red' }}>

// ✅
<div className="p-4 bg-destructive">

// ✅ 동적 값은 CSS 변수
<div className="h-[var(--chart-height)]" style={{ '--chart-height': `${height}px` } as CSSProperties}>
```

## P-006 자식 CSS 선택자 금지

부모에서 자식 요소를 선택자로 스타일링하지 않는다 (Tailwind arbitrary variant `[&>li]:p-4`, 전역 CSS `ul > li` 모두). 각 요소가 자기 스타일을 직접 소유해야 컴포넌트를 옮겨도 스타일이 따라간다.

```tsx
// ❌ 부모가 자식을 선택자로 스타일링
<ul className="[&>li]:p-4 [&>li]:border-b">
  <li>...</li>
</ul>

// ✅ 자식이 자기 스타일 소유
<ul>
  <li className="p-4 border-b">...</li>
</ul>
```

## P-007 공용 컴포넌트 우선 — shadcn 자동 추가 강제 [필수]

UI 컴포넌트가 필요하면 **엄격히 이 순서**를 따른다. 건너뛰고 자작하는 것을 금지한다.

1. **`src/shared/ui/`에 이미 있으면** 그걸 쓴다.
2. **없고 shadcn/ui 카탈로그(https://ui.shadcn.com/docs/components)에 있으면** → **반드시 `npx shadcn@latest add <name>`로 가져온다.** 버튼·인풋·다이얼로그·테이블·탭·토스트·드롭다운 등 shadcn이 제공하는 것을 **손으로 다시 만들지 않는다.**
3. **shadcn에도 없을 때만** 직접 구현한다(그마저도 shadcn 컴포넌트를 조합해 만들 수 있으면 조합).

```tsx
// ❌ shadcn에 있는 컴포넌트(Accordion 등)를 직접 구현
function MyAccordion() {
  const [open, setOpen] = useState(false); /* ... 자작 ... */
}

// ✅ shadcn에서 가져온다 (없으면 즉시 add)
//    $ npx shadcn@latest add accordion
import {
  Accordion,
  AccordionItem,
  AccordionTrigger,
  AccordionContent,
} from '@/shared/ui/accordion';
```

- 이 레포에서 shadcn 컴포넌트는 **`@/shared/ui/*`** 에 산다(FSD). `components.json`의 alias가 그렇게 잡혀 있다.
- **비개발자 사용자는 이 절차를 모른다.** 사용자가 "탭으로 나눠줘", "달력 넣어줘"처럼 말하면, Claude가 알아서 `shadcn add tabs`/`add calendar`를 돌려 가져온 뒤 화면을 만든다 — 사용자에게 CLI 명령을 시키지 않는다.
- `shadcn add`가 실패하면(대개 pnpm 미설치) `npm install -g pnpm` 후 재시도.
- 목적: 같은 종류가 두 모양으로 갈라지지 않게 하고, 이미 검증된 접근성·스타일을 재사용한다.

## P-008 시맨틱 HTML 사용

시각적 외관이 아닌 의미에 맞는 HTML 요소를 사용한다.

- 액션 → `<button>`
- 네비게이션 → `<a>` (우클릭 "새 탭에서 열기" 지원)
- 폼 입력 → `<input>`, `<select>`, `<textarea>`
- 컨테이너 → `<section>`, `<article>`, `<nav>`, `<aside>`

```tsx
// ❌ div에 onClick — 키보드 탐색, 스크린 리더 모두 불가
<div className="cursor-pointer" onClick={submit}>확인</div>

// ✅ 시맨틱 요소 사용
<button type="button" className="cursor-pointer" onClick={submit}>확인</button>
```

## P-009 boolean props 대신 composition

컴포넌트에 boolean props가 늘어나면 variant 컴포넌트 분리 또는 compound component 패턴으로 전환한다.

```tsx
// ❌ boolean props 누적 — 내부 분기가 폭발적으로 늘어남
<Modal isEdit isLoading hasFooter isFullScreen />

// ✅ variant 컴포넌트 분리
<EditModal />
<FullScreenModal />

// ✅ compound component 패턴
<Modal>
  <Modal.Header />
  <Modal.Body />
  <Modal.Footer />
</Modal>
```

## P-010 컴포넌트 내부에 컴포넌트 선언 금지

함수 컴포넌트 내부에서 다른 컴포넌트를 선언하지 않는다.
매 렌더마다 새 컴포넌트가 생성되어 불필요한 언마운트/리마운트가 발생한다.

```tsx
// ❌ Parent 렌더링마다 Child가 새로 생성됨
function Parent() {
  const Child = () => <div>hello</div>; // 매 렌더마다 새 컴포넌트
  return <Child />;
}

// ✅ 모듈 레벨에 선언
function Child() {
  return <div>hello</div>;
}
function Parent() {
  return <Child />;
}
```
