반복되는 상태 로직을 커스텀 훅으로 추출하는 방법, 좋은 커스텀 훅의 설계 원칙, useToggle·useLocalStorage·useDebounce 세 가지 실전 훅을 직접 만듭니다.
🔩 커스텀 훅이란
여러 컴포넌트에서 동일한 상태 로직이 반복될 때, 그것을 커스텀 훅(Custom Hook)으로 추출합니다. 커스텀 훅은 이름이 use로 시작하는 일반 함수입니다. 다른 훅들(useState, useEffect 등)을 내부에서 사용할 수 있고, 여러 컴포넌트에서 로직을 재사용할 수 있습니다.
커스텀 훅이 컴포넌트와 다른 점: 훅은 UI를 반환하지 않습니다. 상태와 함수(그리고 필요하면 값)를 반환합니다.
1️⃣ useToggle — 불리언 토글
가장 단순하지만 실무에서 자주 쓰이는 패턴입니다. 모달 열기/닫기, 드롭다운, 체크박스 등에 활용합니다:
// hooks/useToggle.ts
import { useState, useCallback } from 'react'
export function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue)
const toggle = useCallback(() => setValue((prev) => !prev), [])
const setTrue = useCallback(() => setValue(true), [])
const setFalse = useCallback(() => setValue(false), [])
return { value, toggle, setTrue, setFalse }
}
// 사용
function Modal() {
const { value: isOpen, toggle, setFalse: close } = useToggle()
return (
<>
<button onClick={toggle}>모달 열기</button>
{isOpen && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center">
<div className="bg-white rounded-2xl p-6">
<p>모달 내용</p>
<button onClick={close}>닫기</button>
</div>
</div>
)}
</>
)
}
2️⃣ useLocalStorage — 새로고침 후에도 유지되는 상태
테마, 언어 설정, 마지막 검색어 등 브라우저를 닫아도 유지돼야 하는 상태에 사용합니다:
// hooks/useLocalStorage.ts
import { useState, useEffect } from 'react'
export function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
// SSR 환경(Next.js 등) 대응
if (typeof window === 'undefined') return initialValue
try {
const item = window.localStorage.getItem(key)
return item ? (JSON.parse(item) as T) : initialValue
} catch {
return initialValue
}
})
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(value))
} catch {
// 시크릿 모드 등에서 localStorage 접근 불가 시 무시
}
}, [key, value])
return [value, setValue] as const
}
// 사용 — useState와 완전히 동일한 인터페이스!
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light')
return (
<button
onClick={() => setTheme((t) => (t === 'light' ? 'dark' : 'light'))}
className="px-4 py-2 border rounded"
>
현재 테마: {theme} (새로고침해도 유지됩니다)
</button>
)
}
3️⃣ useDebounce — 입력 지연 처리
검색창에서 타이핑할 때마다 API를 호출하면 너무 많은 요청이 발생합니다. 입력이 멈춘 뒤 일정 시간 후에만 실행되도록 디바운스(debounce)를 적용합니다:
// hooks/useDebounce.ts
import { useState, useEffect } from 'react'
export function useDebounce<T>(value: T, delay = 300): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value)
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => clearTimeout(timer) // 새 값이 오면 이전 타이머 취소
}, [value, delay])
return debouncedValue
}
// 사용
function SearchInput() {
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 500) // 0.5초 후 확정
const { data: results } = useFetch<Result[]>(
debouncedQuery ? `/api/search?q=${debouncedQuery}` : ''
)
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="검색..."
className="border rounded px-3 py-2"
/>
{/* results가 있으면 표시 */}
</div>
)
}
타이핑 중에는 query가 계속 바뀌지만 debouncedQuery는 500ms 동안 입력이 없을 때만 업데이트됩니다. API 요청이 크게 줄어듭니다.
📐 좋은 커스텀 훅 설계 원칙
- 한 가지 책임: 한 훅은 한 가지 관심사만 담습니다. 너무 많은 것을 넣으면 재사용성이 떨어집니다.
- useState처럼 반환: 반환값 인터페이스를
useState처럼 직관적으로 만드세요. - 초기값 파라미터: 기본값을 제공해 유연성을 높입니다.
- TypeScript 제네릭:
useLocalStorage<T>처럼 제네릭으로 타입 안전성을 보장합니다.
✅ 정리
- 커스텀 훅:
use로 시작하는 함수, 상태 로직을 재사용 가능하게 추출합니다. useToggle: 불리언 상태와 toggle/setTrue/setFalse 반환.useLocalStorage: localStorage와 동기화된 상태, SSR 안전 처리 포함.useDebounce: 값 변경을 지연시켜 API 요청 빈도를 줄입니다.
관련 주제
- useRef 개념
- DOM 요소 직접 접근
- 포커스 제어
- 렌더링과 무관한 값 저장
- 개발·프로그래밍
- 개발·프로그래밍 강의
- 리액트 입문 — 컴포넌트로 만드는 웹
- 무료강의
- 무료 온라인 강의
- NUGUNA
- 누구나
댓글
0/1000
불러오는 중...
