유저 입력에 따라 빈번하게 발생하는 이벤트(ex. 검색어 입력과 같이 실시간으로 변하는 데이터를 다룰 때)를 효율적으로 처리하기 위해 디바운스(Debounce
)가 많이 사용된다. 그 동안 lodash
라이브러리의 debounce
함수를 사용했는데, 이번에는 라이브러리를 사용하지 않고 직접 debounce
를 구현해보았다.
디바운스는 특정 이벤트가 연속적으로 발생할 때, 마지막 이벤트가 발생한 후 일정 시간 동안 추가 이벤트가 발생하지 않으면 해당 이벤트를 실행하는 기법이다. 이를 통해 불필요한 함수 호출을 줄이고 성능을 최적화 할 수 있다.
주로 아래와 같은 상황에서 많이 사용된다.
먼저, lodash
라이브러리에서 제공하는 debounce
함수를 사용하는 방법을 살펴보자. 아래와 같이 아주 간편하게 debounce
처리가 가능하다.
import React, { useMemo, useState } from "react";
import { debounce } from "lodash";
function App() {
const [searchKeyword, setSearchKeyword] = React.useState("");
const debouncedSearch = useMemo(
() => debounce((keyword: string) => setSearchKeyword(keyword), 500),
[]
);
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
debouncedSearch(event.target.value);
};
return (
<div>
<input type="text" onChange={handleSearchChange} placeholder="검색어를 입력하세요" />
<p>검색어: {searchKeyword}</p>
</div>
);
}
export default App;
lodash
의 debounce
를 사용하여 handleSearchChange
함수가 호출될 때마다 0.5초 동안 대기한 후 setSearchKeyword
를 실행하게 된다.
lodash
라이브러리의 다른 기능들이 필요한 것이 아니라면, 번들 크기를 줄이고 의존성을 최소화하기 위해 라이브러리를 사용하지 않고 직접 디바운스를 구현할 수 있다. 구현 방법은 생각보다 정말 간단하다. React
에서는 useRef
와 useCallback
훅을 활용해서 구현할 수 있다.
먼저, 디바운스 로직을 재사용 할 수 있도록 useDebounce
라는 커스텀 훅을 만들어주었다.
// hooks/useDebounce.ts
import { useRef, useEffect, useCallback } from "react";
function useDebounce<T extends (...args: any[]) => void>(callback: T, delay: number): T {
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const debouncedFunction = useCallback((...args: any[]) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
callback(...args);
}, delay);
}, [callback, delay]);
// 컴포넌트 언마운트 시 타이머 정리
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
return debouncedFunction as T;
}
export default useDebounce;
위 코드에서 timeout
을 참조하기 위해 useRef
를 사용했다.
let timeout: ReturnType<typeof setTimeout>;
와 같은 형식의 단순 변수를 사용하지 않고 useRef
로 할당한 이유는 뭘까?
React
의 함수 컴포넌트는 렌더링될 때마다 함수가 다시 호출된다. 이 때, 단순 변수는 매번 새로운 값으로 초기화된다. 이는 이전 타이머를 참조할 수 없게 만들어 clearTimeout
이 제대로 동작하지 않을 수 있다.
반면, useRef
는 컴포넌트의 라이프사이클 동안 값을 유지할 수 있어 타이머를 안정적으로 관리가 가능하다. 이를 통해 타이머를 정확히 취소하고, 메모리 누수를 방지할 수 있다.
이제 useDebounce
훅을 컴포넌트에 적용해보았다.
import React, { useState } from "react";
import useDebounce from "./hooks/useDebounce";
function App() {
const [searchKeyword, setSearchKeyword] = React.useState("");
const debouncedSearch = useDebounce((keyword: string) => {
setSearchKeyword(keyword);
}, 500);
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
debouncedSearch(event.target.value);
};
return (
<div>
<input type="text" onChange={handleSearchChange} placeholder="검색어를 입력하세요" />
<p>검색어: {searchKeyword}</p>
</div>
);
}
export default App;
lodash
라이브러리를 사용했을 때와 동일하게 handleSearchChange
함수가 호출될 때마다 0.5초 동안 대기한 후 setSearchKeyword
를 실행하게 된다.