← 목록으로
AI-assisted content
Claude Code 스킬로 React 컨벤션을 구조적으로 강제하기

들어가며

AI 코드 어시스턴트에게 "React 컴포넌트 만들어줘"라고 말하면, 코드는 나옵니다. 하지만 내 팀의 컨벤션대로 나올까요? props 네이밍, 폴더 구조, 상태관리 패턴, 테스트 방식 — 매번 같은 피드백을 반복하고 있다면, 문제는 AI가 아니라 컨텍스트의 부재입니다.

Claude Code에는 이 문제를 해결하는 기능이 있습니다. 스킬(Skill) 시스템입니다. 스킬은 .claude/skills/ 디렉토리에 마크다운 파일로 정의하는 규칙 세트로, Claude가 코드를 작성할 때 자동으로 참조합니다.

이 글에서는 직접 만든 load28-react 스킬의 설계 의도, 구조, 사용법, 그리고 확장 방법을 다룹니다.

왜 만들었는가

React 프로젝트에서 반복되는 문제가 있었습니다.

  1. 컴포넌트 내부에 컴포넌트를 정의해서 매 렌더마다 state가 소실되는 버그
  2. useEffect에서 파생 상태를 계산해서 불필요한 이중 렌더가 발생하는 코드
  3. props를 state에 복사해서 동기화가 어긋나는 패턴
  4. any 타입이 전파되어 TypeScript를 쓰는 의미가 없어지는 상황

처음에는 79개의 규칙을 문서화하고, Claude에게 "이 규칙을 확인해"라고 지시하는 방식을 시도했습니다. 하지만 문제가 있었습니다. 규칙을 암기하고 매칭하는 방식은 근본적으로 누락에 취약합니다. 사람이든 AI든 79개 규칙을 머릿속에 유지하면서 코드를 작성하는 건 비현실적입니다.

그래서 발상을 전환했습니다. "규칙을 확인하는 게 아니라, 슬롯이 비어있으면 코드 자체를 생성할 수 없는 구조를 만들자."

핵심 원리: 규칙은 슬롯이다

load28-react의 가장 큰 특징은 슬롯 기반 설계 프로토콜(Slot-Based Design Protocol) 입니다.

❌ 기존 (규칙 = 지식):
   79개 규칙 암기 → 리액트 코드 구상 → 규칙 매칭 → 누락 발견 → 수정

✅ 현재 (규칙 = 슬롯):
   스키마 로드 → 빈 슬롯 채우기 = 설계 완료
   → 채워진 슬롯 → 코드 생성 (기계적 변환)
   → Grammar Scan → 제출

TypeScript 컴파일러가 타입 오류를 "검사"하는 게 아니라 컴파일 자체가 안 되는 것처럼, 규칙을 "확인"하는 게 아니라 슬롯이 비어있으면 코드 생성 자체가 불가능한 구조입니다.

  • props 슬롯이 7칸뿐이므로 8개 props를 가질 수 없다
  • cleanup 슬롯이 필수이므로 useEffect cleanup을 잊을 수 없다
  • 이벤트 슬롯이 on___/handle___ 형식이므로 네이밍 규칙을 어길 수 없다
  • type_strategy 슬롯을 선택해야 하므로 discriminated union 판단을 건너뛸 수 없다

핵심은 이것입니다: 리액트 코드를 먼저 생각하고 규칙을 매칭하는 것이 아니라, 스키마의 빈 슬롯을 채워서 설계하고, 채워진 슬롯에서 코드를 생성한다.

스킬의 구조

load28-react 스킬은 3단계 프로토콜, 9개 스키마, Grammar 시스템, 6개 카테고리의 규칙, 그리고 레퍼런스 코드로 구성됩니다.

.claude/skills/load28-react/
├── SKILL.md                    # 슬롯 기반 프로토콜 정의
├── ADDING_RULES.md             # 규칙 추가 프로세스 (다층 검증)
├── architecture.md             # 아키텍처 규칙 (A-01~10)
├── component-patterns.md       # 컴포넌트 패턴 (C-01~15)
├── naming-conventions.md       # 네이밍 규칙 (N-01~08)
├── state-and-data.md           # 상태관리 & 데이터 흐름 (S-01~18)
├── performance.md              # 성능 최적화 (P-01~14)
├── testing-a11y.md             # 테스트 & 접근성 & 타입안전 (T-01~15)
├── schemas/                    # 슬롯 기반 스키마
│   ├── _grammar.md             # Grammar 규칙 (Section A + B)
│   ├── component.md            # 컴포넌트 스키마
│   ├── hook.md                 # 커스텀 훅 스키마
│   ├── context.md              # Context + Provider 스키마
│   ├── store.md                # Zustand 스토어 스키마
│   ├── api-layer.md            # API/데이터 레이어 스키마
│   ├── test.md                 # 테스트 파일 스키마
│   ├── type.md                 # 타입 정의 스키마
│   ├── barrel.md               # barrel file 스키마
│   └── module.md               # 피처 모듈 전체 스키마
└── reference-code/             # 패턴별 레퍼런스 구현
    ├── _tags.md                # 태그 정의서
    ├── compound-component--context--generic.md
    ├── http-client--acl--dependency-inversion.md
    └── ... (레퍼런스 파일들)

3단계 프로토콜

SKILL.md가 전체 워크플로우를 정의합니다. Claude는 코드를 작성할 때 반드시 이 순서를 따릅니다.

Phase 1: 스키마 선택 — 작성할 파일의 유형을 식별하고, 대응하는 스키마를 읽습니다. 컴포넌트라면 component.md, 커스텀 훅이라면 hook.md를 로드합니다. 동시에 _grammar.md를 읽고, reference-code/ 디렉토리에서 관련 패턴을 검색합니다.

만들 것스키마
컴포넌트 TSXschemas/component.md
커스텀 훅schemas/hook.md
Context + Providerschemas/context.md
Zustand 스토어schemas/store.md
API/데이터 레이어schemas/api-layer.md
테스트 파일schemas/test.md
타입 정의schemas/type.md
barrel file (index.ts)schemas/barrel.md
피처 모듈 전체schemas/module.md

Phase 2: 슬롯 채우기 + Design Guard — 스키마의 모든 슬롯을 채웁니다. 빈 슬롯이 하나라도 있으면 코드를 생성하지 않습니다. 슬롯 채우기 완료 후, 스키마에 포함된 Design Guard 체크리스트를 확인합니다.

Phase 3: Grammar Scan → 코드 생성 → 보고 — Grammar Section A(Pre-Generation)로 슬롯 수준에서 아키텍처 위반을 먼저 잡고, 채워진 슬롯을 코드로 기계적 변환한 뒤, Grammar Section B(Post-Generation)로 생성된 코드를 스캔합니다.

슬롯 채우기 예시

컴포넌트를 만들 때 채워진 슬롯이 어떤 모습인지 살펴보겠습니다.

## Filled Schema: component

### Identity
- name: ThreadPanel
- file_path: features/thread/ui/ThreadPanel.tsx
- responsibility: 스레드 패널의 메시지 목록과 입력을 렌더링한다
- line_budget: 180/250
- exports: 1

### Props Interface
- interface_name: ThreadPanelProps
- type_strategy: grouped
- slots:
  1. thread: Thread
  2. replies: Reply[]
  3. permissions: ThreadPermissions (그룹)
  4. actions: ThreadPanelActions (그룹)
  5. currentUserId: string
  6. —
  7. —

### Events
| prop        | handler       | 설명         |
|-------------|---------------|-------------|
| onReply     | handleReply   | 답글 제출     |
| onClose     | handleClose   | 패널 닫기     |

### State
| name       | type    | source       | 근거         |
|------------|---------|-------------|-------------|
| isExpanded | boolean | local       | 이 컴포넌트만 사용 |

### Derived
| name         | 계산 원본  | 방식     |
|-------------|----------|---------|
| replyCount  | replies  | 직접 계산 |

### Effects
없음

### Dependencies
- fsd_layer: features
- imports_from: [entities/thread, shared/ui] — 하위 레이어만
- forbidden: [features/message, features/chat, pages/*, widgets/*]
- import_style: barrel import

### 슬롯 완료: 전부 채움 ✓

채워진 슬롯이 곧 설계입니다. 별도의 "설계 문서"가 필요 없습니다. Props 슬롯이 7칸이므로 8개 props를 넣을 수 없고, Events 슬롯이 on/handle 쌍이므로 네이밍을 어길 수 없습니다.

Grammar 시스템

_grammar.md는 코드 생성 전후로 적용되는 규칙을 정의합니다.

Section A (Pre-Generation): 코드를 생성하기 전에 슬롯 값만으로 아키텍처 위반을 잡습니다. 예를 들어 A-01(단방향 의존성)은 Dependencies 슬롯의 imports_from이 FSD 레이어 규칙을 위반하는지 슬롯 수준에서 검증합니다.

Section B (Post-Generation): 생성된 코드를 위에서 아래로 기계적으로 스캔합니다. N-01(PascalCase), S-01(직접 변경 금지), P-04(&& → 삼항) 같은 코드 구문 규칙을 검출합니다.

6개 카테고리, 79개 규칙

각 규칙은 분류(ALWAYS/NEVER), WHY(이유), 코드 예시, 검증 기준으로 구성됩니다. 몇 가지 대표적인 규칙을 살펴보겠습니다.

A-01: 단방향 의존성 (FSD Import Rule)

FSD 레이어 계층 (위→아래 방향만 import 허용):

  app → pages → widgets → features → entities → shared

허용: 상위 → 하위 방향만
금지: shared → features, entities → features, 같은 레이어 슬라이스 간 직접 참조
예외: @x 교차 import (entities 간 명시적 공개 API)

FSD(Feature-Sliced Design) 아키텍처의 핵심 규칙입니다. 양방향 의존성은 순환 참조를 만들고, 하나의 변경이 예측 불가능한 곳에 전파됩니다. @x 교차 import는 같은 레이어 슬라이스 간 참조가 불가피할 때만 entities/[source]/@x/[consumer].ts 형태로 명시적 공개 API를 생성합니다.

S-02: useEffect 내 파생 상태 계산 금지

// ❌ useEffect로 파생 상태 → 이중 렌더
const [items, setItems] = useState<Item[]>([]);
const [total, setTotal] = useState(0);
useEffect(() => {
  setTotal(items.reduce((sum, item) => sum + item.price, 0));
}, [items]);

// ✅ 렌더 중 직접 계산
const total = items.reduce((sum, item) => sum + item.price, 0);

useEffect로 파생 상태를 계산하면 렌더가 두 번 발생합니다. 렌더 본문에서 직접 계산하거나, 비용이 크면 useMemo를 사용합니다. 슬롯 기반에서는 Derived 슬롯에 "직접 계산"인지 "useMemo"인지를 명시하므로, 이 판단을 건너뛸 수 없습니다.

C-01: 컴포넌트 내부 컴포넌트 정의 금지

// ❌ 매 렌더마다 Child가 새 타입으로 생성 → state 소실
function Parent() {
  const Child = () => <input />;
  return <Child />;
}

// ✅ 외부에 정의
const Child = () => <input />;
function Parent() {
  return <Child />;
}

C-15: React 제거 예정/제거된 기능 사용 금지

React 19에서 제거된 API들을 감지합니다. forwardRef(React 19에서는 ref가 일반 prop), defaultProps, propTypes, string ref, findDOMNode, UNSAFE_ lifecycle 등이 대상입니다. 프로젝트의 React 버전을 확인하여 적용합니다.

// ❌ forwardRef (React 19에서 불필요)
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
  return <input ref={ref} {...props} />;
});

// ✅ React 19+: ref를 일반 prop으로 받음
function Input({ ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
  return <input ref={ref} {...props} />;
}

S-18: URL 상태 수동 조작 금지

URL 쿼리파라미터를 URLSearchParamsrouter.push로 직접 조작하면 기존 파라미터가 유실되고, 수동 파싱은 타입 안전성이 없습니다. nuqs를 사용하여 타입 안전한 URL 상태 관리를 강제합니다.

// ❌ URLSearchParams 직접 조작 → 기존 파라미터 유실
const params = new URLSearchParams();
params.set('q', query);
navigate(`?${params.toString()}`); // page, sort 등 전부 유실!

// ✅ nuqs로 URL 상태 관리
const [filters, setFilters] = useQueryStates(searchParams);
setFilters({ q: query, page: 1 }); // 나머지 파라미터 자동 보존

레퍼런스 코드

reference-code/ 디렉토리에는 패턴별 구현 예시가 있습니다. 파일명 자체가 태그 역할을 합니다.

compound-component--context--generic.md
http-client--acl--dependency-inversion.md
controlled--uncontrolled--discriminated-union.md
msw--mock--integration-test.md

레퍼런스 코드도 동일한 슬롯 기반 프로토콜로 작성됩니다. 스키마를 채워서 만든 모범 코드이므로, 규칙 누락 없이 일관된 품질을 유지합니다.

핵심 제약은 도메인 비종속성입니다. workspace, chat, order 같은 프로젝트 특화 용어 대신 Entity, Resource 같은 범용 플레이스홀더를 사용합니다. "이 레퍼런스를 다른 프로젝트에 그대로 복사할 수 있는가?"가 판단 기준입니다.

사용 방법

기본 사용

스킬은 .claude/skills/ 디렉토리에 두는 것만으로 활성화됩니다. Claude Code가 프로젝트를 열면 자동으로 스킬을 인식합니다.

# 레포지토리에 스킬 추가
git clone https://github.com/load28/remote.git
cp -r remote/.claude/skills/load28-react .claude/skills/

이후 Claude에게 React 코드를 요청하면, 슬롯 기반 프로토콜에 따라 스키마를 로드하고, 슬롯을 채우고, Grammar Scan 결과를 함께 보고합니다.

검증 결과 예시

Claude가 컴포넌트를 작성하면 다음과 같은 리포트가 함께 나옵니다.

## load28 React Review

### 채워진 스키마
- 📋 component: ThreadPanel (슬롯 12/12 완료, Design Guard 5/5 확인)
- 📋 hook: useThreadReplies (슬롯 8/8 완료, Design Guard 3/3 확인)

### Grammar Section A (Pre): 전체 통과 (7개 규칙 스캔)
### Grammar Section B (Post): 전체 통과 (34개 규칙 스캔)
- 🔧 P-04: && → 삼항으로 수정 (1건)

✅ 전체 완료

단순히 코드를 생성하는 것이 아니라, 어떤 스키마의 슬롯을 채웠는지, Grammar의 어떤 섹션을 스캔했는지, 수정 사항은 무엇인지를 구조화된 형식으로 보고합니다.

확장 방법

규칙 추가: 다층 검증 프로세스

규칙 추가는 단순히 문서에 한 줄 추가하는 것이 아닙니다. ADDING_RULES.md에 정의된 다층 검증(Multi-Layer Verification) 프로세스를 따릅니다.

핵심 원리는 "규칙이 하나의 위치가 아닌 여러 레이어에 동시 배치될 수 있다"는 것입니다.

레이어시점역할
슬롯 임베딩설계물리적 위반 봉쇄
Grammar A코드 변환 전슬롯값 의미 검증
Design Guard슬롯 완료 후설계 결정 확인
Grammar B코드 변환 후코드 구문 스캔
레퍼런스슬롯 채우기 참고모범 구현 패턴
규칙 문서항상규칙의 WHY

위반 심각도가 높을수록 더 많은 레이어에 배치합니다. 런타임 버그를 유발하는 규칙은 슬롯 임베딩 + Grammar A + Grammar B에 모두 배치하고, 코드 스타일 규칙은 Grammar B에만 배치하는 식입니다.

규칙을 추가하는 절차는 다음과 같습니다.

  1. 중복/충돌 검증 — 전체 카테고리에서 의도가 유사한 기존 규칙 검색
  2. 규칙 코드 부여 — 카테고리별 마지막 번호 + 1 (삭제해도 번호 재사용 금지)
  3. 규칙 문서 작성 — 분류, WHY, 코드 예시, 검증 기준 포함
  4. 배치 위치 결정 — 영향 스키마 매트릭스 작성 후 슬롯/Grammar A/Design Guard/Grammar B 각각에 대해 독립 판단
  5. 레퍼런스 코드 — 구체적 코드 패턴이 필요하면 추가
  6. grep 교차 검증 — 규칙 코드가 모든 영향 스키마에 반영되었는지 확인
## S-19: 새로운 상태관리 규칙

**분류:** NEVER

**WHY:** 이 패턴이 왜 문제인지 한 문장으로 설명합니다.

// ❌ BAD: 안티패턴 코드
// ✅ GOOD: 권장 패턴 코드

**검증:** 이 규칙 위반을 어떻게 탐지하는지 기준을 기술합니다.

레퍼런스 코드 추가

새로운 패턴이 필요하면 reference-code/에 태그 형식의 파일을 추가합니다. 레퍼런스도 동일한 슬롯 기반 프로토콜로 작성해야 합니다.

  1. 레퍼런스에 포함될 구성물 식별 (컴포넌트, 훅, 타입 등)
  2. 각 구성물의 스키마를 읽고 슬롯 채우기 (도메인 비종속 플레이스홀더 사용)
  3. 슬롯 → 코드 변환 + Grammar Scan
  4. _tags.md에서 태그 선택, 태그1--태그2.md 형식으로 저장

다른 기술 스택으로 확장

load28-react는 React에 특화되어 있지만, 같은 구조로 다른 기술 스택의 스킬을 만들 수 있습니다.

.claude/skills/
├── load28-react/       # React 컨벤션
├── load28-nestjs/      # NestJS 백엔드 컨벤션
└── load28-infra/       # 인프라/배포 컨벤션

슬롯 기반 프로토콜(스키마 선택 → 슬롯 채우기 → Grammar Scan)은 기술 스택과 무관하게 재사용할 수 있는 프레임워크입니다. 스키마와 규칙 내용만 바꾸면 됩니다.

마치며

코딩 컨벤션은 문서화만으로는 부족합니다. 문서는 읽히지 않고, 읽혀도 잊히고, 잊히면 리뷰에서 매번 같은 피드백이 반복됩니다.

초기 버전의 load28-react는 79개 규칙을 나열하고 검증하는 방식이었습니다. 하지만 이 접근은 "규칙을 기억해서 매칭하기"라는 본질적 한계가 있었습니다. 슬롯 기반 프로토콜로 전환하면서, 규칙 위반을 "검사해서 잡는 것"에서 "구조적으로 불가능하게 만드는 것"으로 바꿨습니다.

props 슬롯이 7칸이면 8개를 넣을 수 없고, cleanup 슬롯이 필수이면 빠뜨릴 수 없고, 의존성 슬롯이 하위 레이어만 허용하면 역방향 import가 불가능합니다. TypeScript 컴파일러처럼, 규칙을 어기는 것이 아니라 어길 수 없는 구조를 만든 것입니다.

AI 코드 어시스턴트의 가치는 코드를 빠르게 생성하는 데 있는 것이 아닙니다. 팀의 기준에 맞는 코드를 일관되게 생성하는 것이 진짜 가치입니다. 스킬 시스템은 그 기준을 코드로 표현할 수 있게 해주고, 슬롯 기반 프로토콜은 그 기준을 구조적으로 강제할 수 있게 해줍니다.

참고