여러 컴포넌트에서 똑같은 상태 로직을 복사·붙여넣기 하고 있나요? 그렇다면 커스텀 훅을 만들 때예요. 이번 장은 "로직을 재사용하는 기술"을 배웁니다. 생각보다 아주 간단해요.
use로 시작하는 함수를 만들어 그 안에서 다른 훅을 쓸 수 있어요.use~로 짓고 그 안에서 다른 훅(useState·useEffect)을 호출하는 커스텀 훅 규칙. "그냥 함수인데 내부에서 훅을 쓰면 커스텀 훅"이에요. 값을 계산·판단하면 JS, "화면·컴포넌트에 어떻게 반영되나"는 리액트라고 보면 대체로 맞아요.여러 컴포넌트를 만들다 보면 똑같은 상태 로직이 자꾸 반복돼요. 예를 들어 입력값 관리(값 담을 state + onChange), 데이터 페칭(로딩·에러·데이터 상태), 창 크기 감지(resize 이벤트 등록/해제) 같은 것들이죠. 이걸 컴포넌트마다 복사·붙여넣기 하면 관리가 지옥이 됩니다.
useState·useEffect를 함수 하나로 묶어 이름을 붙인 것뿐이에요. 그래서 앞 장들을 이해했다면 이번 장은 아주 쉽습니다.커스텀 훅을 만드는 방법은 놀라울 만큼 단순해요. 이름이 use로 시작하는 함수를 하나 만들고, 그 안에서 useState·useEffect 같은 기본 훅을 사용한 뒤, 컴포넌트가 필요로 하는 값을 return하면 됩니다.
use...로 시작하는 함수useState·useEffect 등return// useCounter.js — 카운터 로직을 담은 커스텀 훅
import { useState } from 'react';
function useCounter(초기값) {
const [count, setCount] = useState(초기값);
const inc = () => setCount(c => c + 1); // 1 증가
const dec = () => setCount(c => c - 1); // 1 감소
const reset = () => setCount(초기값); // 초기값으로
// 컴포넌트가 쓸 값들을 객체로 묶어 반환
return { count, inc, dec, reset };
}
export default useCounter;
use...로 짓는다 → ② 안에서 기본 훅을 쓴다 → ③ 필요한 값을 return한다. 이게 전부예요. 별도의 특별한 API가 있는 게 아닙니다.useCounter는 "숫자를 세는 데 필요한 state와 함수들"을 한 덩어리로 묶었어요. 이제 카운터가 필요한 컴포넌트는 이 훅 한 줄만 호출하면 되고, useState와 증감 함수를 매번 다시 쓸 필요가 없어요.
만든 커스텀 훅은 일반 함수 호출처럼 컴포넌트 안에서 부르면 돼요. 반환된 값을 구조 분해로 받아 쓰면 깔끔합니다.
const { count, inc } = useCounter(0) 는 반환된 객체에서 count·inc를 콕 집어 꺼내 쓰는 거예요.import useCounter from './useCounter';
function Counter() {
// 한 줄로 카운터 로직을 그대로 가져옴
const { count, inc, dec, reset } = useCounter(0);
return (
<div>
<p>현재 값: {count}</p>
<button onClick={inc}>+1</button>
<button onClick={dec}>-1</button>
<button onClick={reset}>리셋</button>
</div>
);
}
useCounter를 써도, 상태를 공유하지 않아요. 커스텀 훅은 호출될 때마다 그 안의 useState가 새로 실행돼서, 각 컴포넌트는 자기만의 독립된 count를 가집니다.
커스텀 훅은 "로직(코드)"을 공유하는 것이지 "상태(값)"를 공유하는 게 아니에요. 상태를 여러 컴포넌트가 함께 쓰려면 Context(CH 10)나 상태관리 도구가 필요해요.
커스텀 훅에는 딱 하나 꼭 지켜야 할 이름 규칙이 있어요. 반드시 이름이 use로 시작해야 합니다. 그래야 리액트(와 린트 도구 — 코드 실수를 자동으로 잡아주는 검사기)가 "이건 훅이다"라고 인식하고, 훅 규칙(조건문·반복문 안에서 훅을 쓰지 말 것 등)을 검사해줘요.
useCounter는 훅으로 인식하지만, getCounter처럼 지으면 일반 함수로 취급돼 훅 규칙 검사를 못 받아요. 훅 규칙을 어겨도 경고가 안 떠서 버그를 놓치기 쉬워집니다.
규칙: 함수 이름이 use로 시작 + 안에서 다른 훅을 사용 → 이게 바로 커스텀 훅이에요.
| 규칙 | 내용 |
|---|---|
| 이름 | 반드시 use로 시작. (useInput, useToggle …) 그래야 훅으로 인식돼요. |
| 반환값 | 자유예요. 값 하나, 배열, 객체 무엇이든 OK. 필요에 맞게 골라요. |
| 내부 | 안에서 useState·useEffect 등 기본 훅을 자유롭게 쓸 수 있어요. |
| 훅 규칙 | 일반 훅과 동일 — 최상위에서만 호출(조건문·반복문 안 금지). |
useState처럼 [값, 함수]), 여러 값을 이름으로 골라 쓰고 싶을 때는 객체가 편해요. 정답은 없고 상황에 맞게 고르면 됩니다.value와 onChange를 묶어, 폼 입력 하나를 한 줄로 관리해요. 입력 필드가 많은 폼에서 특히 유용해요.
function useInput(초기값) {
const [value, setValue] = useState(초기값);
const onChange = (e) => setValue(e.target.value);
return { value, onChange }; // 그대로 input에 펼쳐 쓰기 좋음
}
// 사용: const name = useInput('');
// <input value={name.value} onChange={name.onChange} />
function useToggle(초기값 = false) {
const [on, setOn] = useState(초기값);
const toggle = () => setOn(prev => !prev); // 현재 값을 뒤집음
return [on, toggle]; // 배열로 반환 → 이름 자유롭게 받기
}
// 사용: const [열림, 토글] = useToggle();
useEffect 안에서 API를 호출하고 로딩·에러·데이터 상태를 함께 관리하는, 아주 자주 만드는 훅이에요.
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]); // url이 바뀌면 다시 가져옴
return { data, loading, error };
}
use~ 함수로 뽑아내는 습관을 들이면 코드가 훨씬 깔끔해집니다.use로 시작하는 함수 안에서 useState·useEffect를 쓰고 필요한 값을 return.use로 시작해야 리액트가 훅으로 인식(훅 규칙 검사).