React & Typescript

🚀 성능 최적화: React 커스텀 훅 useDebounce를 활용한 지연 검색 구현

창따오 2025. 12. 12. 15:53
728x90

안녕하세요! 프론트엔드 개발 시 마주치는 흔한 성능 저하 문제 중 하나는 불필요한 API 호출입니다. 특히 검색 기능에서 사용자가 키보드를 누를 때마다(onChange) 서버에 쿼리를 날리면, 서버 부하 증가와 함께 느린 사용자 경험을 유발합니다.

저희 프로젝트에서도 DataGrid에 페이징을 적용한 선수 목록 검색 기능에서 이 문제가 발생하여, useDebounce 훅을 활용한 지연 검색(Debounced Search) 방식으로 성능을 획기적으로 개선했습니다.

이 글에서는 이 최적화 방법을 상세 코드와 함께 공유합니다.

 


1. 🔍 문제 진단: 왜 onChange는 위험한가?

기존의 검색 로직은 다음과 같이 작동했습니다.

  1. 키 입력 (onChange) 발생 -> searchKeyword 상태 변경
  2. usePagingData 훅 내의 loadData 함수가 searchKeyword를 의존성으로 가지고 있어 함수 재생성
  3. useEffect([loadData, pageNumber])가 함수 재생성을 감지하고 즉시 API 호출

사용자가 '삼성'을 검색하기 위해 'ㅅ', '사', '삼', '삼ㅅ', '삼성'을 입력하면, 사용자가 검색을 완료하기도 전에 최소 5번 이상의 불필요한 네트워크 요청이 발생합니다. 이는 리소스 낭비입니다.

2. 💡 해결 전략: 디바운싱(Debouncing) 도입

디바운싱은 마지막 이벤트가 발생한 후, 설정된 지연 시간(Delay)이 지나기 전까지는 실행을 연기하는 기술입니다.

저희는 useDebounce라는 커스텀 훅을 만들어, 사용자가 입력을 멈춘 후 500ms가 지나야만 searchKeyword를 업데이트하도록 로직을 분리했습니다.

3. 🎣 useDebounce 훅 구현 (코드 분리)

디바운싱 로직은 재사용 가능하도록 독립된 훅으로 구현합니다.

// useDebounce.js

import React, {useEffect, useState} from 'react';

function useDebounce(value, delay) {
    const [debouncedValue, setDebouncedValue] = useState(value);

    useEffect(() => {
        // 1. 타이머 설정: delay 시간 후 debouncedValue를 업데이트
        const handler= setTimeout(()=>{
            setDebouncedValue(value);
        },delay)
        
        // 2. Cleanup: value가 바뀌면 기존 타이머를 취소하고 새로 시작
        return () => clearTimeout(handler);
    }, [value, delay]); // value가 바뀔 때마다 이펙트 재실행

    return debouncedValue;
}
export default useDebounce;

4. 🔄 usePagingData에 디바운싱 통합 로직

핵심은 **실시간 입력 상태 (searchKeyword)**와 **API 호출을 위한 지연 상태 (debouncedKeyword)**를 분리하고, API 호출 로직은 지연 상태에만 의존하게 만드는 것입니다.

 
// usePagingData.js (핵심 로직)

export const usePagingData = (apiFn, apiParams = [], pageSize = 5, sortCriteria = 'id,desc') => {
    // ... (상태 정의 생략)

    // 1. [실시간 값] TextField에 바인딩
    const [searchKeyword, setSearchKeyword] = useState('');
    
    // 2. [지연 값] API 호출 트리거용 (500ms 지연)
    const debouncedKeyword = useDebounce(searchKeyword, 500); 

    // API 호출 로직 (loadData)
    const loadData = useCallback(async (page) => {
        // ...
        try {
            // API 호출 시, 지연된 debouncedKeyword를 사용
            const params = [...apiParams, debouncedKeyword, page, pageSize, sortCriteria];
            const res = await apiFn(...params);
            // ...
        } catch (e) { /* ... */ }
    }, [apiFn, apiParams.join(','), debouncedKeyword, pageSize, sortCriteria]); // ✅ debouncedKeyword 의존

    // 데이터 로드 트리거
    useEffect(() => {
        // loadData 함수 자체가 debouncedKeyword가 바뀔 때만 새로 생성됨
        // -> API 호출은 500ms 지연 후에만 발생하게 됨
        if (pageNumber !== 0) {
            loadData(pageNumber);
            return;
        }
        loadData(0); 
    }, [loadData, pageNumber]); // loadData 또는 pageNumber가 변경될 때만 실행

    // ... (handleSearchKeywordChange, handleSearch 등은 동일)
    
    return {
        // ... (반환값)
    };
};

✨ 최종 결과 및 개선 효과

이 수정으로 인해, 사용자가 검색창에 빠르게 타이핑을 하더라도 500ms 동안 입력을 멈추지 않는 한 API 요청은 발생하지 않습니다. 사용자가 입력 후 잠시 멈췄을 때만 자동으로 검색이 실행되어, 성능 부하를 최소화하고 사용자에게는 반응성이 뛰어난 검색 경험을 제공할 수 있게 되었습니다.