리액트 커스텀 훅 만들기: useLocalStorage·useFetch 핵심 2가지
커스텀 훅(Custom Hook)은 컴포넌트 간에 상태 로직을 재사용하는 가장 깔끔한 방법입니다. 이 글에서는 실무에서 특히 많이 쓰는 두 가지 패턴만 선별해 빠르게 알아봅니다.
언제 커스텀 훅을 만들까?
- 여러 컴포넌트에서 같은 로직을 반복해서 쓸 때 (예: 입력 상태, 저장소 동기화, 패칭)
- 컴포넌트 파일이 너무 길어질 때 상태 처리만 분리해 가독성·테스트성을 높이고 싶을 때
- 이벤트 리스너·타이머·네트워크 요청처럼 정리(cleanup)가 필요한 로직을 안전하게 재사용하고 싶을 때
예제 1) useLocalStorage — 상태를 브라우저 저장소와 동기화
상태를 localStorage에 저장해 새로고침/다른 탭에서도 유지합니다. storage 이벤트로 탭 간 동기화도 처리합니다.
코드: src/hooks/useLocalStorage.js
import { useEffect, useState, useCallback } from 'react';
export default function useLocalStorage(key, initialValue) {
const read = () => {
if (typeof window === 'undefined') return initialValue;
try {
const v = window.localStorage.getItem(key);
return v !== null ? JSON.parse(v) : initialValue;
} catch { return initialValue; }
};
const [value, setValue] = useState(read);
useEffect(() => {
try { window.localStorage.setItem(key, JSON.stringify(value)); } catch {}
}, [key, value]);
useEffect(() => {
const onStorage = (e) => {
if (e.key === key) {
try {
setValue(e.newValue !== null ? JSON.parse(e.newValue) : initialValue);
} catch {}
}
};
window.addEventListener('storage', onStorage);
return () => window.removeEventListener('storage', onStorage);
}, [key, initialValue]);
const reset = useCallback(() => {
try { window.localStorage.removeItem(key); } catch {}
setValue(initialValue);
}, [key, initialValue]);
return [value, setValue, reset];
}데모: src/examples/ThemeSwitcherDemo.jsx
import React from 'react';
import useLocalStorage from '../hooks/useLocalStorage';
export default function ThemeSwitcherDemo() {
const [theme, setTheme, reset] = useLocalStorage('demo-theme', 'light');
const toggle = () => setTheme(t => (t === 'light' ? 'dark' : 'light'));
const styles = theme === 'dark'
? { background: '#111', color: '#fff', padding: 12 }
: { background: '#f7f7f7', color: '#222', padding: 12 };
return (
<div style={styles}>
<h3>useLocalStorage 데모</h3>
<p>현재 테마: <strong>{theme}</strong></p>
<button onClick={toggle}>테마 토글</button>
<button onClick={reset} style={{ marginLeft: 8 }}>리셋</button>
<p style={{ marginTop: 8 }}>새로고침/다른 탭에서도 값이 유지되는지 확인하세요.</p>
</div>
);
}▼ 실행 화면 설명: 현재 테마(light/dark)가 굵게 표시되며, [테마 토글] 클릭 시 즉시 테마가 바뀌고 [리셋]을 누르면 기본값으로 돌아옵니다. 새로고침하거나 다른 탭을 열어도 값이 유지되는 것을 확인할 수 있습니다.

예제 2) useFetch — 안전한 데이터 패칭(요청 취소/무한 루프 방지)
요청 취소(AbortController)와 의존성 안정화를 처리하여 개발 모드에서도 불필요한 반복 요청을 막습니다.
코드: src/hooks/useFetch.js
import { useEffect, useRef, useState, useCallback, useMemo } from 'react';
export default function useFetch(url, options) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(!!url);
const [error, setError] = useState(null);
const abortRef = useRef(null);
const stableOptions = useMemo(() => options ?? undefined, [options]);
const execute = useCallback(async () => {
if (!url) return;
setLoading(true);
setError(null);
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
try {
const res = await fetch(url, { ...stableOptions, signal: controller.signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
setData(json);
} catch (e) {
if (e.name !== 'AbortError') setError(e);
} finally {
setLoading(false);
}
}, [url, stableOptions]);
useEffect(() => {
execute();
return () => abortRef.current?.abort();
}, [execute]);
return { data, loading, error, refetch: execute };
}데모: src/examples/UsersFetcherSafe.jsx
import React from 'react';
import useFetch from '../hooks/useFetch';
export default function UsersFetcherSafe() {
const { data, loading, error, refetch } = useFetch(
'https://jsonplaceholder.typicode.com/users'
);
return (
<div style={{ padding: 12, border: '1px solid #ddd' }}>
<h3>useFetch 데모</h3>
{loading && <p>로딩 중...</p>}
{error && <p style={{ color: 'crimson' }}>에러: {String(error.message)}</p>}
{data && (
<ul>
{data.map((u) => (
<li key={u.id}>{u.name} ({u.email})</li>
))}
</ul>
)}
<button onClick={refetch} style={{ marginTop: 8 }}>다시 불러오기</button>
</div>
);
}▼ 실행 화면 설명: 사용자 목록이 표시되고, [다시 불러오기] 클릭 시 네트워크 요청이 한 번 더 수행되어 동일 결과가 갱신됩니다. 네트워크를 끊은 뒤 재시도하면 에러 메시지가 표시됩니다.

빠른 체크리스트
- 의존성 배열 정확히:
useEffect/useCallbackdeps 누락 금지 - 정리(cleanup): 패칭 취소·리스너/타이머 해제는 기본
- SSR 고려:
window/localStorage접근은 서버에서 가드 - 일관된 API: 훅 반환값 형태(객체/배열) 통일
참고 링크
- React – 커스텀 훅: https://react.dev/learn/reusing-logic-with-custom-hooks
- React – 훅 규칙: https://react.dev/reference/rules
함께 보면 좋은 게시글
- 리액트 useEffect 사용법: 의존성 배열과 생명주기 이해
- 리액트 useMemo 제대로 쓰기: 언제 이득이고 언제 오버엔지니어링일까
- 리액트 useCallback 제대로 쓰기: 참조 안정화로 자식 리렌더 줄이기
- 리액트 useReducer vs useState: 언제 쓰고 어떻게 마이그레이션할까
- 리액트 useRef 사용법: DOM 제어, input focus, 이전 값 저장하기
이 글이 도움이 되셨다면 공유 부탁 드립니다.



