React Custom Hook 설계 패턴 - 로직을 분리하고 재사용하는 실무 전략

2026. 5. 6. 07:45·AI
🪝 REACT CUSTOM HOOK · 2026 완전 가이드

React Custom Hook 설계 패턴
로직을 분리하고 재사용하는 실무 전략

컴포넌트가 복잡해지는 이유는 JSX 때문이 아닙니다. 로직 때문입니다. Custom Hook으로 로직을 설계 단위로 분리하는 방법, 2026 React 19 트렌드까지 담았습니다.

🧠 로직 설계 사고방식 💻 실전 예제 4개 ⚛️ React 19 트렌드 반영

📋 목차

  1. 왜 Custom Hook이 필요한가? — 문제의 시작
  2. Custom Hook이란? — 정의와 핵심 규칙
  3. 실무에서 Custom Hook이 진짜 중요한 이유 — 의존성 분리
  4. 좋은 Custom Hook의 4가지 구조
  5. 실전 예제 1 — useToggle (단일 책임 패턴)
  6. 실전 예제 2 — useFetch (완전체 버전)
  7. 실전 예제 3 — useLocalStorage (외부 시스템 동기화)
  8. 실전 예제 4 — Zustand + Custom Hook 조합
  9. Component vs Hook vs Zustand — 역할 분리 기준
  10. 나쁜 Hook vs 좋은 Hook — 실무 차별화 포인트
  11. 실무 설계 패턴 3가지
  12. 2026 React 19 트렌드 — Hook은 어디로 가는가?
  13. 초보자가 자주 저지르는 실수 4가지

이 시리즈를 따라왔다면 이제 Vite로 프로젝트를 만들고, Compound Component로 UI를 설계하고, Zustand로 전역 상태를 관리할 줄 압니다. 그런데 기능이 늘어날수록 컴포넌트 파일이 100줄, 200줄로 불어나는 걸 막을 수가 없습니다. JSX가 문제일까요? 아닙니다. 로직 때문입니다.

Custom Hook은 이 문제에 대한 React의 공식 해답입니다. 단순히 "코드를 재사용하는 도구"가 아니라, "로직을 설계하는 단위"로 이해해야 합니다. 이 글은 개념 설명을 넘어서 실무에서 바로 쓸 수 있는 4가지 Hook 예제와 설계 패턴, 그리고 React 19에서 Hook이 어떻게 진화하고 있는지까지 담았습니다.

이 글을 읽고 나면 언제 Hook을 분리해야 하는지, 좋은 Hook과 나쁜 Hook의 차이가 무엇인지 명확하게 판단할 수 있게 됩니다.

React Custom Hook 설계 패턴 개요 — 컴포넌트와 로직 분리 다이어그램 2026

1. 왜 Custom Hook이 필요한가? — 문제의 시작

처음에는 모든 게 간단합니다. 하지만 로그인 폼에 기능이 하나씩 추가될수록 컴포넌트는 이렇게 변합니다.

⚠️ 문제 상황 — UI와 로직이 뒤섞인 컴포넌트

function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [isValid, setIsValid] = useState(false);

  useEffect(() => {
    setIsValid(email.includes('@') && password.length >= 8);
  }, [email, password]);

  const handleSubmit = async () => {
    setLoading(true);
    setError(null);
    try {
      await login(email, password);
    } catch (e) {
      setError('로그인에 실패했습니다.');
    } finally {
      setLoading(false);
    }
  };

  return ( // JSX는 여기서 시작... 이미 50줄짜리 함수
    <form>...</form>
  );
}

이 컴포넌트의 문제는 세 가지입니다. UI(JSX)와 비즈니스 로직이 한 파일에 섞여 있고, 같은 로그인 로직이 필요한 다른 화면에서 재사용할 수 없으며, 로직만 따로 테스트하기도 어렵습니다.

💡 핵심 인사이트
컴포넌트가 커지는 이유는 JSX가 많아서가 아닙니다. 로직이 UI에 섞여 있기 때문입니다. Custom Hook은 이 로직을 꺼내는 수술 도구입니다.

2. Custom Hook이란? — 정의와 핵심 규칙

Custom Hook은 상태와 로직을 재사용하기 위해 만든 함수입니다. React 공식 문서는 이를 "컴포넌트 사이에서 상태 로직을 공유하는 방법"이라고 정의합니다. 가장 중요한 규칙은 단 하나입니다. use로 시작해야 합니다.

// ✅ Custom Hook — use로 시작, 내부에서 Hook 사용 가능
function useLogin() {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const submit = async (email: string, password: string) => {
    setLoading(true);
    setError(null);
    try {
      await login(email, password);
    } catch {
      setError('로그인에 실패했습니다.');
    } finally {
      setLoading(false);
    }
  };

  return { loading, error, submit };
}

// ✅ 컴포넌트는 이제 UI만 담당
function LoginForm() {
  const { loading, error, submit } = useLogin();
  return (
    <form onSubmit={(e) => { e.preventDefault(); submit(email, pw); }}>
      {error && <p style={{ color: 'red' }}>{error}</p>}
      <button disabled={loading}>{loading ? '로그인 중...' : '로그인'}</button>
    </form>
  );
}

📌 Custom Hook 3가지 핵심 규칙

  • 이름은 반드시 use로 시작 — linter가 강제. getLogin은 Hook이 아님
  • 내부에서 다른 Hook 호출 가능 — useState, useEffect, useCallback 등 모두 가능
  • JSX를 반환하지 않는다 — Hook은 로직만, UI는 컴포넌트가 담당

3. 실무에서 Custom Hook이 진짜 중요한 이유 — 의존성 분리

많은 글이 Custom Hook을 "코드를 재사용하는 도구"로 소개합니다. 틀리진 않지만 핵심이 빠졌습니다. 실무에서 Custom Hook이 중요한 진짜 이유는 재사용이 아니라 의존성 분리(Dependency Separation)입니다.

3계층 의존성 분리 원칙

COMPONENT

🖼️

어떻게 보여줄 것인가

JSX, 스타일, 레이아웃

→

CUSTOM HOOK

🧠

어떻게 동작할 것인가

상태, 비즈니스 로직, 외부 연동

→

ZUSTAND

🗄️

어디에 저장할 것인가

전역 상태, 공유 데이터

이 세 계층을 명확히 나누면 각 레이어가 서로 독립적으로 진화할 수 있습니다. UI 디자인이 바뀌어도 로직은 그대로, 상태관리 도구를 교체해도 컴포넌트는 수정 없음. 이것이 의존성 분리가 주는 진짜 가치입니다.

💡 실무에서 체감되는 차이

의존성이 섞인 코드: 디자인 수정 → 로직 코드 건드림 → 예상치 못한 버그 발생 → 테스트 전체 다시

의존성이 분리된 코드: 디자인 수정 → 컴포넌트만 변경 → 로직은 그대로 → Hook 테스트 통과 → 안심하고 배포

4. 좋은 Custom Hook의 4가지 구조

실무에서 잘 설계된 Hook은 4가지 요소를 순서대로 담고 있습니다. Hook은 상태(state) + 상태 변화(action) + 사이드 이펙트(effect)를 함께 관리하는 구조이기 때문에 "작은 상태 머신"처럼 동작합니다. 이 4요소를 이해하면 어떤 로직이든 Hook으로 설계할 수 있습니다.

📐 좋은 Hook의 표준 구조

function useSomething(param: string) {

  // ① State — 원시 상태값
  const [data, setData] = useState<Item[] | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // ② Derived State — 계산된 값 (useEffect ❌, 렌더 중 계산 ✅)
  const isEmpty = data !== null && data.length === 0;
  const hasError = error !== null;

  // ③ Actions — 상태를 변경하는 함수
  const fetchData = async () => {
    setLoading(true);
    setError(null);
    try {
      const result = await api.get(param);
      setData(result);
    } catch {
      setError('데이터를 불러오지 못했습니다.');
    } finally {
      setLoading(false);
    }
  };

  // ④ Side Effects — 외부 세계와 동기화 (구독, DOM, 이벤트 등)
  useEffect(() => {
    fetchData();
  }, [param]);

  return { data, loading, error, isEmpty, hasError, refetch: fetchData };
}

⚡ 2026 중요 포인트 — Derived State는 useEffect가 아닙니다

isEmpty나 hasError처럼 기존 state에서 계산되는 값을 useEffect로 처리하면 렌더가 한 번 더 발생합니다. 렌더 중에 직접 계산하거나 useMemo를 사용하는 것이 2026년 React 팀의 권장 방향입니다. useEffect는 네트워크 요청, 이벤트 리스너, 타이머처럼 "외부 시스템과의 동기화"에만 사용하세요.

📌 좋은 Custom Hook 한눈에 정리

① 상태를 관리하고

State

② 상태를 계산하며

Derived State

③ 상태를 변경하고

Actions

④ 외부와 동기화한다

Side Effects

5. 실전 예제 1 — useToggle (단일 책임 패턴)

모달 열기/닫기, 아코디언, 드롭다운 — 어디서든 필요한 가장 기본적인 Hook입니다. 단일 책임 원칙의 교과서적인 예시입니다.

📄 useToggle.ts

import { useState, useCallback } from 'react';

function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);

  // useCallback으로 함수 참조 안정화 — 자식 컴포넌트에 전달할 때 유용
  const toggle = useCallback(() => setValue((v) => !v), []);
  const setOn = useCallback(() => setValue(true), []);
  const setOff = useCallback(() => setValue(false), []);

  return { value, toggle, setOn, setOff };
}

export default useToggle;

사용하는 쪽은 이렇게 씁니다.

function App() {
  const modal = useToggle();
  const sidebar = useToggle(true); // 초기값 true — 사이드바 기본 열림

  return (
    <>
      <button onClick={modal.toggle}>모달 {modal.value ? '닫기' : '열기'}</button>
      {modal.value && <Modal onClose={modal.setOff} />}

      <button onClick={sidebar.toggle}>사이드바 토글</button>
      {sidebar.value && <Sidebar />}
    </>
  );
}

✅ 포인트 — 같은 로직을 modal, sidebar, accordion, dropdown 어디서든 재사용합니다. toggle만 노출할 수도 있고 setOn/setOff를 분리해서 정밀 제어도 가능합니다.

6. 실전 예제 2 — useFetch (완전체 버전)

초안의 기본 버전에서 더 나아가, 실무에서 실제로 필요한 요소들을 모두 담은 완전체 버전입니다. 에러 처리, 수동 refetch, 중복 요청 방지(abort)까지 포함합니다.

📄 useFetch.ts — 실무 완전체

import { useState, useEffect, useCallback } from 'react';

type FetchState<T> = {
  data: T | null;
  loading: boolean;
  error: string | null;
};

function useFetch<T>(url: string) {
  const [state, setState] = useState<FetchState<T>>({
    data: null,
    loading: true,
    error: null,
  });

  const fetchData = useCallback(async (signal?: AbortSignal) => {
    setState((prev) => ({ ...prev, loading: true, error: null }));
    try {
      const res = await fetch(url, { signal });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const json: T = await res.json();
      setState({ data: json, loading: false, error: null });
    } catch (e) {
      if (e instanceof Error && e.name === 'AbortError') return; // 취소된 요청은 무시
      setState({ data: null, loading: false, error: '데이터를 불러오지 못했습니다.' });
    }
  }, [url]);

  useEffect(() => {
    const controller = new AbortController();
    fetchData(controller.signal);
    return () => controller.abort(); // 컴포넌트 언마운트 or url 변경 시 이전 요청 취소
  }, [fetchData]);

  // Derived State — 계산은 렌더 중에
  const isEmpty = state.data !== null && Array.isArray(state.data) && state.data.length === 0;

  return { ...state, isEmpty, refetch: () => fetchData() };
}

export default useFetch;

사용할 때는 이렇게 씁니다.

type User = { id: number; name: string };

function UserList() {
  const { data, loading, error, isEmpty, refetch } = useFetch<User[]>('/api/users');

  if (loading) return <Spinner />;
  if (error) return <p>{error} <button onClick={refetch}>다시 시도</button></p>;
  if (isEmpty) return <p>사용자가 없습니다.</p>;

  return <ul>{data?.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}

🔍 2026 실무 팁 — 단순 데이터 fetch라면 useFetch로 충분합니다. 하지만 캐싱, 백그라운드 동기화, 낙관적 업데이트까지 필요하다면 TanStack Query(React Query)를 사용하는 것이 현재 표준입니다. useFetch는 학습과 간단한 fetch에 좋고, 복잡한 서버 상태는 전용 라이브러리에 맡기세요.

Custom Hook 4가지 구성 요소 — State, Derived State, Actions, Side Effects 다이어그램

7. 실전 예제 3 — useLocalStorage (외부 시스템 동기화)

useEffect의 올바른 사용 사례입니다. localStorage는 React 바깥의 "외부 시스템"이므로 effect로 동기화하는 것이 정확합니다. 다크모드 설정, 검색어 기억 등에 실무에서 자주 쓰입니다.

📄 useLocalStorage.ts

import { useState, useEffect } from 'react';

function useLocalStorage<T>(key: string, initialValue: T) {
  // 초기화 시 localStorage에서 값을 읽음
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? (JSON.parse(item) as T) : initialValue;
    } catch {
      return initialValue;
    }
  });

  // ✅ useEffect의 올바른 사용: React 외부 시스템(localStorage)과의 동기화
  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(storedValue));
    } catch {
      console.error('localStorage 저장 실패');
    }
  }, [key, storedValue]);

  return [storedValue, setStoredValue] as const;
}

export default useLocalStorage;

📄 사용 예시

function ThemeToggle() {
  const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light');
  // 새로고침해도 값이 유지됨!
  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      현재 테마: {theme}
    </button>
  );
}

8. 실전 예제 4 — Zustand + Custom Hook 조합

2026년 실무에서 가장 많이 쓰이는 패턴입니다. Custom Hook이 Zustand 스토어의 추상화 계층 역할을 합니다. 컴포넌트가 Zustand에 직접 의존하지 않도록 중간에 Hook을 두는 구조입니다. 이 구조가 중요한 이유는 3가지입니다. 첫째, 컴포넌트가 store에 직접 의존하지 않아 상태관리 도구를 교체해도 컴포넌트는 수정 불필요. 둘째, 상태 변경 로직을 Hook 한 곳에 집중할 수 있음. 셋째, Hook만 따로 테스트 가능해 대규모 프로젝트에서 필수 패턴이 됩니다.

📄 useCart.ts — Zustand + Custom Hook

import { useCartStore } from '@/store/cartStore';
import { useCallback } from 'react';

function useCart() {
  // ✅ selector로 필요한 상태만 구독 (이전 글의 Zustand 패턴 활용)
  const items = useCartStore((s) => s.items);
  const addItem = useCartStore((s) => s.addItem);
  const removeItem = useCartStore((s) => s.removeItem);

  // Derived State — 스토어 밖에서 계산
  const totalCount = items.reduce((sum, item) => sum + item.quantity, 0);
  const totalPrice = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  const isEmpty = items.length === 0;

  // 비즈니스 로직 추가 가능 (스토어에 넣기 애매한 것들)
  const addWithLimit = useCallback((item: CartItem) => {
    if (totalCount >= 99) {
      alert('장바구니에 최대 99개까지 담을 수 있습니다.');
      return;
    }
    addItem(item);
  }, [addItem, totalCount]);

  return { items, totalCount, totalPrice, isEmpty, addItem: addWithLimit, removeItem };
}

export default useCart;

📄 컴포넌트에서 사용 — Zustand를 직접 import하지 않음

function CartIcon() {
  const { totalCount, isEmpty } = useCart();
  // useCartStore를 전혀 모름 — 추상화 성공!
  return <span>🛒 {isEmpty ? '비어있음' : `${totalCount}개`}</span>;
}

🏆 이 패턴의 3가지 장점

  • 추상화 — 컴포넌트가 Zustand에 직접 의존하지 않음. 나중에 다른 상태관리 도구로 교체해도 컴포넌트 수정 불필요
  • 비즈니스 로직 집중 — 수량 제한, 유효성 검사 같은 규칙을 Hook 안에 모음
  • 테스트 용이성 — Hook만 따로 테스트. 컴포넌트 렌더링 없이 로직 검증 가능

9. Component vs Hook vs Zustand — 역할 분리 기준

React 실무 설계의 3계층입니다. 각각의 역할을 명확히 구분하면 코드가 어디에 있어야 하는지 고민할 시간이 크게 줄어듭니다.

계층 역할 담당 내용 예시
Component UI 렌더링 JSX, 이벤트 핸들러 연결, 조건부 렌더링 CartPage, Button, Modal
Custom Hook 로직 설계 상태 관리, 비즈니스 규칙, 외부 시스템 연동 useCart, useFetch, useAuth
Zustand Store 전역 상태 여러 컴포넌트가 공유하는 상태와 액션 useCartStore, useUIStore

💡 한 줄 정리 — 컴포넌트는 화면을 그리고, Hook은 동작을 만들고, Zustand는 상태를 관리한다

10. 나쁜 Hook vs 좋은 Hook — 실무 차별화 포인트

개념을 알아도 실제로 Hook을 어떻게 나누어야 할지 감이 안 오는 경우가 많습니다. 나쁜 패턴과 좋은 패턴을 나란히 비교하면 차이가 즉각 보입니다.

❌ 나쁜 Hook — 모든 것을 하나에 몰아넣음

// ❌ 이건 Hook이 아니라 "작은 앱"이다
function useApp() {
  const [user, setUser] = useState(null);       // 유저 상태
  const [cart, setCart] = useState([]);          // 장바구니 상태
  const [modal, setModal] = useState(false);     // 모달 상태
  const [theme, setTheme] = useState('light');   // 테마 상태
  const [notifications, setNotifications] = useState([]);

  // 로그인 로직
  const login = async () => { ... };
  // 장바구니 추가 로직
  const addToCart = () => { ... };
  // 모달 토글 로직
  const toggleModal = () => { ... };

  return { user, cart, modal, theme, login, addToCart, toggleModal };
}

// 결과: 컴포넌트는 간단해졌지만 Hook이 300줄짜리 괴물이 됨

✅ 좋은 Hook — 기능 단위로 단일 책임 분리

// ✅ 각 Hook은 하나의 책임만 갖는다
function useAuth() {
  const [user, setUser] = useState(null);
  const login = async (email: string, pw: string) => { ... };
  const logout = () => { setUser(null); };
  return { user, login, logout };
}

function useCart() {
  const items = useCartStore((s) => s.items);
  const addItem = useCartStore((s) => s.addItem);
  const totalPrice = items.reduce((sum, i) => sum + i.price, 0);
  return { items, addItem, totalPrice };
}

function useModal() {
  const [isOpen, setIsOpen] = useState(false);
  const open = () => setIsOpen(true);
  const close = () => setIsOpen(false);
  return { isOpen, open, close };
}

// 컴포넌트에서 필요한 것만 골라 사용
function CheckoutPage() {
  const { user } = useAuth();
  const { items, totalPrice } = useCart();
  const modal = useModal();
  // 각 Hook이 독립적 → 하나 바꿔도 다른 것에 영향 없음
}

✅ 언제 Hook을 분리해야 하나? — 실무 판단 기준 3가지

  • 로직이 UI와 섞이기 시작할 때 — JSX 안에 async 로직, 복잡한 상태 계산이 보이면 즉시 분리 신호
  • 동일한 로직이 2곳 이상 등장할 때 — 복붙하는 순간이 Hook 추출 타이밍
  • 상태 + 액션 + Effect가 한 컴포넌트에 함께 있을 때 — 세 가지가 모이면 거의 항상 Hook으로 분리할 수 있음

11. 실무 설계 패턴 3가지

팀 프로젝트에서 실제로 쓰이는 Hook 설계 방식입니다.

📁 패턴 1 — Feature(기능) 단위 분리

기능별로 Hook을 나눕니다. 가장 기본적이고 직관적인 방법입니다.

hooks/
  useAuth.ts       // 로그인, 로그아웃, 토큰 갱신
  useCart.ts       // 장바구니 CRUD + 계산
  useModal.ts      // 모달 열기/닫기/콘텐츠 관리
  useToast.ts      // 토스트 알림
  useWindowSize.ts // 브라우저 창 크기

🏗️ 패턴 2 — 계층형 구조 (Hook in Hook)

큰 기능을 작은 Hook으로 나누고 조합합니다. 복잡한 도메인 로직을 다룰 때 유리합니다.

// useUser (최상위 — 외부에 노출)
function useUser() {
  const profile = useUserProfile();   // 프로필 CRUD
  const settings = useUserSettings(); // 알림·테마 설정
  const activity = useUserActivity(); // 최근 활동
  return { profile, settings, activity };
}

// useUserProfile (내부 구현 — 외부에 직접 노출 X)
function useUserProfile() { ... }

⚡ 패턴 3 — Hook + Zustand + TanStack Query 역할 분리 (2026 추천)

각 도구에 역할을 명확히 배분합니다. 2026년 실무 팀에서 가장 많이 채택하는 구조입니다.

// 서버 데이터 (API) → TanStack Query
function useUserQuery(id: string) {
  return useQuery({ queryKey: ['user', id], queryFn: () => fetchUser(id) });
}

// 클라이언트 UI 상태 → Zustand
function useUIStore() {
  const isModalOpen = useModalStore((s) => s.isOpen);
  ...
}

// 비즈니스 로직 조합 → Custom Hook
function useUserPage(userId: string) {
  const { data: user, isLoading } = useUserQuery(userId);
  const modal = useToggle();
  return { user, isLoading, modal };
}

12. 2026 React 19 트렌드 — Hook은 어디로 가는가?

React 19와 생태계가 변하면서 Custom Hook의 역할과 작성 방식도 달라지고 있습니다. 핵심 변화 4가지를 정리합니다.

① useEffect 남용 탈피 — "Effect는 탈출구"

React 팀은 공식 문서에서 "useEffect는 탈출구(escape hatch)"라고 명시합니다. 파생 상태 계산, API 데이터 변환 등을 useEffect에 넣는 것은 이제 안티패턴입니다. useEffect는 이벤트 리스너 등록, WebSocket 구독, 비React 라이브러리 연동처럼 진짜 외부 시스템 동기화에만 씁니다.

② React 19 use() — useFetch의 미래

React 19에서 도입된 use() API는 Promise를 렌더 중에 직접 소비할 수 있습니다. 기존의 useEffect + useState 조합으로 만들던 데이터 페칭 패턴을 크게 단순화합니다.

import { use, Suspense } from 'react';

// 기존 방식 (useEffect + useState)
function useUsers() {
  const [users, setUsers] = useState(null);
  useEffect(() => { fetch('/api/users').then(...).then(setUsers); }, []);
  return users;
}

// React 19 use() 방식 — Suspense와 함께
const usersPromise = fetch('/api/users').then((r) => r.json());
function UserList() {
  const users = use(usersPromise); // 로딩 중엔 Suspense가 처리
  return <ul>{users.map(...)}</ul>;
}

③ React Compiler (React Forget) — useMemo/useCallback 자동화

2026년 점진적으로 도입되는 React Compiler는 컴포넌트와 Hook을 컴파일 타임에 자동으로 메모이제이션합니다. useMemo와 useCallback을 직접 쓸 일이 줄어들지만, Hook 설계 원칙 자체는 바뀌지 않습니다. 오히려 순수한 로직 분리 원칙이 더 중요해집니다.

④ 결론 — Custom Hook은 더 중요해진다

React 19로 서버 컴포넌트, use(), React Compiler가 도입되어도 클라이언트 로직 분리의 핵심 단위는 Custom Hook입니다. 도구가 진화해도 "컴포넌트는 UI, Hook은 로직"이라는 원칙은 변하지 않습니다. 오히려 생태계가 복잡해질수록 잘 설계된 Hook이 팀 생산성을 결정합니다.

React 19 Hook 진화 트렌드 2026 — use(), React Compiler, Custom Hook 역할 변화 타임라인

13. 초보자가 자주 저지르는 실수 4가지

❌ 실수 1 — Hook이 너무 커진다 (useAppLogic)

useAppLogic, usePage 같이 모든 걸 하나에 넣으면 컴포넌트의 문제가 Hook으로 옮겨갈 뿐입니다. Hook도 단일 책임이어야 합니다. 기능 단위로 쪼개세요.

❌ 실수 2 — Hook에서 JSX를 반환한다

Hook은 로직만 반환합니다. JSX를 반환하는 함수는 컴포넌트입니다. 이 구분이 흐려지면 나중에 테스트와 재사용 모두 어려워집니다.

❌ 실수 3 — 파생 상태를 useEffect로 처리한다

isEmpty, isValid 같은 계산값을 useEffect로 동기화하면 렌더가 한 번 더 발생하고 코드도 복잡해집니다. 기존 state에서 계산되는 값은 렌더 중에 직접 선언하거나 useMemo를 사용하세요.

// ❌ 잘못된 패턴
useEffect(() => { setIsEmpty(data.length === 0); }, [data]);

// ✅ 올바른 패턴
const isEmpty = data.length === 0; // 렌더 중 계산

❌ 실수 4 — 서버 데이터를 useFetch로 관리하려 한다

API 데이터는 캐싱, 재검증, 낙관적 업데이트가 필요합니다. useFetch를 직접 확장하다 보면 결국 TanStack Query를 다시 만드는 셈이 됩니다. 서버 상태는 처음부터 TanStack Query를 사용하고, useFetch는 간단한 one-off 요청에만 씁니다.

✅ 실전 체크리스트 — Custom Hook 분리 판단 기준

  • ☑컴포넌트의 useState가 3개 이상인가? → Hook 분리 적극 검토
  • ☑같은 로직이 2개 이상 컴포넌트에 반복되는가? → 즉시 Hook으로 추출
  • ☑비즈니스 로직(유효성 검사, 계산)이 JSX 안에 있는가? → Hook으로 이동
  • ☑useEffect 안에서 파생 상태를 계산하고 있는가? → 렌더 중 계산 또는 useMemo로 교체
  • ☑Zustand store에 컴포넌트별 비즈니스 규칙이 들어가 있는가? → Custom Hook으로 이동
  • ☑컴포넌트 파일이 100줄이 넘는가? → 로직 분리 시작 신호

🔑 핵심 정리

  • Custom Hook은 코드 재사용 도구가 아니라 로직을 설계하는 단위다
  • 컴포넌트는 UI를 그리고, Hook은 동작을 만들고, Zustand는 전역 상태를 관리한다 — 역할을 분리하면 코드가 단순해진다
  • 좋은 Hook의 4요소: State → Derived State (렌더 중 계산) → Actions → Side Effects
  • useEffect는 진짜 외부 시스템 동기화에만 사용. 파생 상태 계산에 쓰는 건 2026년 기준 안티패턴
  • Zustand를 Custom Hook으로 감싸면 추상화 계층이 생겨 테스트와 유지보수가 쉬워진다
  • React 19 use()와 React Compiler가 변해도 "컴포넌트는 UI, Hook은 로직"이라는 원칙은 불변이다
  • React 실력은 컴포넌트를 얼마나 잘 나누느냐가 아니라, 로직을 얼마나 잘 분리하느냐에서 결정된다

❓ 자주 묻는 질문 (FAQ)

Q1. Custom Hook을 꼭 별도 파일로 분리해야 하나요? 컴포넌트 파일 안에 써도 되나요?

작은 프로젝트에서는 같은 파일 안에 써도 동작합니다. 하지만 재사용 가능성이 조금이라도 있다면 처음부터 별도 파일로 분리하는 습관을 들이는 것이 좋습니다. hooks/ 폴더 아래 useToggle.ts, useFetch.ts 식으로 관리하면 팀 협업에서도 찾기 쉽습니다.

Q2. useFetch 같은 커스텀 데이터 패칭 Hook과 TanStack Query는 언제 각각 써야 하나요?

판단 기준은 "캐싱이 필요한가"입니다. 단순히 한 번 데이터를 가져와서 보여주면 되는 경우에는 useFetch로 충분합니다. 하지만 같은 데이터를 여러 컴포넌트에서 공유하거나, 백그라운드에서 자동으로 최신화하거나, 낙관적 업데이트(optimistic update)가 필요하다면 TanStack Query를 사용하세요. 실무에서는 대부분의 API 데이터가 TanStack Query 범주에 해당합니다.

Q3. React 19의 use() 훅이 나왔으면 Custom Hook useFetch는 이제 쓸모없어지는 건가요?

그렇지 않습니다. use()는 주로 서버 컴포넌트와 Suspense 기반의 선언적 데이터 로딩에 최적화되어 있습니다. 클라이언트 컴포넌트에서 조건부 요청, 수동 refetch, 사용자 액션에 의한 요청 등 복잡한 시나리오에는 여전히 Custom Hook 패턴이 필요합니다. 또한 useFetch를 직접 만들어보는 과정 자체가 useEffect의 올바른 사용법과 AbortController, 클린업 함수를 이해하는 최고의 학습 경로입니다.

Q4. Custom Hook을 TypeScript로 작성할 때 반환 타입을 명시해야 하나요?

소규모 Hook은 TypeScript 추론에 맡겨도 됩니다. 다만 팀에서 공유하는 Hook이나 여러 컴포넌트가 의존하는 Hook은 반환 타입을 명시하는 것이 좋습니다. 타입이 명시되면 자동완성이 정확해지고, Hook 내부 구현을 바꿀 때 타입 오류로 호출부 변경이 필요한 곳을 즉시 찾을 수 있습니다. as const 반환(배열 형태일 때)과 인터페이스 반환(객체 형태일 때)을 상황에 맞게 구분해서 쓰세요.

NEXT STEP

Hook으로 로직을 나눴다면, 이제 프로젝트 구조를 설계할 차례입니다

컴포넌트, Hook, Store, API 레이어를 어떤 폴더 구조로 관리해야 팀 전체의 생산성이 올라갈까요? 실무에서 검증된 React 폴더 구조 전략을 다음 글에서 정리합니다.

→ React 폴더 구조 설계 전략
→ TanStack Query vs Zustand 역할 분리
→ Zustand 실전 프로젝트 적용

📌 이 글은 React 학습 시리즈입니다
1편: Vite + TypeScript React 입문  |  2편: Compound Component & Render Props
3편: Context API vs Zustand  |  4편: Custom Hook 설계 패턴 (현재 글)
💡 실제 프로젝트에 적용해보면 로직 분리의 효과가 훨씬 선명하게 느껴집니다

🔗 시리즈 이전 글

✓ TypeScript + React 입문자를 위한 첫 프로젝트 만들기 (Vite 기반)

✓ React 컴포넌트 설계 패턴 완전 입문 — Compound Component & Render Props

✓ React 상태관리 비교: Context API vs Zustand

'AI' 카테고리의 다른 글

Claude API 시작 가이드 —Blender·Adobe 자동화를 직접 만드는 방법  (0) 2026.05.08
크롬 확장 프로그램 자동화 가이드 - Magical + Thunderbit 사용법  (1) 2026.05.07
React 상태관리 비교: Context API vs Zustand — 언제 무엇을 써야 할까?  (0) 2026.05.06
AI 고블린 문제, OpenAI가 밝힌 진짜 원인은 '버그'가 아니었다  (1) 2026.05.05
Cursor AI 단축키만 외우지 마세요 - 생산성 2배 올리는 고급 기능 총정리  (2) 2026.05.04
'AI' 카테고리의 다른 글
  • Claude API 시작 가이드 —Blender·Adobe 자동화를 직접 만드는 방법
  • 크롬 확장 프로그램 자동화 가이드 - Magical + Thunderbit 사용법
  • React 상태관리 비교: Context API vs Zustand — 언제 무엇을 써야 할까?
  • AI 고블린 문제, OpenAI가 밝힌 진짜 원인은 '버그'가 아니었다
Arahant
Arahant
  • Arahant
    MANI
    Arahant
    • 분류 전체보기 (58) N
      • 시사 (4)
      • 라이프 (4)
      • AI (50) N
  • 인기 글

  • 태그

    1인AI미디어
    1인사업자자동화
    1인크리에이터도구
    1인크리에이터자동화
    2026AI개발자
    2026AI개발자취업
    2026AI트렌드
    2026개발자취업
    2026웹개발
    2026웹개발트렌드
  • 최근 글

  • 블로그 메뉴

    • 홈
    • about
    • contact
    • 개인정보 처리방침
  • hELLO· Designed By정상우.v4.10.6
Arahant
React Custom Hook 설계 패턴 - 로직을 분리하고 재사용하는 실무 전략
상단으로

티스토리툴바