이번 편의 결과물: 목록 페이지에 디바운스된 검색창이 생기고, 지연 로직이 재사용 가능한 useDebounce 훅으로 분리됩니다. 다루는 개념: 잦은 상태 변경이 일으키는 문제(매 타이핑마다 필터링), useDebounce(value, delay) 구현, 목록 페이지에 검색창 추가
이 편에서 만드는 파일
bookshelf/src/
├── hooks/
│ └── useDebounce.js + 신규
└── pages/
└── BookListPage.jsx ~ 검색 input과 useDebounce 적용개념 정리
검색창에 onChange로 바로 필터링하면, 글자 하나를 칠 때마다 전체 books 배열을 다시 훑습니다. 목록이 몇 개일 때는 문제가 안 되지만, 10편에서 다룰 대량 데이터나 필터링 비용이 큰 조건(여러 필드 검색, 정렬 포함)에서는 타이핑이 버벅입니다.
디바운스(debounce)는 값이 바뀐 뒤 일정 시간 동안 추가 변경이 없을 때만 마지막 값을 확정하는 기법입니다. 검색창에 적용하면 사용자가 타이핑을 멈춘 뒤에만 필터링이 실행됩니다.
| 상태 | 역할 |
|---|---|
searchTerm | input의 value. 타이핑마다 즉시 바뀜(입력 반응성 유지) |
debouncedSearchTerm | useDebounce(searchTerm, delay)의 반환값. delay 동안 조용하면 갱신됨 |
| 필터링 대상 | books.filter(...)는 searchTerm이 아니라 debouncedSearchTerm 기준 |
입력창 자체는 searchTerm을 그대로 써서 즉시 반응하게 두고, 필터링 계산에만 지연된 값을 씁니다. 이렇게 나누면 “입력은 매끄럽고 계산은 절약된다”는 두 목표를 동시에 만족합니다.
실습
1. 파일 만들기
src/hooks/useDebounce.js를 새로 만듭니다.
2. 코드 작성
// src/hooks/useDebounce.js
import { useState, useEffect } from 'react';
export function useDebounce(value, delay = 300) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timerId = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timerId);
}, [value, delay]);
return debouncedValue;
}value가 바뀔 때마다 이전 타이머를 정리(clearTimeout)하고 새 타이머를 겁니다. delay 안에 value가 또 바뀌면 직전 타이머는 실행되기 전에 취소되므로, 마지막 값만 delay 뒤에 반영됩니다.
목록 페이지에 검색창을 추가합니다.
// src/pages/BookListPage.jsx (일부)
import { useState } from 'react';
import { useDebounce } from '../hooks/useDebounce.js';
// ...기존 import 유지
function BookListPage() {
// ...기존 useLocalStorage(books), filter 등 유지
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 300);
const visibleBooks = books.filter((book) =>
book.title.toLowerCase().includes(debouncedSearchTerm.trim().toLowerCase())
);
return (
<section className="book-list-page">
{/* ...기존 헤더·책 추가 링크 유지 */}
<input
type="search"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="제목으로 검색"
aria-label="책 제목 검색"
/>
{/* ...기존 BookList에 visibleBooks 전달 (books 대신) */}
</section>
);
}기존에 books를 바로 넘기던 자리를 visibleBooks로 바꿉니다. 상태 토글·삭제 핸들러는 books(전체 배열) 기준으로 그대로 둡니다.
3. 실행
npm run dev4. 확인
- 검색창에 타이핑하는 즉시 글자가 입력창에 보입니다(지연 없음).
- 타이핑을 멈추고 약 0.3초 뒤에 목록이 필터링됩니다.
- 검색어를 지우면 전체 목록이 다시 보입니다.
- 개발자 도구 콘솔에
console.log(debouncedSearchTerm)을 임시로 추가하면, 빠르게 타이핑하는 동안에는 로그가 거의 안 찍히다가 멈춘 뒤 한 번만 찍히는 것을 볼 수 있습니다.
직접 해보기
useDebounce의 두 번째 인자(delay)를 300에서 800으로 바꿔 체감 차이를 비교해 보세요.- 검색 대상에
author도 포함하도록visibleBooks필터 조건을 확장해 보세요.
풀이 보기
const visibleBooks = books.filter((book) => {
const keyword = debouncedSearchTerm.trim().toLowerCase();
return (
book.title.toLowerCase().includes(keyword) ||
book.author.toLowerCase().includes(keyword)
);
});delay를 늘리면 타이핑 후 목록이 갱신되기까지 체감 대기 시간이 길어집니다. 값이 너무 크면 반응이 느려 보이고, 너무 작으면 디바운스 효과가 거의 없습니다. 300~500 사이가 일반적인 검색창 기준입니다.
자주 하는 실수
| 증상 | 원인 | 고치는 법 |
|---|---|---|
| 입력창에 글자가 늦게 나타남 | input의 value를 debouncedSearchTerm으로 연결함 | input은 즉시 반응하는 searchTerm, 필터링만 지연된 값 사용 |
| 검색이 전혀 지연되지 않음 | useDebounce에 넘긴 delay가 0이거나 useEffect 의존성 배열에 value가 빠짐 | [value, delay]를 의존성 배열에 정확히 포함 |
| 컴포넌트를 벗어나도 타이머가 계속 돎 | useEffect에서 clearTimeout을 반환하지 않음 | 클린업 함수로 return () => clearTimeout(timerId) 유지 |
| 대문자·소문자가 섞이면 검색이 안 됨 | 원본과 검색어 대소문자를 안 맞춤 | 양쪽 다 toLowerCase() 적용 |