React Custom Hook 분리 타이밍이 고민이신가요? 헬퍼(일반) 함수와의 명확한 차이점부터 실무 적용 기준, 클린 코드를 위한 Before&After 예시까지 한눈에 깔끔하게 정리해 드립니다.
"이 코드, 꼭 Custom Hook으로 만들어야 할까?"
리액트로 프로젝트를 진행하다 보면 컴포넌트 코드가 수백 줄로 늘어나는 순간을 마주합니다. API를 호출하고, 로딩 상태를 관리하고, 이벤트를 바인딩하는 로직이 얽히면 가독성이 떨어지기 마련이죠.
이때 우리는 '로직 분리'를 고민합니다. 하지만 막상 코드를 떼어내려고 하면 고민이 시작됩니다. "이건 그냥 utils.ts에 일반 함수로 넣으면 안 되나? 왜 굳이 Custom Hook을 만들어야 하지?"
이 글에서 그 명확한 기준을 딱 정해드립니다. 이 기준만 기억하시면 더 이상 설계 단계에서 시간을 낭비하지 않게 될 것입니다.
핵심 요약, 커스텀 훅 vs 일반 함수 (3초 판별법)
가장 빠르게 판단할 수 있는 기준은 단 하나, 바로 "리액트의 내장 훅(useState, useEffect 등)이나 상태(State)를 사용하는가?" 입니다.
| 구분이 필요할 때 | Custom Hook (use...) | 일반 함수 (Helper / Utility) |
| 리액트 상태 사용 여부 | useState, useEffect 등 내장 훅 사용 필수 |
내부에서 리액트 훅을 사용하지 않음 |
| 반환값의 성격 | 상태(state)와 그 상태를 변경하는 함수 |
순수한 계산 결과, 데이터 포맷팅 값 등 |
| 컴포넌트 리렌더링 | 훅 내부 상태가 바뀌면 컴포넌트도 리렌더링됨 | 컴포넌트의 라이프사이클에 직접 관여하지 않음 |
핵심 규칙: 로직 내부에 리액트 State나 라이프사이클(useEffect)이 들어가지 않는다면, Custom Hook이 아니라 순수 일반 함수로 분리하는 것이 정답입니다.
Custom Hook을 만들어야 하는 결정적 타이밍 3가지
① 복수의 컴포넌트에서 '상태 기반 로직'이 중복될 때
가장 고전적인 이유입니다. 예를 들어, 여러 페이지에서 모달(Modal)을 열고 닫는 상태(isOpen, toggle)나, 윈도우 창의 크기(windowSize)를 추적해야 한다면 상태가 포함된 로직이므로 Custom Hook으로 만드는 것이 맞습니다.
② 하나의 컴포넌트에 useEffect가 너무 많아 흐름을 읽기 어려울 때
꼭 '재사용' 목적이 아니더라도 Custom Hook은 유용합니다. 컴포넌트 하나에서 복수의 API를 호출하고 복잡한 동기화 작업을 수행하느라 useEffect가 3~4개씩 쌓여있다면, 이를 도메인(비즈니스) 로직별로 묶어 Custom Hook으로 격리해 주세요. 컴포넌트의 가독성이 극적으로 올라갑니다.
③ 컴포넌트(UI)와 비즈니스 로직(기능)을 분리하고 싶을 때
UI를 담당하는 View 컴포넌트는 오직 '어떻게 보일 것인가'에만 집중하는 것이 좋습니다. 데이터가 '어떻게 불러와지고 처리되는지'의 메커니즘을 Custom Hook 내부로 숨기면(캡슐화), 컴포넌트는 선언적인 코드만 남겨 깔끔해집니다.
한눈에 비교하는 코드 예시
데이터를 페칭(Fetching)해오는 로직을 예시로 보겠습니다.
❌ Before: 컴포넌트에 로직이 얽혀있는 경우
UI를 그려야 하는 컴포넌트에 API 호출, 에러 핸들링, 로딩 상태 관리 로직이 전부 노출되어 있어 코드가 길고 가독성이 떨어집니다.
// UserProfile.tsx
import { useState, useEffect } from 'react';
export default function UserProfile({ userId }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetch(`https://api.example.com/users/${userId}`)
.then(res => res.json())
.then(data => {
setData(data);
setLoading(false);
});
}, [userId]);
if (loading) return <div>로딩 중...</div>;
return <div>{data?.name}님 안녕하세요!</div>;
}
⭕ After: Custom Hook으로 비즈니스 로직 캡슐화
리액트 훅(useState, useEffect)을 내포하고 있으므로 일반 함수가 아닌 Custom Hook으로 분리합니다.
// hooks/useFetchUser.ts (Custom Hook)
import { useState, useEffect } from 'react';
export function useFetchUser(userId) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetch(`https://api.example.com/users/${userId}`)
.then(res => res.json())
.then(data => {
setData(data);
setLoading(false);
});
}, [userId]);
return { data, loading };
}
// UserProfile.tsx (UI 컴포넌트는 선언적으로 변경)
import { useFetchUser } from './hooks/useFetchUser';
export default function UserProfile({ userId }) {
const { data, loading } = useFetchUser(userId); // 👈 단 한 줄로 로직 가져오기
if (loading) return <div>로딩 중...</div>;
return <div>{data?.name}님 안녕하세요!</div>;
}
자주 묻는 질문 (FAQ)
Q1. 재사용할 계획이 없는 로직도 Custom Hook으로 만들어도 되나요?
A. 네, 적극 권장합니다. Custom Hook은 복사-붙여넣기를 줄이는 '재사용'의 목적도 있지만, 복잡한 비즈니스 로직을 UI 컴포넌트로부터 격리하여 코드를 읽기 쉽게 만드는 '가독성 및 유지보수'의 목적도 매우 큽니다.
Q2. Custom Hook 내부에서 다른 Custom Hook을 불러와도 되나요?
A. 물론입니다. 컴포넌트 내부에서 리액트 훅을 조합하듯이, Custom Hook 안에서 또 다른 커스텀 훅이나 내장 훅을 조합하여 더 복잡한 기능을 구현할 수 있습니다. 다만 훅의 기본 규칙(루프나 조건문 안에서 호출 금지 등)은 똑같이 준수해야 합니다.
Q3. use를 안 붙이고 일반 함수명으로 만들면 어떻게 되나요?
A. 함수 이름이 use로 시작하지 않으면 리액트 린터(Linter)가 해당 함수를 일반 함수로 인식합니다. 만약 그 내부에서 리액트 훅(useState 등)을 호출하면 "리액트 훅은 컴포넌트나 커스텀 훅 내부에서만 호출되어야 한다"는 에러 규칙(Rules of Hooks) 위반 경고가 발생합니다.