무조건 지키는 코드 컨벤션 — 예외 없는 절대 규칙
이 규칙들에는 예외가 없어요. "상황에 따라 다르죠"도 없습니다. React Compiler부터 Props CASE, 추상 동사 금지, useEffect 제한까지 — 전부 자동완성·추적·가독성을 위해서예요.

이 글의 규칙들에는 예외가 없어요. "상황에 따라 다르죠"도 없습니다.
왜 이렇게까지 하냐고요? 자동완성 품질, 코드 추적 가능성, 가독성. 이 셋 때문이에요. 역할별로 나눠두면 IDE가 다음 단계를 제안해주고, 이름만 읽어도 흐름이 추적되거든요. AI에게 코드를 맡기는 시대라 오히려 더 중요해졌습니다.
(프로세스 글 8단계의 '프로젝트 컨벤션' 체크가 바로 이 문서예요.)

1 · React 최적화는 React Compiler에게
useMemo, useCallback은 지양해요. React Compiler가 자동으로 메모이제이션하니까, 수동 메모이제이션은 컴파일러가 커버하지 못하는 예외 상황에서만 씁니다.
// ❌ Bad
const filtered = useMemo(() => items.filter(predicate), [items, predicate]);
const handleClick = useCallback(() => onClick(id), [onClick, id]);
// ✅ Good — React Compiler가 자동 최적화
const filtered = items.filter(predicate);
const handleClick = () => onClick(id);2 · 함수 구현 시 레이어 구분 필수
하나의 함수나 훅 안에서 데이터, 로직, 프레젠테이션을 섞지 않아요.
// ❌ Bad — 레이어가 뒤섞임
const UserPage = () => {
const data = useQuery(...);
const sorted = data.filter(...).sort(...);
const handleDelete = () => { ... };
return <div>{sorted.map(...)}</div>;
};
// ✅ Good — 레이어 분리
const UserPage = () => {
const { data, item } = useUserService();
return <UserList users={data.users} onDelete={item.delete} />;
};3 · Props 역할 분리
핵심 원칙: 컴포넌트 제어 props와 도메인 데이터를 flat하게 섞지 않는다. 갯수가 문제가 아니라, 역할이 다른 props가 한 레벨에 섞이는 게 문제예요. 판단은 위에서 아래 순서로 합니다.
CASE 1 · 도메인 데이터만 → 펼쳐도 OK
하나의 도메인 데이터만 표시하는 컴포넌트는 flat 전달을 허용해요.
// ✅ OK
<UserProfile name={name} email={email} avatar={avatar} />CASE 2·3 · 제어와 도메인 혼재 → 객체로 분리
open, onClose, loading 같은 제어 props와 name, email 같은 도메인 데이터가 같은 레벨이면 무조건 분리하고, 도메인 덩어리가 2개 이상이면 각각 객체로 나눠요.
// ❌ Bad
<UserPopup onClose={handleClose} open={isOpen} name={name} email={email} /><TransferPopup open sourceName={s.name} sourceId={s.id} targetName={t.name} />
// ✅ Good
<UserPopup onClose={handleClose} open={isOpen} user={user} /><TransferPopup open={isOpen} onClose={handleClose} source={source} target={target} />CASE 4 · 독립 UI 영역 2개 이상 → Compound 필수
badge={{...}}, actions={{...}} 같은 UI 영역 설정 객체는 객체로 포장만 한 것이지 선언적 분리가 아니에요.
// ❌ Bad — UI 영역을 객체로 포장한 것뿐
<UserCard user={user} badge={{ color: 'blue' }} actions={{ onEdit, onDelete }} />
// ✅ Good — 각 UI 영역이 독립 컴포넌트로 선언
<UserCard user={user}>
<UserCard.Badge color="blue" label="Admin" />
<UserCard.Actions onEdit={handleEdit} onDelete={handleDelete} />
</UserCard>CASE 5 · 경계 케이스
독립 UI 영역이 아닌 단순 이벤트 콜백 1~2개는 props로 괜찮아요. 단, 콜백이 특정 UI 영역에 종속되면(onBadgeClick + badgeColor) Compound로 전환합니다.
4 · Return 구조화와 네이밍
훅·서비스가 서로 다른 역할의 메서드를 반환할 때 flat 나열은 금지예요. 역할이 하나뿐이면 그루핑하지 않고요(불필요한 nesting도 금지).
// ✅ 역할이 하나 — flat OKreturn { get, create, update, delete: remove };
// ❌ 역할이 다른데 flatreturn { getUser, createUser, getUserList, selectUser, deselectUser };
// ✅ 역할별 네임스페이스 · 데이터와 메서드도 분리return {
data: { users, isLoading, error },
item: { get: getUser, create: createUser },
list: { get: getUserList, filter: filterUserList },
selection: { ids: selectedIds, toggle: toggleSelect },
};그리고 추상적 동사 절대 금지. 이름만 보고 뭘 하는지 즉시 알 수 있어야 해요. 아래 동사가 메서드명이나 키에 들어가면 무조건 잘못된 코드입니다.
resolve— 조회인지 생성인지 변환인지 알 수 없음 →get·find·loaddefine— 선언? 생성? 설정? 불명확 →create·set·registerprocess— 모든 것이 process →validate·transform·submithandle— 이벤트 핸들러가 아닌 곳 금지 → 실제 동작 동사execute·perform— 범용적 →run·apply·triggermanage·do— 의미 없음 → 실제 동작으로 분해flat— 변환 방식 불명확 →normalize·toList
(코드에서 resolve가 보이면 일단 의심하세요.)
네임스페이스 키에는 동사를 섞지 않아요 — 키는 순수 명사, 메서드는 구체적 동사. 자가 점검은 간단해요. service.content.resolve()는 뭘 하는지 모르지만, service.content.getById(id)는 압니다.
5 · 레이어 침범 절대 금지
위 다이어그램이 요약이에요. 각 레이어는 자기 역할만 하고, 다른 레이어의 책임을 절대 가져오지 않습니다.
- 변환은 변환 전담 레이어에서만 — 훅이
data.user_name을 바꿔 내보내거나, "convert 서비스"를 새로 만들어 변환 레이어를 중복 생성하지 않는다. adapter든 mapper든 프로젝트의 전담 레이어에서만 변환한다. UI가 원시 데이터 구조를 직접 참조하는 것도 금지. - API/페칭 레이어는 호출만 — 페칭 함수 안에서 도메인 변환이나 에러 토스트(UI 책임)를 하지 않는다.
- 컴포넌트 파일은 UI만 — 한 파일에 formatter, adapter, validator, hook, 하위 컴포넌트를 몰아넣어 500줄을 만들지 않는다. 역할별 파일로 분리하고 컴포넌트는 얇게 유지한다.
- 도메인 경계를 넘는 직접 import 금지 — 다른 도메인의 내부 구현(
@/modules/order/service)을 직접 끌어오지 않는다. 공개 contract/interface, DI/IoC 주입, Registry 조합, Event/Pub-Sub 같은 공식 경계 수단으로만 소비한다. 같은 도메인 내부에서는 자유. - 의존이 역방향이면 역전시킨다 — 순환·역방향 의존을 직접 import로 뚫지 말고, 인터페이스로 계약을 세우고 구현을 주입한다.
6 · useEffect 남용 금지
useEffect를 쓸 수 있는 유일한 경우는 React 상태로 제어할 수 없는 외부 시스템뿐이에요 — DOM 직접 조작(focus·scroll), 타이머, 외부 이벤트 구독(WebSocket 등), 브라우저 API(IntersectionObserver 등). 그 외는 전부 React 상태 흐름으로 해결합니다.
// ❌ state 감지 → useEffect 후속 처리useEffect(() => { if (selectedId) fetchDetail(selectedId); }, [selectedId]);
// ✅ 이벤트 핸들러에서 직접
const handleRowClick = (id) => { setSelectedId(id); fetchDetail(id); };대표적인 금지 패턴과 대체 수단이에요.
- 클릭·submit 후속 처리를 effect로 → 이벤트 핸들러에서 직접
- 하나의 액션을 useEffect 체인으로 분산 → 로직을 서비스로 분리해 핸들러에서 순차 호출 (체인은 순서 보장이 안 되고 사이드이펙트 추적 불가)
- query/props를 읽기 전용인데 state로 복사 → 직접 사용 (폼처럼 수정하는 상태 관리는 당연히 OK — 이벤트 타이밍에 reset)
- 파생 값 계산 → 렌더링 중 계산
- prop 변경 시 state 초기화 →
key로 컴포넌트 교체 - 자식이 fetch해서 부모로 올려보내기 → 부모에서 fetch 후 props로 내려주기
- window/navigator 구독을 effect로 직접 →
useSyncExternalStore
7 · 개인 Codex 스킬로 기계화
이 규칙들은 문서로만 두지 않고, 개인 codex-rules 저장소의 스킬 두 개로 만들어 설치했어요. 전역 규칙과 주제가 겹치면 더 엄격한 이 스킬을 우선 적용합니다.
- react-component-api-structure — props가 도메인 구조와 UI 구조를 보존하는지 검사. 도메인 객체를
userId·userName으로 분해하는 것,show*·has*boolean 2개 초과,title*·action*같은 반복 prefix, semantic region을 숨기는slots·render*props를 잡아낸다. shared UI 컴포넌트는 도메인 타입을 알면 안 된다. - domain-api-structure — 공유 함수·서비스·util의 구조 검사.
resolveItemName같은 exported flat helper 금지, owner 도메인 → 명사 네임스페이스 → 구체 동사 메서드 순서 강제,targetProduct류는target.product로 승격, 전역 god service 금지.
작업 완료 전에 각 스킬의 보조 검사 스크립트(check-react-component-api-structure.mjs, check-domain-api-structure.mjs)를 돌려요. 사람이 기억할 규칙은 언젠가 잊히니까, 규칙은 검사기로 만들어야 재발하지 않아요. arcaC 회고에서 배운 그대로입니다.
마지막으로 다시, 이유. 이 모든 까다로움은 미학이 아니라 실용이에요. 역할별 객체와 명사 네임스페이스는 자동완성이 다음 후보를 정확히 제안하게 만들고, 이름만 보고 아는 동사는 코드 추적에서 파일을 열어볼 필요를 없애고, 레이어 경계는 아무 파일이나 열어도 그 책임을 3초 안에 알게 해줘요. AI가 코드를 쓰는 비중이 커질수록, 사람이 빠르게 읽고 판정할 수 있는 구조의 가치는 오히려 올라갑니다.


