본문 바로가기

개발자 강화/프론트엔드

[매일메일] React의 Concurrent Mode(동시성 모드) (FE.250207)

동시성 모드

여러 작업을 비동기적으로 동시에 처리면서, 중간에 중요한 작업이 들어오면 우선순위를 바꿔서 작업을 먼저 처리

 

이전의 리액트는 스택구조여서, 한 번 렌더링을 시작하면 끝까지 멈추지 않고 다 처리함

그러나 리액트 동시성 모드는 중간에 멈추거나 작업을 뒤로 미뤄두며 중요한 작업을 먼저 끝낼 수 있음

 

동시성을 활용한 기능

useTransition

isPending: 대기 중인 Transition이 있는지

startTransition: 상태 업데이트를 Transition으로 표시할 수 있게 함

import { useState, startTransition } from "react";

function SearchComponent() {
  const [query, setQuery] = useState("");
  const [filteredResults, setFilteredResults] = useState([]);

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const value = event.target.value;
    setQuery(value); // 즉시 업데이트 (높은 우선순위)

    startTransition(() => {
      // 덜 중요한 업데이트 (낮은 우선순위)
      const results = fetchFilteredResults(value); 
      setFilteredResults(results);
    });
  };

  return (
    <div>
      <input type="text" value={query} onChange={handleChange} />
      <ul>
        {filteredResults.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

 

낮은 우선순위 상태 업데이트를 비동기적으로 처리할 수 있음

- 사용자가 즉각적인 응답을 기대하는 작업(입력 필드 업데이트, 버튼 클릭)

- 덜 급한 작업(리스트 필터링, 검색 결과 업데이트)

 

검색 입력창 타이핑은 즉시 입력값 반영하고(query, 사용자 입력 시 즉시 업데이트),

필터링된 결과 리스트 업데이트는 덜 중요한 작업으로 처리함(입력 후 약간 지연이 있어도 됨)

 

useDeferredValue

일부 UI 업데이트를 지연시킬 수 있는 React Hook

사용자가 빠르게 입력할 때마다 리렌더링 되지 않게 최적화할 수 있음

 

매개변수

value: 지연시키려는 값.

initialValue: 컴포넌트 초기 렌더링 값. 생략 시 초기 렌더링에는 useDeferredValue의 지연 효과 없음.

 

반환값

currentValue: value에 업데이트가 발생하면 먼저 이전 값으로 리렌더링하고, 백그라운드에서 새 값으로 리랜더링.

import { useState, useDeferredValue } from 'react';

function SearchPage() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  // ...
}

업데이트 발생 시

deferredQuery(지연된 값)은 최신 query(값)보다 뒤쳐짐. (이전 값으로 리렌더링하기 때문)

React는 deferredQuery(지연된 값)을 업데이트하지 않은 채로 렌더링한 후, 백그라운드에서 새로 받은 값으로 리렌더링함

 

 

Suspense에 deferred Query 적용해서 개선하기

(이 블로그 관련 글: Suspense란? https://developer-dreamer.tistory.com/158 )

1. Suspense만 적용한 경우

import { Suspense, useState } from 'react';
import SearchResults from './SearchResults.js';

export default function App() {
  const [query, setQuery] = useState('');
  return (
    <>
      <label>
        Search albums:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</h2>}>
        <SearchResults query={query} />
      </Suspense>
    </>
  );
}

 

input 창에 입력 -> loading(1s) -> 결과 업데이트 -> input 창 입력 -> loading(1s) -> 결과 업데이트

업데이트된 query로 새로운 값을 탐색해 반환하는 작업을 하는동안 Suspense의 fallback UI를 보여준다

 

2. Suspense와 useDeferredValue를 함께 적용한 경우

import { Suspense, useState, useDeferredValue } from 'react';
import SearchResults from './SearchResults.js';

export default function App() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  return (
    <>
      <label>
        Search albums:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</h2>}>
        <SearchResults query={deferredQuery} />
      </Suspense>
    </>
  );
}

query는 즉시 업데이트되므로 input에 새 값이 표시됨

deferredQuery는 데이터가 로딩될 때까지 이전 값을 유지하므로 SearchReults는 잠시 동안 이전 값을 표시함

 

input 창 입력 -> 잠시 지연(1s) -> 결과 업데이트 -> input 창 입력 -> 이전 값 유지(1s) -> 결과 업데이트

 

fallback ui 대신 이전 검색 값이 표시되다가, 새로운 값으로 업데이트 됨

 

import { Suspense, useState, useDeferredValue } from 'react';
import SearchResults from './SearchResults.js';

export default function App() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;
  return (
    <>
      <label>
        Search albums:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</h2>}>
        <div style={{
          opacity: isStale ? 0.5 : 1,
          transition: isStale ? 'opacity 0.2s 0.2s linear' : 'opacity 0s 0s linear'
        }}>
          <SearchResults query={deferredQuery} />
        </div>
      </Suspense>
    </>
  );
}

 

input 창 입력 -> 잠시 지연(1s) -> 결과 업데이트 -> input 창 입력 -> 이전 값 유지+흐려짐(1s) -> 결과 업데이트

 

input 창에 새로운 입력값이 입력됨과 동시에 이전에 표시된 결과 값이 흐려짐(회색으로 변함)

새로운 결과가 반환되면 새로운 결과 표시와 함께 다시 텍스트 색이 검은색으로 돌아옴

 

현재 보고 있는 값이 사용자가 방금 입력한 검색에 대한 결과가 아니라 오래된 값임을 시각적으로 표시함

 


function App() {
  const [text, setText] = useState('');
  const deferredText = useDeferredValue(text);
  return (
    <>
      <input value={text} onChange={e => setText(e.target.value)} />
      <SlowList text={deferredText} />
    </>
  );
}

 

지금까지는 입력은 즉시 -> 결과 업데이트는 조금 느리게 방법을 썼다면

반대로 입력을 느리게 반영하면서 결과 업데이트도 함께 반영하는 방법도 있다

 

입력과 결과 업데이트에 속도 차가 발생하면 사용자가 봤을 때 화면의 동작이 뚝 끊기는 느낌이 들 수 있다

하지만 본인이 타이핑한 값이 조금 화면에 늦게 뜨는 대신, 입력한 값이 인풋창에 표시됨과 동시에 결과도 업데이트 되면

직관적으로 본인이 입력한 값이 -> 화면에 뜨는구나! 라는 생각을 하게 된다

 

input 창 입력(화면에 표시되지 않음) -> 검색 결과 반환 -> input 창에 입력한 값과 검색 결과를 동시에 화면에 표시함

 

위의 방법들과 달리 입력에 지연을 적용하는 경우임!!

 


 

React 독스 친절한 점: 이게 디바운싱, 쓰로틀링이랑 뭐가 다른지도 설명해줌

 

디바운싱: 타이핑을 멈출 때까지(1초 동안) 기다렸다가 목록을 업데이트 (불필요한 api 요청 방지)

쓰로틀링: 가끔식(최대 1초에 1번) 목록을 업데이트함 (1초에 한 번씩만 api 요청 보냄)

 

useDeferredValue는 React 자체와 깊게 통합되어 있어서 사용자 기기에 맞게 조정되므로 렌더링 최적화에 적합하다네요

 

useDeferredValue에 의해 수행되는 지연된 리렌더링은 기본적으로 중단할 수 있음

React가 큰 목록을 리렌더링하는 도중에 사용자가 다른 키를 입력하면

React는 리렌더링을 중단하고 키 입력을 처리한 후 백그라운드에서 리렌더링을 시작함

 

디바운싱와 쓰로틀링은 렌더링이 키 입력을 차단하는 순간을 지연 시킬 뿐임

 

useDeferredValue는 api 요청 최적화와는 무관하고 렌더링 지연을 통해 사용자 경험을 개선하는 방식이고

디바운싱과 쓰로틀링은 api 요청 횟수를 줄여서 네트워크 트래픽 방면으로 최적화를 하는 방식!

그래서 둘이 목표로 하는 방향 자체가 다른 것 같음!

 

(이 블로그 관련글: 디바운스와 쓰로틀, 무한스크롤 https://developer-dreamer.tistory.com/118)

 


 

동시성 모드 장점

사용자와 상호작용하는 부분이 훨씬 매끄럽게 느껴짐

사용자가 스크롤 할 때 다른 연산이 큰 작업이 있어도, 동시성 모드로 스크롤이 우선적으로 부드럽게 작동하게 만듦

 

동시성 기능 활용 시 주의점

모든 컴포넌트에서 동시성 모드를 무분별하게 적용하면 성능이 떨어짐

동시성이 필요한 부분에 적용해야 함

 

동시성 모드: 사용자와 상호작용이 빈번하고 응답성이 중요한 경우에 사용

 

1. 검색 필터링, 자동완성: 사용자가 검색어를 입력할 때마다 결과가 업데이트 되는 경우

검색어 입력 > 검색 결과 업데이트

모든 입력마다 화면이 리렌더링되면 앱이 느려지고, 입력할 때마다 끊김을 느낌

동시성 모드:

검색어 입력을 더 중요한 작업으로 간주하고, 검색 결과 업데이트는 백그라운드에서 처리.

입력이 빠르고 부드럽게 유지됨

 

2. 무거운 데이터나 리스트 로딩: 긴 스크롤 목록을 보면서 네트워크를 통해 데이터 로딩

새로운 항목 불러오기 > 스크롤 작업

동시성 모드:

로딩을 백그라운드로 넘기고, 스크롤을 최우선으로 부드럽게 렌더링

 

3. 애니메이션이 포함된 화면 전환, 중요도가 높은 사용자 입력 작업

사용자 입력 > 입력 처리

 

동시성 모드:

사용자가 버튼을 클릭했을 때 UI가 즉각적으로 반응하고, 이후에 비동기 작업이 처리됨

클릭 시 지연 없이 상호작용이 자연스러워짐

 


출처

[1] 매일메일. 리액트 동시성 모드에 관해서 설명해주세요. 81번. https://maeil-mail.kr 

[2] React Docs. useDeferredValue https://ko.react.dev/reference/react/useDeferredValue

[3] React Docs. useTransition https://ko.react.dev/reference/react/useTransition