[React] Concurrent Features

박창조·2025년 7월 21일

React

목록 보기
4/9

이 글에서는 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을 생성 하는 예제

https://2471589.playcode.io/

⇒ 입력을 입력할 때 무거운 렌더링으로 인해 입력이 버벅이는 것을 확인할 수 있다.

그래서 도입한 동시성(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이 애플리케이션 성능을 개선하는 방법

React 18 Concurrent Rendering

React v18.0 – React

Automatic batching for fewer renders in React 18 · reactwg react-18 · Discussion #21

React 18 톺아보기 - 04. Concurrent Render | Deep Dive Magic Code

profile
사랑을 꿈꾸는 냄새나는 개발자 입니다 :)

0개의 댓글