리액트 동시성 모드는 UI 업데이트의 우선순위를 자동으로 관리해 중요한 인터렉션을 끊김 없이 처리하는 렌더링 방식이다.
•예를 들어, 사용자 입력이나 애니메이션 같은 중요한 작업은 즉시 처리하고, 덜 중요한 렌더링 작업은 뒤로 미루거나 중단·재개할 수 있어 사용자 경험이 크게 향상된다.
•쉽게 말해, 사용자 경험을 최우선으로 생각하는 똑똑한 렌더링 모드라고 말할 수 있다.
React 18버전에서 도입된 동시성 API
•startTransition / useTransition:
화면의 중요한 업데이트와 덜 중요한 업데이트를 구분하여, 덜 중요한 업데이트는 우선순위를 낮춰 처리
입력 이벤트는 즉시 반영하고, 서버에 데이터를 요청하는 등의 작업은 startTransition 안에 넣어 우선순위를 낮춤
•useDeferredValue:
렌더링이 무거운 컴포넌트에 전달할 값을 의도적으로 지연시켜 UI 성능 저하를 방지
실시간 입력값과 별개로 느린 컴포넌트에 전달할 값을 useDeferredValue로 래핑해서 렌더링 우선순위를 낮춤
•Suspense:
비동기 작업(예: 데이터 로딩)이 완료될 때까지 컴포넌트 렌더링을 잠시 멈추고, 대신 사용자에게 로딩 fallback UI를 보여줌
• 자동 배칭(Automatic Batching) 및 interruptible rendering: 내부적으로 렌더링 작업을 쪼개고 우선순위를 조절해 끊김 없는 UI를 지원
왜 동시성 모드가 필요한가?
React 18 이전의 동기 렌더링 모델은 한 번 렌더링이 시작되면 끝날 때까지 중단할 수 없었다.
•UI가 복잡해지고, 화면 렌더링 시간이 브라우저 프레임 예산(16ms)을 초과하면 입력 지연, 스크롤 버벅임 등의 문제 발생
•이런 문제를 개선하기 위해 렌더링 작업을 중단, 재개, 우선순위 변동이 가능한 동시성 모델이 등장
사용자에게 가장 중요한 입력, 클릭, 애니메이션은 가장 먼저 처리하고, 중요도가 낮은 UI 업데이트는 나중에 처리함으로써 반응성을 크게 높였다.
startTransition
사용자의 입력값 (input, click..)은 즉시 UI에 반영되고, 덜 중요한 업데이트는 startTransition안에 우선 순위를 낮게 처리하도록 설정한다.
// input change handler
const handleChange = (e) => {
const value = e.target.value;
// 사용자의 입력값은 즉시 UI에 반영
setInput(value)
// concurrent fn
startTransition(() => {
// 비교적 중요하지 않은 상태 업데이트, api 호출 등
// 우선순위가 낮은 작업 위치
const data = await fetch(value)
setList(data)
})
}
useTransition이라는 훅도 추가되었다. startTransition안에 설정한 UI 업데이트에 로딩 UI가 필요 할 경우 선언하여 사용할 수 있다.
const [isPending, startTransition] = useTransition();
useDeferredValue
입력값처럼 긴급하게 업데이트되어야 하는 상태와는 별개로, 렌더링이 무거운 컴포넌트에 전달하는 값을 지연시켜 UI 성능 저하를 방지한다.
간단하게 설명하자면 의도적으로 렌더링을 지연 시키고 싶은 컴포넌트가 있다면 props로 전달할 값을 useDeferredValue로 래핑하고 전달하면 된다. 이때 지연 상태를 UI로 보여주고 싶다면 Suspense를 사용하여 fallback UI를 보여줄 수 있다.
function SlowList({ query }) {
const filtered = ["apple", "banana", "orange"].filter((item) =>
item.includes(query)
);
return (
<ul>
{filtered.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
);
}
export default function App() {
const [text, setText] = useState("");
const deferredText = useDeferredValue(text); // 값 지연 처리
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
{/* Suspense 로딩 UI */}
<Suspense fallback={<div>loading...</div>}>
<SlowList query={deferredText} /> {/* 느린 리스트에 지연 값 전달 */}
</Suspense>
</div>
);
}
Suspense
비동기 작업이 완료될 때까지 컴포넌트 렌더링을 잠시 멈추고, 대신 사용자에게 fallback UI를 보여줄 수 있게 하는 기능이다.
비동기 데이터 로딩이나 컴포넌트 지연 로딩 상황에서, 아직 준비되지 않은 컴포넌트를 만나면 React가 렌더링을 잠시 멈춘다. 그렇게 멈춰 있는 동안 Suspense 컴포넌트에서 설정한 fallback UI (스피너, 로딩 메시지)를 대신 보여주고 비동기 작업이 완료되면 fallback UI를 제거하고 정상적으로 컴포넌트 렌더링한다.
{/* 비동기가 끝날 때까지 fallback UI 렌더 */}
<Suspense fallback={<div>loading...</div>}>
<SlowList query={deferredText} />
</Suspense>
마무리
프로젝트를 하면서 Suspense는 비동기로 데이터를 패칭해오는 컴포넌트를 감싸 스트리밍 렌더링을 구현할때 많이 사용해 봤지만 startTransition이나 useDefferedValue를 사용해보지 않았었다. 지금 생각보면 이 기능들을 적용할 부분들이 몇 군데 생각이 나는 것 같다. React 동시성 모드로 UI 렌더링 작업의 우선순위를 개발자가 직접 제어하면 더 높은 수준의 사용자 경험을 제공할 수 있을 것 같다. 빨리 써봐야지~