React Hook 커스텀 패턴 — 실무에서 자주 쓰는 10가지
매번 새로 만들다가 결국 사내 hook 라이브러리를 만들었다. 이 10개 hook만 있어도 대부분의 프로젝트를 커버할 수 있다.
1. useDebounce
function useDebounce<T>(value: T, delay: number = 500): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
2. useLocalStorage
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue] as const;
}
// 사용
function App() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Toggle</button>;
}
3. useMediaQuery
function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false);
useEffect(() => {
const mediaQuery = window.matchMedia(query);
setMatches(mediaQuery.matches);
const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
}, [query]);
return matches;
}
// 사용
function ResponsiveComponent() {
const isMobile = useMediaQuery('(max-width: 768px)');
return isMobile ? <MobileView /> : <DesktopView />;
}
4. usePrevious
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
5. useIntersectionObserver
function useIntersectionObserver(
ref: RefObject<HTMLElement>,
options: IntersectionObserverInit = {}
): IntersectionObserverEntry | null {
const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null);
useEffect(() => {
if (!ref.current) return;
const observer = new IntersectionObserver(([entry]) => {
setEntry(entry);
}, options);
observer.observe(ref.current);
return () => observer.disconnect();
}, [ref, options]);
return entry;
}
// 사용: 이미지 lazy loading
function LazyLoadImage() {
const ref = useRef<HTMLImageElement>(null);
const entry = useIntersectionObserver(ref);
return (
<img
ref={ref}
src={entry?.isIntersecting ? 'image.jpg' : 'placeholder.jpg'}
/>
);
}
6. useFetch
interface UseFetchState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
function useFetch<T>(url: string): UseFetchState<T> {
const [state, setState] = useState<UseFetchState<T>>({
data: null,
loading: true,
error: null,
});
useEffect(() => {
const controller = new AbortController();
(async () => {
try {
const response = await fetch(url, { signal: controller.signal });
const data = await response.json();
setState({ data, loading: false, error: null });
} catch (error) {
setState({ data: null, loading: false, error: error as Error });
}
})();
return () => controller.abort();
}, [url]);
return state;
}
// 사용
function UserProfile() {
const { data, loading, error } = useFetch('/api/user/123');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{data?.name}</div>;
}
추가 패턴
다른 유용한 hook들:
- useAsync: 비동기 작업 처리 및 캐싱
- useToggle: boolean 상태 토글
- useMounted: hydration mismatch 방지
- useUpdateEffect: mount 때 실행 안 함
Hook 라이브러리화
이 hook들을 npm 패키지로 만들어 팀이 공유할 수 있다:
// package.json
{
"name": "@company/react-hooks",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts"
}
// index.ts
export { useDebounce } from './useDebounce';
export { useLocalStorage } from './useLocalStorage';
export { useMediaQuery } from './useMediaQuery';
export { usePrevious } from './usePrevious';
export { useIntersectionObserver } from './useIntersectionObserver';
export { useFetch } from './useFetch';
모든 프로젝트에서:
import { useDebounce, useLocalStorage, useFetch } from '@company/react-hooks';
결론
이 10개 hook만 있어도 대부분의 React 프로젝트를 커버할 수 있다. 매번 새로 만드는 대신, 라이브러리로 만들어 팀이 공유하면 개발 속도가 크게 올라간다.