Skip to Content
WebReactReact 중급07. 커스텀 훅 추출: useDebounce와 검색

이번 편의 결과물: 목록 페이지에 디바운스된 검색창이 생기고, 지연 로직이 재사용 가능한 useDebounce 훅으로 분리됩니다. 다루는 개념: 잦은 상태 변경이 일으키는 문제(매 타이핑마다 필터링), useDebounce(value, delay) 구현, 목록 페이지에 검색창 추가

이 편에서 만드는 파일

bookshelf/src/ ├── hooks/ │ └── useDebounce.js + 신규 └── pages/ └── BookListPage.jsx ~ 검색 input과 useDebounce 적용

개념 정리

검색창에 onChange로 바로 필터링하면, 글자 하나를 칠 때마다 전체 books 배열을 다시 훑습니다. 목록이 몇 개일 때는 문제가 안 되지만, 10편에서 다룰 대량 데이터나 필터링 비용이 큰 조건(여러 필드 검색, 정렬 포함)에서는 타이핑이 버벅입니다.

디바운스(debounce)는 값이 바뀐 뒤 일정 시간 동안 추가 변경이 없을 때만 마지막 값을 확정하는 기법입니다. 검색창에 적용하면 사용자가 타이핑을 멈춘 뒤에만 필터링이 실행됩니다.

상태역할
searchTerminput의 value. 타이핑마다 즉시 바뀜(입력 반응성 유지)
debouncedSearchTermuseDebounce(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 dev

4. 확인

  • 검색창에 타이핑하는 즉시 글자가 입력창에 보입니다(지연 없음).
  • 타이핑을 멈추고 약 0.3초 뒤에 목록이 필터링됩니다.
  • 검색어를 지우면 전체 목록이 다시 보입니다.
  • 개발자 도구 콘솔에 console.log(debouncedSearchTerm)을 임시로 추가하면, 빠르게 타이핑하는 동안에는 로그가 거의 안 찍히다가 멈춘 뒤 한 번만 찍히는 것을 볼 수 있습니다.

직접 해보기

  1. useDebounce의 두 번째 인자(delay)를 300에서 800으로 바꿔 체감 차이를 비교해 보세요.
  2. 검색 대상에 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의 valuedebouncedSearchTerm으로 연결함input은 즉시 반응하는 searchTerm, 필터링만 지연된 값 사용
검색이 전혀 지연되지 않음useDebounce에 넘긴 delay가 0이거나 useEffect 의존성 배열에 value가 빠짐[value, delay]를 의존성 배열에 정확히 포함
컴포넌트를 벗어나도 타이머가 계속 돎useEffect에서 clearTimeout을 반환하지 않음클린업 함수로 return () => clearTimeout(timerId) 유지
대문자·소문자가 섞이면 검색이 안 됨원본과 검색어 대소문자를 안 맞춤양쪽 다 toLowerCase() 적용

확인 문제

문제 14지선다
useDebounce에서 입력창의 value에 searchTerm을 쓰고 필터링에는 debouncedSearchTerm을 쓰는 이유는
문제 24지선다
useDebounce의 useEffect에서 clearTimeout을 클린업으로 반환하지 않으면 생기는 문제는
문제 34지선다
delay 값을 크게(예: 2000ms) 설정했을 때 나타나는 현상으로 알맞은 것은

참고 자료

Last updated on