상태: Draft (논의 후 확정)
react-simplikit을 운영하며 축적한 훅 설계 철학을 하나의 공통 원칙으로 정의한다. 이 원칙은 두 가지 용도로 사용된다:
- 코드 리뷰 —
react-hook-review스킬이 이 원칙 기반으로 피드백 - 코드 작성 —
react-hook-writing스킬이 이 원칙 기반으로 가이드
| 방향 | 출처 | 범위 |
|---|---|---|
| 훅 코딩 원칙 (Section 2) | CLAUDE.md, AGENTS.md, 내부 스킬 | 반환값, TypeScript, 성능, 문서화 등 코딩 스타일 |
| 훅 사용 패턴 (Section 3) | React 공식 문서 (react.dev) | state 설계, effect 사용법, 메모이제이션, 커스텀 훅 설계 |
| # | 요구사항 | 상세 |
|---|---|---|
| R1 | 리뷰/생성 공통 원칙 | 두 스킬이 동일한 원칙 참조 |
| R2 | Why 중심 | 규칙(What)만 나열하지 않고 철학(Why)을 narrative로 설명 |
| R3 | Opinionated 투명성 | 🟢 Best Practice vs 🟡 Opinionated 명시 |
| R4 | 프로젝트 무관 | react-simplikit 경로/명령어/유틸 없이 범용 원칙만 |
| R5 | Cross-tool | Claude Code 플러그인 + Codex(AGENTS.md) + Cursor(.cursorrules) |
| # | 질문 | 선택지 |
|---|---|---|
| Q1 | C14(Named useEffect)를 포함할지? | A) "Recommended"로 포함 B) 제외 |
| Q2 | C2(SSR-Safe)를 비-SSR 프로젝트에도 권장할지? | A) 항상 B) SSR 사용 시만 |
| Q3 | C9(JSDoc)의 @example을 필수로 할지? | A) 4-tag 전부 필수 B) @example은 권장 |
| Q4 | 추가할 원칙이 있는지? | — |
| Q5 | 원칙 먼저 확정할지, 바로 플러그인 구조로 갈지? | A) 원칙 먼저 B) 바로 플러그인 |
| Q6 | 플러그인 배포 채널 | A) git-subdir B) npm C) 미정 |
CLAUDE.md + AGENTS.md + 내부 스킬에서 추출한 코딩 스타일 원칙.
C1과 C7은 React 전역 베스트 프랙티스가 아니라 프로젝트 컨벤션이므로 인라인 🟡로 표기. C14는 아래 별도 🟡 섹션에 위치.
반환값이 1개여도 { value } 형태. 객체는 순서 무관, 이름으로 의미 전달, 확장 시 breaking change 없음.
function useDebounce<T>(value: T, delay: number): { value: T };
function useToggle(init: boolean): { value: boolean; toggle: () => void };
function usePagination(): { page: number; next: () => void; prev: () => void };useState(FIXED_VALUE) + useEffect(sync). 브라우저 API 초기화 금지. 서버에 window 없음 → 크래시 또는 hydration mismatch.
// ✅ SSR 안전
const [width, setWidth] = useState(0);
useEffect(function syncWidth() {
setWidth(window.innerWidth);
}, []);
// ❌ SSR 크래시
const [width, setWidth] = useState(window.innerWidth);
// ⚠️ 클라이언트 전용 앱에서만 허용
const [width, setWidth] = useState(() => {
if (typeof window === 'undefined') return 0;
return window.innerWidth;
});모든 부수효과에 cleanup 반환. 메모리 누수 방지. StrictMode 이중 마운트가 즉시 노출.
// 이벤트 리스너
useEffect(function subscribe() {
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []);
// AbortController (비동기)
useEffect(
function fetchData() {
const controller = new AbortController();
fetch(url, { signal: controller.signal }).then(/* ... */);
return () => controller.abort();
},
[url]
);
// 타이머
useEffect(function tick() {
const id = setInterval(callback, 1000);
return () => clearInterval(id);
}, []);제네릭 <T> 사용. any 전파 → 타입 시스템 무력화. 정당한 사유(generic callback 등) 시 per-line eslint-disable + 코멘트 허용.
// ✅ Generic
function useDebounce<T>(value: T, delay: number): T;
// ✅ 정당한 예외 (코멘트 필수)
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- generic callback
type AnyFunction = (...args: any[]) => unknown;tree-shaking 보장 + import 명확성. export default 금지.
if (value) 금지 → 0, "", false falsy 버그 방지. == null로 null+undefined 동시 체크.
if (ref == null) { return; } // ✅ null + undefined
const controlled = valueProp !== undefined; // ✅ 구분 필요할 때
if (count) { ... } // ❌ count=0 통과 못함훅의 인자를 개별 파라미터 대신 객체(props)로. 순서 무관 + 이름으로 의미 전달 + 확장 시 breaking change 없음.
// ✅ 객체
function useDebounce<T>({
value,
delay,
leading,
}: {
value: T;
delay: number;
leading?: boolean;
}): { value: T };
// ❌ 위치 기반
function useDebounce<T>(
value: T,
delay: number,
leading?: boolean
): { value: T };nested if-else 대신 early return. 실패 조건 먼저 걸러내고 성공 로직은 플랫하게.
// ✅
function process(value: string | null) {
if (value == null) {
return DEFAULT;
}
return transform(value);
}
// ❌
function process(value: string | null) {
if (value != null) {
return transform(value);
} else {
return DEFAULT;
}
}모든 public API에 @description + @param + @returns + @example. AI 문서 생성 + IDE 툴팁.
/**
* @description 값의 변경을 지연시킨다.
* @param value - 디바운스할 값
* @param delay - 지연 시간 (ms)
* @returns 디바운스된 값
* @example
* const debouncedQuery = useDebounce(query, 300);
*/고빈도(30+/sec) 이벤트에만 적용. 일반 훅에는 불필요.
| 기법 | 적용 시점 |
|---|---|
| Throttle (16ms) | scroll, resize, pointer, keyboard |
| Deduplicate | 값 미변경 시 setState skip |
| startTransition | 비긴급 파생 계산 (React 18+) |
함수 선언은 function 키워드. 화살표는 인라인 콜백(map, filter)에만.
function toggle(state: boolean) {
return !state;
} // ✅ 선언
items.filter(item => item != null); // ✅ 인라인
const toggle = (state: boolean) => !state; // ❌ 선언에 화살표프로덕션 코드에 외부 런타임 의존성 금지. peerDependencies만 허용. 번들 사이즈 최소화 + 의존성 충돌 방지.
훅 내부에서 외부 모듈을 직접 호출하지 않고 인자로 주입. 테스트 용이성 + 교체 가능성.
// ✅ 의존성 주입
function useFetch<T>(fetcher: (url: string) => Promise<T>, url: string) { ... }
// ❌ 외부 모듈 직접
function useFetch<T>(url: string) { const res = await axios.get(url); ... }useEffect(function handleResize() {...}). 에러 스택에서 "handleResize" vs "anonymous". Trade-off: 화살표보다 장황. cleanup 이름은 "Recommended" (필수 아님).
| 항목 | 이유 |
|---|---|
| Import extensions (.js/.ts) | 빌드 도구 의존적 |
| 100% test coverage | 프로젝트 정책 |
| 파일 구조/커밋 컨벤션 | 훅 설계 철학 아님 |
별도 문서: react-hook-usage-patterns.md
React 공식 문서(react.dev) 기반 17개 패턴 (U1-U18, U4 제거):
| 카테고리 | 개수 | 핵심 |
|---|---|---|
| State Design | U1-U3, U5-U7 (6개) | 파생값 계산, props 복사 금지, useRef, union type, state 그룹화 |
| Effect Usage | U8-U14 | effect는 외부 동기화 전용, 체인 금지, key 리셋, 비동기 cleanup |
| Memoization | U15-U16 | useMemo 1ms+, useCallback + memo() 조합만 |
| Hook Design | U17 | lifecycle wrapper 금지, 구체적 목적 훅만 |
| Identity and Rendering | U18 | 안정적인 key, 의도적 remount, 렌더링 효율 |
이 문서 (principles, 원칙 정의)
↓ 압축
react-hook-review/SKILL.md (체크리스트)
react-hook-writing/SKILL.md (가이드)
↓ 추가 압축
AGENTS.md Part 1 (Codex용)
↓ 참조
.cursorrules (Cursor용)
packages/plugin/ (planned)
├── .claude-plugin/plugin.json
├── .codex-plugin/plugin.json
├── principles/ ← 공통 원칙 Single Source
├── skills/
│ ├── react-hook-review/SKILL.md ← C1-C14 + U1-U18 체크리스트
│ ├── react-hook-review/references/key-rendering.md
│ └── react-hook-writing/
│ ├── SKILL.md ← 테마별 가이드
│ └── references/patterns.md ← 구현 예시 3개
└── README.md
| 도구 | 파일 | 현재 | 변경 |
|---|---|---|---|
| Claude Code (내부) | .claude/skills/ |
✅ 10개 | 유지 |
| Claude Code (플러그인) | packages/plugin/ |
❌ | Phase 1-5로 생성 |
| Codex | AGENTS.md |
✅ 162줄 | Part 1(Universal) + Part 2(Project) 분리 |
| Cursor | .cursorrules |
✅ 28줄 | AGENTS.md 참조 유지 |
| 추출됨 (철학) | 남겨짐 (구현) |
|---|---|
| "항상 객체 반환" | packages/core/src/hooks/ 경로 |
| "Named useEffect improves stack traces" | yarn test, yarn fix 명령 |
| "SSR-safe: fixed initial + useEffect sync" | renderHookSSR.serverOnly() 유틸 |
| "4 JSDoc tags for AI doc generation" | 100% coverage 기준 |
| Before (프로젝트 전용) | After (범용) |
|---|---|
renderHookSSR.serverOnly() |
Vitest + delete global.window |
yarn test / yarn fix |
"Run your test suite" |
packages/core/ 경로 |
"your source directory" |
react-simplikit 언급 |
제거 |
| Phase | 내용 | 산출물 |
|---|---|---|
| 1 | 디렉토리 + plugin.json + README | packages/plugin/ 구조 |
| 2 | react-hook-review SKILL.md | C1-C14 + U1-U18 체크리스트 |
| 3 | react-hook-writing SKILL.md + patterns.md | 테마별 가이드 + 3개 훅 예시 |
| 4 | 일반화 검증 (grep) | 프로젝트 참조 0건 |
| 5 | 플러그인 validate + 로컬 테스트 | 동작 확인 |
| 항목 | 통과 기준 |
|---|---|
| 플러그인 구조 | claude plugin validate . 에러 0 |
| 범용성 | 다른 React 프로젝트에서 프로젝트 참조 0건 |
| 철학 깊이 | 각 규칙의 Why가 narrative |
| Opinionated 투명성 | 🟡 패턴에 trade-off 존재 |
- Codex/Gemini 대응 (AGENTS.md Part 1 활용)
- Component 설계 철학 추가
- Marketplace 전환 (Plugin 3개+ 시)