description: "React Hooks 설계 원칙 및 사용 규칙" paths:
하나의 훅은 하나의 관심사만 다룬다. 여러 역할이 섞이면 분리한다.
// ❌ 너무 많은 역할
function useUserDashboard() { /* 인증 + 데이터 + UI 상태 전부 */ }
// ✅ 역할 분리
function useAuth() { /* 인증만 */ }
function useUserData(userId: string) { /* 데이터만 */ }
다음 중 하나라도 해당하면 커스텀 훅으로 추출한다:
useEffect + 관련 state가 3개 이상 묶임useState 여러 개가 서로 연관돼 함께 바뀌는 경우 useReducer로 전환한다.
// ❌ 연관된 state가 따로 관리됨 → 불일치 발생 가능
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState(null);
const [error, setError] = useState(null);
// ✅ useReducer로 묶기
type State =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: Data }
| { status: 'error'; message: string }
const [state, dispatch] = useReducer(reducer, { status: 'idle' });
전환 기준:
기존 state나 props로 계산 가능한 값은 별도 state로 만들지 않는다.
useEffect로 state를 동기화하는 패턴은 렌더 사이클을 낭비하고 버그를 유발한다.
// ❌ useEffect로 state 동기화
const [items, setItems] = useState(initialItems);
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);
// ✅ useMemo (비용이 큰 계산만)
const sortedItems = useMemo(
() => [...items].sort((a, b) => b.price - a.price),
[items]
);
useEffect에서 직접 API를 호출하지 않는다. 반드시 TanStack Query를 사용한다.
// ❌ useEffect로 데이터 패칭
useEffect(() => {
fetch('/api/users').then(res => res.json()).then(setUsers);
}, []);
// ✅ TanStack Query
const { data: users } = useQuery(userQueries.list());
이벤트 리스너, 타이머, 구독은 반드시 클린업한다.
useEffect(() => {
const handler = (e: Event) => { /* ... */ };
window.addEventListener("resize", handler);
return () => window.removeEventListener("resize", handler);
}, []);
의존성을 의도적으로 생략하지 않는다. eslint-plugin-react-hooks의 경고를 무시하지 않는다.
값이 매 렌더마다 새로 만들어져서 의존성에 넣기 곤란하다면 useRef 또는 useCallback으로 안정화한다.
// ❌ 의존성 생략
useEffect(() => {
doSomething(value); // value가 바뀌어도 재실행 안 됨
}, []); // eslint-disable-line — 금지
// ✅ 의존성 명시
useEffect(() => {
doSomething(value);
}, [value]);
useCallback, useMemo는 실제 성능 문제가 있을 때만 사용한다. 선제적 최적화는 오히려 코드를 복잡하게 만든다.
적합한 경우:
React.memo로 감싸져 있을 때// ✅ React.memo 자식에게 전달되는 핸들러
const handleSubmit = useCallback(() => {
onSubmit(formData);
}, [formData, onSubmit]);
// ❌ 단순 이벤트 핸들러에 불필요한 useCallback
const handleClick = useCallback(() => setOpen(true), []); // 과잉
브라우저 전용 API 접근 시 반드시 체크한다.
// ✅ 초기값에서 체크
const [value, setValue] = useState(() => {
if (typeof window === "undefined") return null;
return localStorage.getItem(key);
});
// ✅ effect에서 체크
useEffect(() => {
if (typeof window === "undefined") return;
// browser-only code
}, []);
use 접두사 필수useXxx (조회), useSaveXxx (저장), useDeleteXxx (삭제)isLoading, isOpen, hasError"SSR Safety 부분에 useIsClient 커스텀 훅 패턴도 추가되면 좋겠어요. typeof window 체크를 매번 하기보다 훅으로 빼서 쓰는 경우가 많아서요."