12.1 useDefferedValue 개요12.1.1. useDeferredValue이란?12.1.2 useDeferredValue 사용법12.2 debounce12.2.1 debounce란?12.2.2 debounce 사용해보기12.3 throttle12.3.1 throttle이란?12.3.2 throttle 사용해보기12.4 useDeferredValue 사용해보기
12.1 useDefferedValue 개요
12.1.1. useDeferredValue이란?
useDeferredValue는 React v18.0에 새로 나온 Hook으로, 리렌더링의 우선순위에 따라 의도적으로 렌더링을 지연할 수 있는 Hook입니다. useDeferredValue는 값의 업데이트 우선순위를 지정하여 우선순위가 높은 작업을 실행하는 동안 useMemo와 같이 기존의 값을 가지고 있으며 업데이트를 지연시킵니다.
이러한 기능은 이후 절에서 다룰 debounce, throttle과 비슷해 보이지만 useDeferredValue Hook을 이용하면 딜레이 시간을 고정하지 않고 더 긴급한 요청이 끝난 후 바로 지연된 렌더링을 실행한다는 이점이 있습니다.
const deferredValue = useDeferredValue(value);
useDeferredValue는 첫 번째 인자로 지연하길 원하는 값을 받고 해당 값의 복사본을 반환합니다.
12.1.2 useDeferredValue 사용법
브라우저에서 더 우선적으로 출력되어야 할 값을 계산하는 동안 useDeferredValue를 이용하여 DOM 트리에서 긴급하지 않은 부분의 리렌더링을 의도적으로 지연시킬 수 있습니다.
예를 들어 현재 실행되는 렌더링이 input에서와 같이 타이핑 즉시 화면에 반영되어야 하는 렌더링이라면 React에서는 이러한 긴급한 렌더링을 완료하기 전까지는 지연된 값을 기존의 값으로 출력하다가, 긴급한 렌더링이 끝나면 새로운 값을 출력하는 것입니다.
간단한 예제를 통해 useDeferredValue 사용법을 알아보겠습니다.
import React, { useState } from 'react'; export default function App() { let heavyArray = new Array(10000).fill(0); const [type, setType] = useState(0); const typeChange = (e) => { setType(e.target.value); }; return ( <div> <input type="text" onChange={typeChange} /> { heavyArray.map(() => { return <div>{type}</div>; })} </div>); }
위 코드는 input에 텍스트를 입력하면 같은 내용이 10000번 출력되는 코드입니다. 간단한 코드지만 사용자가 입력한 모든 값에 대해 10000개의 결과를 리렌더링하면서 성능에 문제가 발생한 것을 확인할 수 있습니다.
이 때 useDeferredValue를 이용한다면
heavyArray
의 출력을 지연시킬 수 있습니다.import React, { useState, useDeferredValue } from 'react'; export default function App() { let heavyArray = new Array(10000).fill(0); const [type, setType] = useState(0); let deferredType = useDeferredValue(type); const typeChange = (e) => { setType(e.target.value); }; return ( <div> <input type="text" onChange={typeChange} /> { heavyArray.map(() => { return <div>{deferredType}</div>; })} </div>); }
같은 코드에서 useDeferredValue를 사용한
deferredType
값을 넣어주면 type
값이 변경되는 시점이 지연되어 해당 값을 사용하여 출력되는 요소들의 렌더링은 후에 처리될 수 있어 성능 개선에 도움을 줄 수 있습니다.이처럼 useDeferredValue를 이용하면 긴급한 렌더링을 우선적으로 실행하도록 원하는 값의 렌더링을 지연시킬 수 있습니다. 하지만 useDeferredValue는 값의 우선순위만을 지정하기 때문에 자식 컴포넌트의 값이나 상태의 업데이트를 지연시키기 위해서는 useMemo 혹은 React.memo를 사용해야 합니다. 이에 대해서는 이후 12.4에서 살펴보도록 하겠습니다.
이어지는 내용으로는 React v18.0 출시 이전에 의도적으로 렌더링을 지연하기 위해 사용하는 기법인 debounce와 throttle를 먼저 살펴본 뒤, useDeferredValue Hook에 대해 직접 예시 코드로 살펴보도록 하겠습니다.
12.2 debounce
12.2.1 debounce란?
스크롤 이벤트 또는 키보드 입력과 같은 연이은 이벤트가 발생하는 경우, 모든 이벤트에 대해 콜백함수가 동작하는 것이 아닌 마지막 이벤트 발생 시점으로부터
setTimeout
으로 설정한 시간만큼 기다리다가 콜백함수가 동작할 수 있도록 하는 기능을 구현할 때 사용됩니다. 대체로 무한 스크롤 또는 검색어 입력과 같은 기능을 구현할 때 주로 사용됩니다. 아래 예제 코드를 통해 보다 자세히 살펴보도록 하겠습니다.12.2.2 debounce 사용해보기
다음은 입력한 숫자의 배수를 찾아주는 예시를 구현한 것입니다.
import { useState, useEffect } from "react"; function App() { const [value, setValue] = useState(""); const [searchResult, setSearchResult] = useState([]); const findNumber = (value) => { const numberGenerater = Array(10000) .fill() .map((value, index) => index + 1); setSearchResult(numberGenerater.filter((num) => !(num % parseInt(value)))); }; useEffect(() => { if (value) { findNumber(value); } else { setSearchResult([]); } }, [value]); return ( <div> <p>입력한 숫자의 배수를 찾아보아요~🎈</p> <form action="" onSubmit={(e) => e.preventDefault()}> <label htmlFor="">입력</label> <input type="number" placeholder="숫자를 입력하세요" value={value} onChange={(e) => setValue(e.target.value)} /> </form> <ul style={{ listStyle: "none" }}> 검색 결과 {searchResult.map((num, index) => ( <li key={index}>{num}</li> ))} </ul> </div> ); } export default App;
위 예제에 대하여 간략히 설명하면 다음과 같습니다.
- input 요소에 값을 넣으면 onChange에 의하여
value
가 변경될 때마다setValue
를 통하여value
값이 변경됩니다.
value
값이 변동됨에 따라 감시하고 있던 useEffect가 동작하게 되고,value
에 값이 있을 경우findNumber
함수가 동작하게 됩니다.
findNumber
함수가 동작함에 따라 10,000개의 값이 들어있는 배열이 생성되고, 그 중value
의 배수인 값들만 “검색결과”에 나타나게 됩니다.
숫자를 input에 입력하면, 값이 입력될 때마다
findNumber
함수가 동작하게 됨을 확인할 수 있습니다. 아래의 그림은 그에 대한 예시입니다.



이제 debounce를 이용한 예제를 살펴보겠습니다.
import { useState, useEffect } from "react"; function App() { const [value, setValue] = useState(""); const [searchResult, setSearchResult] = useState([]); const findNumber = (value) => { const numberGenerater = Array(1000) .fill() .map((value, index) => index + 1); setSearchResult(numberGenerater.filter((num) => !(num % parseInt(value)))); }; useEffect(() => { const debounce = setTimeout(() => { return value ? findNumber(value) : setSearchResult([]); }, 500); return () => clearTimeout(debounce); }, [value]); return ( <div> <p>입력한 숫자의 배수를 찾아보아요~</p> <form action="" onSubmit={(e) => e.preventDefault()}> <label htmlFor="">입력</label> <input type="number" placeholder="숫자를 입력하세요" value={value} onChange={(e) => setValue(e.target.value)} /> </form> <ul style={{ listStyle: "none" }}> 검색 결과 {searchResult.map((num, index) => ( <li key={index}>{num}</li> ))} </ul> </div> ); } export default App;
변경된 부분은 useEffect 부분입니다. 설정 시간인 500ms 동안
findNumber
함수 동작에 지연을 인위적으로 제한합니다. 그리고 500ms가 지나면 debounce의 setTimeout
콜백함수를 초기화 시켜줍니다. 이를 통해 debounce를 고려하지 않았을 때와 같이 “3421”을 input에 입력하였을 때 매 입력에 대해 검색 결과가 나타나는 것이 아니라 “3421”을 모두 입력하고 500ms가 지났을 때에만, 검색 결과가 나타나게 됨을 확인할 수 있습니다.
즉, debounce를 이용하여 설정한 시간만큼 함수의 동작을 지연시킬 수 있고, 이를 통해 불필요한 함수 동작을 방지할 수 있습니다.
12.3 throttle
12.3.1 throttle이란?
throttle은 이벤트를 일정한 주기마다 발생하도록 하는 기법입니다. callback 함수가 설정한 time 뒤에 실행되고, time이 지나기 전에 다시 호출될 경우에는 callback을 실행시키지 않고 함수를 종료하는 형태로 되어 있습니다. 만약, 설정시간을 200ms로 설정하면 해당 이벤트는 200ms 동안 최대 한 번만 발생합니다.
이벤트의 호출을 설정한 시간을 기준으로 일정하게 실행하도록 제한을 두면 과도한 이벤트 핸들러의 실행되는 빈도를 줄여 웹 성능이 저하되는 것을 방지할 수 있는 성능적인 이점을 가져갈 수 있습니다.
12.3.2 throttle 사용해보기
12.3.1절에서 throttle의 개념에 대해 살펴보았습니다. 지금부터는 예제 코드를 통해 일반 이벤트와 throttle을 적용한 이벤트가 어떻게 다르게 동작하는지 살펴보도록 하겠습니다. 아래는 트리거 박스 요소 위에 마우스가 있는 동안 이동할 때마다 각각
raw
상태와 throttled
상태가 1씩 증가하는 예제 코드입니다.import { useCallback, useRef, useState } from 'react'; const App = () => { const [raw, setRaw] = useState(0); const [throttled, setThrottled] = useState(0); const lastRan = useRef(Date.now()); const handleOnMouseMove = useCallback(() => { setRaw((r) => r + 1); if (Date.now() - lastRan.current >= 1000) { setThrottled((t) => t + 1); lastRan.current = Date.now(); } }, []); return ( <div> <div style={{ width: '100px', height: '100px', backgroundColor: 'rosybrown', }} onMouseMove={handleOnMouseMove} > 트리거 영역 </div> <p>일반 이벤트: {raw}</p> <p>쓰로틀 이벤트: {throttled}</p> </div> ); }; export default App;

위의 코드에서 두 상태는 초기 상태로 같은 0으로 시작합니다.
먼저, 일반 이벤트에 대해 살펴보겠습니다.
raw
상태는 트리거 영역 박스 위에서 마우스가 움직일 때마다 handleOnMouseMove
콜백함수가 실행됩니다. 그 결과로 261까지 변화한 것을 확인할 수 있고, 이는 App 컴포넌트가 261번 리렌더링되는 것을 의미합니다. 만약 이벤트 핸들러가 무거운 계산이나 기타 DOM 조작과 같은 작업을 과하게 수행하는 경우 이는 성능 문제가 발생하고 사용자 경험까지 떨어뜨릴 수 있습니다.if (Date.now() - lastRan.current >= 1000) { setThrottled((t) => t + 1); lastRan.current = Date.now(); }
다음으로 throttle을 적용한 이벤트에 대해 살펴보겠습니다.
throttled
상태도 마찬가지로 트리거 영역 박스 위에서 마우스가 움직일 때마다 handleOnMouseMove
콜백함수가 실행됩니다. 하지만, raw
상태와는 다르게 위 코드를 작성하여 if 문에서 1000ms마다 한 번씩 실행되도록 설정했습니다. 따라서 raw
상태가 261로 업데이트 되는 동안 throttled
상태는 3으로 업데이트 되어 과도하게 리렌더링 되는 것을 방지할 수 있습니다.12.4 useDeferredValue 사용해보기
키보드를 활용한 사용자 입력 등 짧은 시간 동안 이벤트가 많이 일어나는 경우 화면이 끊기는 현상이 일어날 수 있습니다. 검색어 자동 완성을 그 예로 들 수 있는데, 사용자 입력 한 번에 검색어 업데이트가 한 번씩 일어난다면 불필요한 리렌더링이 초당 수 번씩 발생하며, 사용자가 원하는 검색어에 대한 자동완성이 느려질 수 있습니다.
사용자가 input 창에 텍스트를 입력하면 해당 텍스트를 10000개를 반복하는 예제를 작성하고, 그 성능을 측정해보겠습니다.
.app { margin: 20px auto; width: 300px; } .input { width: 200px; font-size: 20px; background-color: sandybrown; border-radius: 5px; } .search-div { width: 200px; font-size: 15px; padding: 3px; border: 0.1px solid black; border-radius: 5px; }
import React, { useState, useCallback } from 'react'; import './App.css'; export default function App() { const [name, setName] = useState(''); const onChange = useCallback((e) => { setName(e.target.value); }, []); return ( <div className='app'> <div>검색창</div> <input className='input' value={name} onChange={onChange} /> {name ? Array(10000) .fill() .map((v, i) => ( <div className='search-div' key={i}> {name} </div> )) : null} </div> ); }


사용자는 위 검색창에 ‘Hello world’ 라는 텍스트를 입력하는 동안 직관적으로도 화면의 끊김 현상을 마주할 수 있습니다. 10000개의 텍스트를 반복하는 동안, 불필요한 리렌더링이 발생하기 때문입니다. Chrome에서 제공하는 개발자 도구의 Perfomance 탭에서 ‘Hello world’ 텍스트를 모두 렌더링하는 데 걸린 시간을 확인할 수 있습니다. 2000ms동안 ‘Hello world’ 텍스트가 모두 렌더링 되는 데 걸린 시간은 586ms입니다.
그렇다면 이번엔 useDefferedValue Hook을 사용하여 검색창의 성능을 측정해보도록 하겠습니다.
import React, { useState, useCallback, useMemo, useDeferredValue } from 'react'; import './App.css'; export default function App() { const [name, setName] = useState(''); const deferredName = useDeferredValue(name); const result = useMemo(() => deferredName, [deferredName]); const onChange = useCallback((e) => { setName(e.target.value); }, []); return ( <div className='app'> <div>검색창</div> <input className='input' value={name} onChange={onChange} /> {deferredName ? Array(10000) .fill() .map((v, i) => ( <div className='search-div' key={i}> {result} </div> )) : null} </div> ); }
위 App.jsx에서는 useMemo Hook을 사용하였습니다.
name
이 변경될 때가 아닌, deferredName
이 변경될 때만 리렌더링을 진행하면서 불필요한 리렌더링을 막을 수 있습니다. useDeferredValue 사용 시에 useMemo를 함께 사용하면서 하위 컴포넌트나 상태의 업데이트를 지연시킬 수 있습니다.
이번에는 ‘Hello world’ 라는 텍스트를 입력하는 동안, 10000개의 검색 결과가 출력되는 데 끊김 현상이 훨씬 완화되었음을 체감할 수 있습니다. 또한 개발자 도구에서 총 2000ms동안 렌더링 하는데 걸린 시간은 376ms로, useDefferedValue를 사용하지 않았을 때보다 약 1.5배 빠른 성능을 보여줍니다.
불필요한 리렌더링을 막기 위해 debounce나 throttle 같은 처리를 진행하는데, debounce와 throttle에서 적절한 delay를 선택하는 것은 꽤나 어려운 일입니다.
React v18.0은 fiber라는 엔진을 개선하여 자체적인 스케줄러를 가지게 되었고, 작업의 우선순위를 정하여 순차적으로 처리하는 기능을 내포하게 되었습니다. 낮은 우선순위를 지정하기 위한 useDeferredValue라는 빌트인 훅을 사용하여 위의 예제를 다뤄보고, 성능을 확인하였습니다.