커스텀 훅: 로직을 재사용 가능한 단위로 분리하기
React에서 컴포넌트는 UI를 재사용하는 단위입니다. 그렇다면 로직은 어떻게 재사용할까요? 커스텀 훅이 그 답입니다. 커스텀 훅은 여러 컴포넌트에서 공통으로 필요한 상태 관련 로직을 별도의 함수로 분리하는 방법입니다.
커스텀 훅의 규칙
커스텀 훅은 두 가지 규칙만 지키면 됩니다.
- 함수 이름이
use로 시작해야 합니다 - 내부에서 다른 훅을 호출할 수 있습니다
// 커스텀 훅
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(prev => prev + 1);
const decrement = () => setCount(prev => prev - 1);
const reset = () => setCount(initialValue);
return { count, increment, decrement, reset };
}
// 사용하는 컴포넌트
function Counter() {
const { count, increment, decrement, reset } = useCounter(10);
return (
<div>
<p>{count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>초기화</button>
</div>
);
}use로 시작하는 이름 덕분에 React는 이 함수가 훅의 규칙을 따라야 한다는 것을 인식합니다.
실전 예제들
useLocalStorage: 로컬스토리지와 동기화된 상태
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = (value) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
// 사용
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
현재 테마: {theme}
</button>
);
}useFetch: 데이터 가져오기 로직 추상화
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(data => {
setData(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, [url]);
return { data, loading, error };
}
// 사용
function UserProfile({ userId }) {
const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
if (loading) return <div>로딩 중...</div>;
if (error) return <div>오류 발생</div>;
return <div>{user.name}</div>;
}useDebounce: 연속된 입력을 지연 처리
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer); // 클린업: 이전 타이머 취소
}, [value, delay]);
return debouncedValue;
}
// 사용: 타이핑을 멈춘 후 300ms 뒤에만 검색
function SearchInput() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) {
searchAPI(debouncedQuery);
}
}, [debouncedQuery]);
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}훅 합성: 훅 위에 훅 쌓기
커스텀 훅은 다른 커스텀 훅을 조합해서 만들 수 있습니다.
function useSearchResults(url) {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300); // 커스텀 훅 사용
const { data, loading } = useFetch(`${url}?q=${debouncedQuery}`); // 또 다른 커스텀 훅
return { query, setQuery, results: data, loading };
}
// 이제 컴포넌트는 아주 단순해집니다
function SearchPage() {
const { query, setQuery, results, loading } = useSearchResults('/api/search');
return (
<>
<input value={query} onChange={e => setQuery(e.target.value)} />
{loading ? <div>검색 중...</div> : <ResultList items={results} />}
</>
);
}커스텀 훅의 장점
- 재사용성: 같은 로직을 여러 컴포넌트에서 사용할 수 있습니다
- 관심사 분리: 컴포넌트는 UI만, 훅은 로직만 담당합니다
- 테스트 용이성: 로직을 독립적으로 테스트할 수 있습니다
- 가독성: 컴포넌트 코드가 간결해집니다
정리
커스텀 훅은 특별한 API가 아닙니다. use로 시작하는 이름을 가진 일반 함수입니다. 같은 패턴의 훅 조합이 두 곳 이상에서 반복된다면, 커스텀 훅으로 추출할 때가 된 것입니다. UI 로직과 비즈니스 로직을 분리하는 가장 React다운 방법입니다.