일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- Baekjoon
- JS
- 리액트
- next13
- lodash
- 제로베이스
- Next.js13
- 자바스크립트 알고리즘 문제
- NPM
- stack문제
- 자바스크립트
- 타입스크립트
- react
- 프론트엔드
- 프로그래머스
- 알고리즘문제풀이
- leetcode
- leetcode문제풀이
- CSS
- JavaScript
- 자바스크립트 연결리스트
- 자바스크립트 알고리즘
- 자바스크립트 문제풀이
- til
- 리액트쿼리
- Next
- 자바스크립트 문제
- 자바스크립트코딩테스트
- 자바스크립트 문제 풀이
- HTML
- Today
- Total
코드노트
React의 useTransition 가이드 정리, 동작 흐름 확인해보기 본문
useTransition이란?
useTranstion은 React18에서 도입된 Concurrent Rendering 기능의 일부로, UI 반응성과 성능을 개선하기 위해
"덜 중요한 작업을 늦춰서 처리" 하는 훅이다.
사용자가 빠르게 반응을 체감해야하는 작업이 있다면 가벼운 작업과 무거운 작업을 분리 할 수 있으며, UI의 지연(Lag)을 줄이는데 효과가 있다.
- 가벼운 작업 예시 : 버튼 클릭, 입력 필드 타이핑 등
- 무거운 작업 예시 : 목록 필터링, 차트 업데이트 등
언제 사용할까?
- 대량 데이터 필터링, 렌더링, 정렬
- 애니메이션, 차트, 목록 등 복잡한 UI 업데이트
- 사용자의 입력 반응은 빠르게 결과는 나중에 처리하고 싶을 때
사용하지 않아도 되는 경우
- 상태 업데이트가 가벼운 경우
- 비동기 fetch 작업 위주로 구성될 때
- 전체 앱이 단순한 상태 흐름을 가질 때
사용법은 간단하다.
const [isPending, startTransition] = useTransition();
startTransition(() => {
// 우선순위가 낮은 상태 업데이트
setSomeState(data);
});
- isPending : 현재 transiton중인 상태를 나타낸다. ( true / false )
- startTransition : 작업을 시작하는 함수
예제 실시간 검색 필터링
import { useState, useTransition } from 'react';
const largeList = [...Array(10000)].map((_, i) => ({
id: i,
label: `Item ${i}`,
}));
function SearchInput() {
const [input, setInput] = useState('');
const [filteredList, setFilteredList] = useState([]);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const keyword = e.target.value;
setInput(keyword);
startTransition(() => {
const filtered = largeList.filter(item =>
item.label.toLowerCase().includes(keyword.toLowerCase())
);
setFilteredList(filtered);
});
};
return (
<div>
<input value={input} onChange={handleChange} />
{isPending && <p>검색 중...</p>}
<ul>
{filteredList.map(item => <li key={item.id}>{item.label}</li>)}
</ul>
</div>
);
}
- 검색어가 입력될 때마다 handleChange가 실행되지만 setInput으로 input상태를 업데이트 한다.
- startTansition을 통해서 입력 반영 이후에 필터링은 나중에 처리되도록 진행한다.
- 이때 isPending 값을 통해서 검색중이라는 UI를 유저에게 보여지게 할 수 있다.
- React Scheduler가 직접 우선순위를 조절하고 렌더링 시점까지 관리된다.
만약 debounce를 사용해도 되는거 아니야? 라고 할 수 있지만
import { useState, useMemo, useEffect } from 'react';
import debounce from 'lodash.debounce';
function SearchDebounced({ data }) {
const [input, setInput] = useState('');
const [filtered, setFiltered] = useState([]);
const debouncedSearch = useMemo(() =>
debounce((keyword) => {
const result = data.filter(item =>
item.label.toLowerCase().includes(keyword.toLowerCase())
);
setFiltered(result);
}, 300), [data]
);
useEffect(() => {
debouncedSearch(input);
}, [input, debouncedSearch]);
return (
<>
<input value={input} onChange={(e) => setInput(e.target.value)} />
<ul>
{filtered.map((item) => <li key={item.id}>{item.label}</li>)}
</ul>
</>
);
}
- 이렇게 debounce를 사용하게 되면 300ms동안 입력이 멈추면 그때 filter로직이 실행되게 된다.
- 필터 함수 호출 횟수가 줄어들기 때문에 성능에는 좋지만 입력 즉시 반응하지 않기 때문에 UX에서는 느린걸 느낄 수 있다.
- 그 외에도 로딩 상태 추가하기 위해서는 상태하나를 더 추가해야하는 번거로움이 필요하다.
그럼 debounce말고 무조건적인 useTransition이 좋은걸까?
그건 또 아니다.
만약 외부 api를 줄여야하는 경우가 있다면?
-> 검색시마다 api요청을 진행하게 된다면 debounce를 사용해서 api호출 횟수를 줄이는게 바람직 하다.
다시 말하지만 useTransition은 단순하게 지연을 시키는 것이 아니라 React 내부에서 상태 업데이트의 우선순위를 낮춰서 UI가 끊기지 않게 해주는 스케줄링 기술이다.
그렇다면 코드레벨을 통해서 useTransition의 동작 흐름을 살펴 보자!
const [isPending, startTransition] = useTransition();
- useTransition의 리턴하는 구조이다.
// 내부 구조 유추 형태 (단순화)
function useTransition() {
const [isPending, setPending] = useState(false);
function startTransition(callback) {
setPending(true);
// 실제 업데이트 예약
unstable_runWithPriority(LowPriority, () => {
callback();
setPending(false);
});
}
return [isPending, startTransition];
}
- 내부적으로 실행되는 코드를 단순하게 살펴보면 React는 내부적으로 state의 queue 상태를 가진 객체를 만든다.
- 실제로는 react-reconciler, scheduler, react-dom 패키지가 함께 작동한다.
// scheduler 구현 일부를 단순화한 예
unstable_runWithPriority(LowPriority, () => {
// → 이 안에 있는 state 업데이트는 낮은 우선순위로 처리됨
setFilteredList(filteredList);
});
- startTransition으로 감싸면 React는 그 안의 state update를 "low priority update"로 등록한다.
- 이렇게 진행되면 즉시 렌더링 되는게 아니라 긴급(urgent)한 작업들이 먼저 끝나기를 기다리게 된다.
- 여기서 핵심은 Scheduler가 이 업데이트를 바로 처리하지 않는 다는 것이다. React 18을 보면 새롭게 도입된 Concurrent Mode의 핵심 기능 중 하나이다.
React의 작업 우선 순위
// 내부 우선순위 enum
export const ImmediatePriority = 1;
export const UserBlockingPriority = 2;
export const NormalPriority = 3;
export const LowPriority = 4;
export const IdlePriority = 5;
- 보통 setState는 Normal ~ UserBlocking 수준으로 바로 처리되지만, startTransition()안에 있는 작업은 LowPriority로 들어간다.
function flushWork(hasTimeRemaining, currentTime) {
// 시간이 남을 때만 low priority 작업 실행
if (hasTimeRemaining || currentTime > deadline) {
// do work
} else {
// 중단하고 urgent 작업 대기
}
}
그럼 React 렌더링 중간에 지금 여유가 있는지 확인하고 Idle time이나 frame time이 안남았을때까지 기다렸다가 처리한다.
동작 순서
[사용자 입력]
↓
(setInput) --> High Priority → 즉시 렌더링
↓
(startTransition 안의 setFilteredList) → Low Priority → 나중에 렌더링
debounce, useTransition 차이점 정리
항목 | debounce | useTransition |
실행 타이밍 | delay 이후 1번 실행 | 상태는 즉시 반영, 렌더링은 나중에 |
목적 | 함수 호출/요청 빈도 줄이기 | 렌더링 병목 분산 및 반응성 개선 |
사용 범위 | 네트워크 요청 최적화, 단순 함수 제어 | React 렌더링 우선순위 스케줄링 |
UX에 미치는 영향 | 입력에 약간의 지연 있음 | 입력은 즉시 반영됨 |
내부 메커니즘 | setTimeout, clearTimeout 기반 | React Scheduler + Fiber 기반 |
이제 UI적으로 왜 유리한지 이해 되는것 같다.
- 입력 값 변화는 즉시 화면에 반영된다!
- 그러나 filteredList는 나중에 반영되기 때문에 렌더링 비용이 분산된다!
- 렌더링 프레임을 블로킹하지 않고 부드러운 사용자 경험을 제공할 수 있다!
마무리 해보자면
- useTransition은 React 내부의 Scheduler와 Fiber구조를 활용해 렌더링 우선순위를 조절하는 비동기 렌더링 기술이다.
- startTransition 내부의 업데이트는 낮은 우선순위로 처리되어, 사용자 입력에 대한 UI응답을 빠르게 하고, 무거운 렌더링은 나중에 처리한다.
- debounce와 달리, 호출 타이밍 제어가 아니라 렌더링 제어에 특화된 방식이다.
'Code note > 리액트' 카테고리의 다른 글
리액트 네이티브 절대 경로 지정 Feat. babel (0) | 2024.04.20 |
---|---|
ref 여러개 하나의 ref로 관리, mergeRefs 함수 정리, feat, ForwardedRef (0) | 2024.04.18 |
리액트 네이티브 TextInput 기능 정리, RN (0) | 2024.04.17 |
리액트쿼리를 쓴다면 이건 꼭 알고 쓰자 (0) | 2024.03.20 |
리액트 가상돔 Virtual DOM 정리 (0) | 2023.08.04 |