커스텀 훅이란?
use로 시작하는 함수로, React 훅을 내부에서 사용하는 재사용 가능한 로직 단위입니다.
useLocalStorage
import { useState } from "react";
function useLocalStorage<T>(key: string, initialValue: T) {
const [stored, setStored] = useState<T>(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
function setValue(value: T | ((prev: T) => T)) {
const newValue = value instanceof Function ? value(stored) : value;
setStored(newValue);
localStorage.setItem(key, JSON.stringify(newValue));
}
function removeValue() {
setStored(initialValue);
localStorage.removeItem(key);
}
return [stored, setValue, removeValue] as const;
}
// 사용
function Settings() {
const [theme, setTheme] = useLocalStorage("theme", "light");
const [language, setLanguage] = useLocalStorage("lang", "ko");
return (
<div>
<button onClick={() => setTheme(t => t === "light" ? "dark" : "light")}>
테마: {theme}
</button>
</div>
);
}
useDebounce
입력값이 멈춘 후 일정 시간이 지나야 값이 업데이트됩니다.
import { useState, useEffect } from "react";
function useDebounce<T>(value: T, delay: number = 500): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
// 사용: 검색 API를 타이핑마다 호출하지 않고 멈추면 호출
function SearchBox() {
const [query, setQuery] = useState("");
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) {
fetch(`/api/search?q=${debouncedQuery}`);
}
}, [debouncedQuery]);
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
useFetch
interface FetchState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
function useFetch<T>(url: string) {
const [state, setState] = useState<FetchState<T>>({
data: null, loading: true, error: null,
});
useEffect(() => {
let cancelled = false;
setState({ data: null, loading: true, error: null });
fetch(url)
.then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json() as Promise<T>;
})
.then(data => { if (!cancelled) setState({ data, loading: false, error: null }); })
.catch(error => { if (!cancelled) setState({ data: null, loading: false, error }); });
return () => { cancelled = true; };
}, [url]);
return state;
}
// 사용
function UserList() {
const { data: users, loading, error } = useFetch<User[]>("/api/users");
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return <ul>{users?.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
useToggle
function useToggle(initial = false) {
const [value, setValue] = useState(initial);
const toggle = useCallback(() => setValue(v => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return [value, toggle, setTrue, setFalse] as const;
}
// 사용
function Modal() {
const [isOpen, toggleModal, openModal, closeModal] = useToggle();
return (
<>
<button onClick={openModal}>열기</button>
{isOpen && (
<div className="modal">
<button onClick={closeModal}>닫기</button>
</div>
)}
</>
);
}
useWindowSize
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handler = () => setSize({
width: window.innerWidth,
height: window.innerHeight,
});
window.addEventListener("resize", handler);
return () => window.removeEventListener("resize", handler);
}, []);
return size;
}
// 사용
function ResponsiveComponent() {
const { width } = useWindowSize();
return <div>{width < 768 ? <MobileView /> : <DesktopView />}</div>;
}
커스텀 훅 설계 원칙
flowchart TB
subgraph PRINCIPLES["좋은 커스텀 훅"]
P1["단일 책임\n하나의 일에 집중"]
P2["테스트 가능\n순수 로직 분리"]
P3["재사용 가능\n컴포넌트와 분리"]
P4["use 접두사\nHook 규칙 적용"]
end
정리
커스텀 훅을 만들 때:
- 컴포넌트 여러 곳에서 같은 로직이 반복되면 훅으로 추출
use접두사 필수 (React 훅 규칙 적용)- 상태와 로직만 반환, UI는 컴포넌트가 담당
다음 편에서는 React Router — 싱글페이지 앱에서 페이지 라우팅을 구현하는 방법을 배웁니다.