React 18: useDeferredValue를 통한 입력창 무겁고 버벅이는 타자 현상 교정
- 원인: 입력값이 바뀔 때마다 무거운 리스트 렌더링이 같은 우선순위로 동기 처리되면서 메인 스레드를 붙잡음
- 해결: 입력 자체는 즉시 반영하고 무거운 렌더링만 뒤로 미루고 싶을 때 →
useDeferredValue/ 하위 트리가 실제로 무거우면React.memo도 같이 적용 - 주의: 렌더링 자체를 빠르게 만들어주는 훅이 아니라 우선순위만 미루는 것 — 계산량이 줄지 않으면 체감 효과가 약할 수 있음
const deferredText = useDeferredValue(text);
<HeavyList query={deferredText} />
↓ 왜 이런 문제가 생기는지 원리 보기
대량의 리스트를 필터링하는 검색창에 타자를 치면 입력창 자체가 버벅이면서 글자가 밀리는 경우가 있습니다. 원인은 입력값이 바뀔 때마다 무거운 리스트 렌더링이 같은 우선순위로 동기 처리되면서 브라우저 메인 스레드를 잠깐 붙잡기 때문인데, React 18의 useDeferredValue는 이 렌더링의 우선순위만 뒤로 미뤄서 입력 반응성을 지켜주는 훅입니다.
입력창 타자 밀림 현상이 발생하는 구조적 원인
일반적인 React 앱에서 입력창의 상태(state)와 화면에 뿌려지는 리스트 데이터가 동일한 입력 흐름에 묶여 있을 때, 유저가 키보드를 입력하면 다음과 같은 현상이 순차적으로 일어납니다.
- 사용자가 텍스트 입력 (키 입력 이벤트 발생)
onChange핸들러가 트리거되며 상태 변경 (setState)- [동기적 리렌더링] 입력값을 가공해야 하는 하위의 무거운 리스트 컴포넌트가 같은 우선순위로 곧바로 다시 렌더링을 시작
- 이 과정에서 브라우저의 싱글 스레드(Main Thread)가 렌더링 계산에 묶여버림
- 브라우저가 다음 키 입력을 즉각 화면에 반영(Paint)하지 못하고 프레임 드랍(Frame Drop) 발생
몇 년 전 외주로 맡았던 프로젝트 중에 수천 건의 상품 데이터를 다루는 필터링 화면이 있었습니다. 검색어를 입력할 때마다 화면이 순간적으로 멈칫거린다는 얘기를 듣고, 처음엔 리스트 쪽 key 값이 잘못됐거나 불필요한 리렌더링이 도는 줄 알고 React.memo부터 여기저기 붙여봤는데 큰 차이가 없었습니다. 나중에 크롬 개발자 도구 성능 탭으로 프레임을 찍어보고 나서야, 문제는 리렌더링 횟수가 아니라 입력값 갱신과 리스트 렌더링이 같은 우선순위로 동기 처리되고 있다는 데 있다는 걸 알았습니다. 디바운스로 우선 급한 불은 껐지만 타자가 빠른 사용자한테는 여전히 살짝 밀리는 느낌이 남아 있었는데, 이후 유지보수를 이어가다가 React 18로 올리는 김에 useDeferredValue로 바꾸고 나서야 입력 자체가 걸리는 느낌이 사라졌습니다.
useDeferredValue의 동작 원리와 특징
useDeferredValue는 값의 업데이트 우선순위를 낮춰 브라우저가 사용자 입력을 먼저 처리할 수 있도록 돕는 훅입니다.
- 우선순위 분리: 텍스트 입력처럼 즉각적인 피드백이 필요한 '긴급(Urgent) 업데이트'는 먼저 처리하고, 데이터 필터링·렌더링처럼 시간이 걸리는 작업은 백그라운드에서 나중에 처리합니다.
- 중단 가능한 렌더링: 지연된 값을 렌더링하는 도중에 사용자가 새로운 키보드 입력을 하면, React는 진행 중이던 렌더링 작업을 그대로 버리고 새 입력을 먼저 처리합니다.
- 별도 타이머 불필요:
lodash의 debounce 같은 외부 라이브러리나setTimeout없이 리액트 내장 스케줄러로 처리됩니다.
useDeferredValue 실전 코드 예제
아래 예제는 입력창 타자 지연을 해결하기 위해 useDeferredValue를 적용한 구현입니다. 지연 렌더링이 진행 중일 때 리스트를 살짝 흐리게 표시해서, 화면이 최신 입력을 아직 못 따라간 상태라는 걸 사용자에게 알려주는 패턴도 함께 넣었습니다.
import { useState, useDeferredValue, useMemo } from 'react';
// 성능 부하가 예상되는 무거운 리스트 컴포넌트
function HeavyList({ query }) {
const items = useMemo(() => {
const list = [];
// 5,000개의 무거운 아이템 시뮬레이션
for (let i = 0; i < 5000; i++) {
if (i.toString().includes(query) || query === '') {
list.push(<li key={i}>검색 아이템 #{i} (매칭어: {query})</li>);
}
}
return list;
}, [query]);
return <ul>{items}</ul>;
}
export default function App() {
const [text, setText] = useState('');
// 1. 입력 상태 값의 지연 버전 생성
const deferredText = useDeferredValue(text);
// 2. 최신 입력을 아직 못 따라간 상태인지 판별
const isStale = text !== deferredText;
return (
<div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
<h3>useDeferredValue 데모</h3>
{/* 3. 입력창은 지연되지 않은 기존 text 상태와 바인딩 */}
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="검색어를 입력해보세요..."
style={{
padding: '10px 14px',
width: '100%',
fontSize: '16px',
borderRadius: '6px',
border: '1px solid #ccc',
boxSizing: 'border-box'
}}
/>
{/* 4. 지연 중일 때는 살짝 흐리게 표시해 갱신 대기 상태임을 알려줌 */}
<div style={{ opacity: isStale ? 0.5 : 1, transition: 'opacity 0.2s' }}>
<HeavyList query={deferredText} />
</div>
</div>
);
}
코드 동작 해설
deferredText는 text가 바뀔 때 곧바로 갱신되지 않고, React가 우선순위가 낮은 렌더링 단계에서 뒤늦게 반영합니다. 그 사이 text와 deferredText가 서로 다른 값을 가리키는 순간이 생기는데, 이 차이를 isStale로 판별해서 리스트를 흐리게 보여주면 사용자는 "지금 갱신 중이구나"를 자연스럽게 인지할 수 있습니다.
참고로 useDeferredValue는 아래쪽 렌더링을 더 빠르게 만들어주는 훅이 아니라, "언제 그 렌더링을 실행할지" 우선순위만 뒤로 미루는 훅입니다. 하위 컴포넌트가 실제로 무겁다면 React.memo로 감싸서 props가 그대로일 때 리렌더 자체를 건너뛰게 해줘야 체감 효과가 확실합니다. 위 예제의 HeavyList는 useMemo로 계산 결과를 이미 캐싱해 두었기 때문에 그것만으로 충분하지만, 컴포넌트 트리가 더 복잡해지는 경우라면 memo도 같이 고려하는 게 안전합니다.
입력 최적화 기법 비교
프로젝트 규모와 성격에 따라 골라 쓸 수 있는 방법을 정리했습니다.
| 구분 | 디바운스 (Debounce) | 쓰로틀 (Throttle) | useDeferredValue (React 18) |
|---|---|---|---|
| 작동 원리 | 특정 시간 동안 입력이 없을 때 한 번 실행 | 설정한 주기마다 최소 한 번 렌더링 실행 | React 스케줄러가 우선순위에 따라 렌더링 시점을 조정 |
| 사용자 경험 | 입력이 멈출 때까지 결과 화면이 갱신되지 않음 | 일정 주기로 화면이 끊기듯 갱신됨 | 입력창은 항상 즉시 반응하고, 결과는 뒤이어 갱신 |
| 기기 성능 대응 | 고정 지연 시간(예: 300ms)으로 하드코딩 | 기기 사양과 무관하게 고정된 주기 | 저사양 기기일수록 렌더링 자체가 오래 걸려 지연이 더 길게 체감됨 |
| 구현 방식 | 외부 라이브러리(lodash) 또는 커스텀 훅 필요 |
외부 라이브러리 또는 useRef 기반 구현 필요 |
React 내장 훅 한 줄로 적용 (단, 하위 트리가 무거우면 memo 병행 권장) |
React 18 useDeferredValue API 스펙과 동시성 렌더링에 대한 상세 내용은 공식 문서에서 확인할 수 있습니다.
React 18 동시성 렌더링과 useDeferredValue를 좀 더 자세히 다룬 글은
[호마다의 IT 개발 입문 블로그]
에서 볼 수 있습니다.
댓글
댓글 쓰기