Concurrent Features
이 글에서는 React 18 에서 도입된 배경과 함께 Concurrent Features 을 살펴보고, 관련된 기능인 Transition, stream SSR, Suspense 등 내용을 알아본다.
Concurrent Features 은 무엇인가 ❓
Concurrent Features 는 2022년 3월에 발표된 React 18 버전에서 정식으로 도입된 새로운 렌더링 메커니즘 이다.
Concurrent Features 도입으로 React의 렌더링을 더 유연하고 효율적으로 동작하게함으로서 복잡하고 무거운 UI 작업에서도 빠르고 부드러운 사용자 경험을 제공한다.
이전 렌더링 방식의 한계 : Blocking, Synchronous
React 18 이전에는 렌더링은 중간에 개입할 수 없도록 “동기적으로 동작하였다.” 렌더링이 한번 시작되면 중간에 해당 작업을 중단하거나, 수정할 수 없었다.
게다가 싱글 스레드로 동작하는 자바스크립트이기에 기존 방식 대로 렌더링이 되었을 때 다음과 같은 문제가 발생했다.
- 대용량 데이턴, 복잡한 UI로 인해 렌더링이 오래 걸린다면 응답 지연
- 메인 스레드를 점유하면서, 다음에 수행해야 하는 작업은 Blocking 되어 상호작용 저하
- 화면 전환 시 로딩 지연, 앱이 버벅이는 현상 발생
예를 들어 기존 방식에서는 사용자 입력 중에 무거운 렌더링이 발생하면, 버벅이면 입력이 멈추는 것처럼 느껴졌었다.
다음은 사용자 입력마다 50000개의 DOM을 생성 하는 예제
⇒ 입력을 입력할 때 무거운 렌더링으로 인해 입력이 버벅이는 것을 확인할 수 있다.
그래서 도입한 동시성(Concurrency)
**React의 렌더링 엔진(Fiber)**을 확장하여 동기/비동기 작업을 구분하고, 병렬로 처리 가능한 환경을 제공하도록 고안하고, 도입한 것이 “동시성” 개념이다.
**동시성(Concurrency)**은 여러 작업이 마치 동시에 실행되는 것처럼 보이는 것 처럼 동작하는 것을 의미한다. 운영체제를 공부하면서 흔하게 듣게 되는 “동시성”과 같은 의미이다.
실제로는 한 번에 하나의 작업만 처리하지만, 여러 조각으로 나누어 매우 짧은 시간 간격으로 작업을 번갈아가면서(Context Switch) 하기 때문에 여러 작업을 “동시에” 처리하는 것 처럼 느낄 수 있다.
Concurrency 도입이 가져온 변화
React 팀에서는 이야기 한 바에 따르면, 우선순위 큐 및 다중 버퍼링 등을 통해 Concurrent Feature을 도입하였다고 이야기 한다. Concurrency 방식을 도입함으로서 크게 다음과 같은 변화를 가져왔다.
- 렌더링 도중에도 다른 작업으로 전환(Interrupt) 가능 (중단 가능)
- React는 렌더링이 중단되더라도 UI가 일관되게 표시되도록 보장
- 상태 업데이트나 렌더링 작업을 우선순위별로 스케줄링 가능
- 작업이 동시에 처리되는 것 처럼 느낌
즉, 사용자와 직접 연결된 작업(클릭, 입력)은 빠르게 처리 하고, 오래 걸리는 작업(리스트 필터링, API 응답 반영)은 천천히 처리 하며 사용자 경험을 높였다.
👈🏻 이전 React
[A 작업 시작] → [A 완료] → [B 시작] → [B 완료]
💫 Concurrent Feature 적용 시
[A 작업 시작] → [B가 급함!] → [B 먼저 처리] → [A 마저 처리]
주요 변화
1️⃣ 자동 배칭(Automatic Batching)
“여러 State 업데이트를 하나의 리렌더링으로 그룹화”
이전에는 한번의 이벤트에서 여러번의 setState를 호출하게 되면, 각각 렌더링이 발생했다면, Automatic Batching 를 도입함으로 각각의 State 업데이트를 하나의 렌더링으로 묶어 한번만 발생하도록 하였다.
// 변경 전 : Batching 없음
setTimeout(() => {
setCount(c => c + 1); // 렌더링 1
setText('hello'); // // 렌더링 2
}, 1000);
// 변경 전 : React 이벤트 핸들러 내부 에서는 Batching
useEffect(() => {
setCount(1);
setText('hello');
// 렌더링
}, []);
위와 같이 변경 전에는 useEffect() 같은 React의 네이티브 핸들러 안에서는 batching 처리가 되고 있었지만, setTimeOut 이나 Promise 안에서는 각 State 업데이트마다 한 번씩, 총 두 번 렌더링 되었다.
// 변경 후: 네이티브 이벤트 핸들러 또는 다른 이벤트들이 Batch
setTimeout(() => {
setCount(c => c + 1);
setText('hello');
// 렌더링 발생
}, 1000);
Concurrency 가 적용된 이후에는 React 네이티브 핸들러 뿐만아니라, setTimeout, EventListener, Promises 등 비동기 처리 내에서 상태를 자동으로 Batch 처리 해줌으로서 렌더링이 한번만 발생하게 된다.
2️⃣ 트랜지션(Transitions)
“ 긴급한 업데이트와 긴급하지 않은 업데이트를 구분하기 위한 React의 새로운 개념”
- 긴급한 업데이트 : 입력, 클릭, 누르기 등과 같은 직접적인 상호작용
- Transition 업데이트(무거운 UI 작업) : DOM 수정, 네트워크 응답을 UI 에 적용 하는 경우
입력, 클릭과 같은 상호작용은 즉각적인 반응이 필요할 때가 있다. 간혹 “최신순 버튼”, “검색 필터링” 등 과 같은 곳에서 상호작용이 일어났을 때, 느리게 진행되는 작업으로 인해 곧바로 반응이 일어나지 않으면 **“잘못 됐다”**고 느끼게 된다.
이럴 때 Transition 기능을 통해 “빠르게 반응해야 할 작업과느려도 되는 작업을 구분해서 처리” 할 수 있도록 할 수 있다.
startTranstion()
Transition을 시작하는 기본 메서드
const handleChange = (e) => {
const value = e.target.value;
setText(value);
// 트랜지션 직접 사용
startTransition(() => {
// 긴급하지 않는 작업 처리
const filtered = items.filter((item) =>
item.toLowerCase().includes(value.toLowerCase())
);
setList(filtered);
});
};
다음과 같이
useTransition()
보류 중인 Pending State를 추적하는 값을 포함하여 Transition을 시작하는 Hook.
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const value = e.target.value;
setInput(value);
// 필터링 작업은 우선순위 낮게 처리
startTransition(() => {
// 긴급하지 않는 작업 처리
const filtered = items.filter((item) =>
item.toLowerCase().includes(value.toLowerCase())
);
setList(filtered);
});
};
3️⃣ useDeferredValue
“**입력값은 즉시 반영하되, 그 값을 사용하는 연산이나 렌더링은 지연시켜** UI의 반응성을 높이는 개념“
사용자가 입력할 때 input 은 바로 업데이트되지만, 비싼 연산은 deferredInput 으로 조금 늦게 처리됨
const [input, setInput] = useState("");
const deferredInput = useDeferredValue(input);
const results = useMemo(() => slowFilter(input), [deferredInput]);
4️⃣ Suspense 확장
비동기 작업(데이터 패칭, 코드 스플리팅 등) 이 완료될 때까지 대체 UI(fallback)를 보여주는 기능
기존 Suspense의 한계
- 코드 스플리팅(lazy loading) 을 적용하는 컴포넌트에서만 사용 가능
- 직접적으로 데이터 패칭을 기다릴 수는 없음 (라이브러리 필요)
- SSR과 완전히 호환되지 않음
개선점
- 데이터 패칭과 직접 연결 (라이브러리와 통합)
- 서버에서
Suspense를 감지해 fallback을 단계적으로 스트리밍 전송 - 여러 개의
Suspense가 중첩되어 있을 때, 각기 다른 부분만 개별적으로 fallback 처리 가능 - render 중 fallback → 본 콘텐츠로 자연스럽게 전환 가능 (interruptible rendering)
function UserProfile({ id }) {
const { data } = useQuery({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
suspense: true,
});
return <div>{data.name}</div>;
}
function App() {
return (
<Suspense fallback={<p>사용자 정보를 불러오는 중...</p>}>
<UserProfile id={1} />
</Suspense>
);
}
5️⃣ Streaming SSR
React 18에서 지원하는 새로운 SSR 방식 ”HTML을 스트리밍 방식으로 클라이언트에 점진적으로 전달하는 기술”
🔁 전통적 SSR vs Streaming SSR
| 구분 | 전통 SSR (React 17 이전) | Streaming SSR (React 18) |
|---|---|---|
| 렌더링 방식 | 모든 컴포넌트가 렌더된 후 HTML 응답 전송 | 렌더링되는 대로 HTML 조각 조각 전달 |
| 초기 로딩 시간 | 길어짐 (모두 준비될 때까지 대기) | 짧아짐 (부분적으로 즉시 렌더링 가능) |
| 사용자 체감 속도 | 느림 | 빠름 |
| - React는 렌더링 작업을 “우선순위에 따라” 나누어, 중요한 부분 먼저, 나머지는 나중에 스트리밍 | ||
| - 사용자에게 즉시 중요한 UI를 보여주고, 덜 중요한 부분은 이후에 점진적으로 채워지는 경험을 제공 | ||
| - 사용자에게 더 빠른 피드백 제공 | ||
| - 중요 콘텐츠를 먼저 보여주는 전략 가능 | ||
| - 대규모 앱에서 UX/성능 모두 개선 가능 |
Reference
(번역) React 18이 애플리케이션 성능을 개선하는 방법
Automatic batching for fewer renders in React 18 · reactwg react-18 · Discussion #21
React 18 톺아보기 - 04. Concurrent Render | Deep Dive Magic Code
함께 보면 좋은 콘텐츠
- Engineering Guide
React Complier에 대하여
2025년 10월 1일 React 19.2가 정식으로 출시가 되었다. 더불어 10월 7-8일에 “React Conf 2025” 가 진행되면서 최신 React에 대한 새로운 주제가 많이 발표되었다.
- Engineering Guide
Streaming SSR
최근 계속 프론트엔드 개발자로 최적화된 사용자 경험을 위한 기술과 렌더링 방식에 대해 공부하며, 관련 개념을 정리하고 있다.
- Engineering Guide
Suspense 동작 원리
지난 포스팅에서 Suspense에 대해 간단하게 알아보았다.