[React] Concurrent mode는 무엇일까?

한호수 (The Lake)·2023년 4월 29일
0
post-thumbnail

리액트에서 Concurrent mode 라는 키워드를 접했을 때 mode라는 단어에 on, off 처럼 끄고 켤수있는 기능인가? 라는 생각이 들어 공부하게 되었다.

개요

일단 Concurrent mode 라는 단어를 직역하자면 동시성 모드라는 뜻이다. 여기서 동시성은 동시에 두가지 이상의 일을 지원함을 의미한다. 이와 비슷한 의미인 병행성(parallelism)을 비교해서 알아보자.

동시성(concurrent)은 마치 아침식사를 준비할때 커피물을 끓이는 동안 식빵을 잘라 토스트를 준비하고 토스트를 굽는동안 다 끓은 커피물을 찻잔에 따르는 행위를 할 수 있는것 처럼 한가지 일이 끝날때까지 기다리지 않고 동시에 진행할 수 있는 상태를 의미한다.

출처 : What is React Concurrent Mode?

병행성(parallelism)은 동시에 두가지 이상의 일을 실행할 수 있다는 뜻으로 마치 팔이 8개인 문어(사실 팔은 6개이고 2개는 다리)는 동시에 두 세 가지 일을 진행 할 수 있는것과 같다.

그렇다면 리액트에서 Concurrent mode는 무엇일까?

리액트가 돌아가는 환경인 자바스크립트는 싱글 스레드로 동작하기 때문에 동기적으로 동작하는 어떤 작업이 오래걸린다면 그 동안 아무것도 할 수 없다. 그것은 화면을 보여주는 렌더링도 마찬가지이다.

리액트 16버전 이전에는 이런 렌더링 하는 과정이 동기적으로 이루어졌기 때문에 렌더링이 끝날때까지 사용자의 입력도 에니메이션도 처리하지 못하는 경우가 생기게 되었다.

리액트팀은 이를 해결하기 위해서 16버전에서 렌더링 엔진을 변경하게 되었고 스케쥴러를 통해 우선순위에 따라 급한작업이 있다면 먼저 처리할 수 있도록 해결하였다.

동시성 모드는 이러한 리액트팀의 노력으로 만들어진 동시성을 지원하는 환경을 의미한다.

그렇다면 어떻게 켤 수 있을까?

리액트 18 버전이 업데이트 되면서 이전 버전에 대한 호환성을 유지하고 점진적이고 안전한 업그레이드를 지원하기 위해서 기존 ReactDOM.render를 통한 lagacy modecreateRoot를 통한 concurrent mode를 선택할 수 있게 하였다.

lagacy mode

// index.js

import ReactDOM from 'react-dom';
import App from './App';

const container = document.getElementById('app'); 

ReactDOM.render(<App />, container); 

concurrent mode

// index.js
import ReactDOM from 'react-dom';
import App from './App'; 

const container = document.getElementById('app'); 

const root = ReactDOM.createRoot(container);  // createRoot를 사용한다.

root.render(<App />); 

기본적으로 CRA(create-react-app) 으로 생성하게 되면 createRoot를 사용하게된다.

동시성 모드에서는 어떤걸 지원할까?

동시성 모드를 사용하면 리액트 18버전에서 동시성 렌더링을 제공하는 새로운 기능을 사용할 수 있다. 대표적으로는 useTransitionuseDeferredValue 훅이 있다.

useTransition

useTransition 훅을 사용하면 isPendingstartTransition 을 반환한다.

  • isPending은 현재 상태 업데이트가 진행중인지 끝나서 보여줄 준비가 되었는지를 판단할 수 있도록 boolean 타입으로 제공된다.

  • startTransition은 함수타입으로써 콜백함수를 인자로 받으며 콜백함수 안에서 우선순위를 낮춰 렌더링할 setState 함수를 실행한다.

function TabContainer() {
  const [isPending, startTransition] = useTransition();
  const [tab, setTab] = useState('about');

  function selectTab(nextTab) {
    startTransition(() => {
      setTab(nextTab); // setTab가 변경되서 업데이트되는 랜더링은 낮은 우선순위를 갖는다.
    });
  }
  // ...
}

아래에는 startTransition을 사용하지 않고 DOM을 조작했을때 DOM이 많아질수록 렌더링을 동기적으로 진행하기 때문에 이미 상태가 바뀌어서 이전 렌더링이 필요 없더라도 멈출 수 없고, 모든 조작이 Blocking 되어 사용자는 아무것도 할 수 없게 된다.

동기적으로 동작하는 렌더링 모습
보라색 박스가 commit 단계로 인한 직접적으로 그려지는 단계이다.

아래에는 startTransition을 사용하고 나서 DOM을 조작했을때 우선순위에 따라 사용자 입력 같은 급한 작업은 렌더링 과정을 일시중단하고 먼저 처리함으로써 사용자에게 더 좋은 UX를 제공하게된다.

Transition마다 우선순위가 존재하며 불필요한 렌더링은 건너뛰고 마지막 상태만 반영한다.

useDeferredValue

결론적으로 useDeferredValueuseTransition은 상태의 업데이트 우선순위를 낮춘다는 점에서 같은 동작을 한다. 하지만 startTransition는 콜백함수 내부에 setState를 사용해야하지만, useDeferredValue는 state 값을 인자로 받아서 지연된 값을 반환한다. 업데이트 중에 먼저 이전 값을 보여주고 백그라운드에서 업데이트가 완료되면 새 값으로 다시 렌더링을 시도한다.

// ...
const [state , setState] = useState("")
const deferredValue = useDeferredValue(state)

// ...

return (<div>
    deferredValue.map((v)=> <li>{v}</li>) // 기존 State처럼 사용하면된다.
</div>)

위 코드에서 state 값은 계속해서 바뀌게 되지만 실제 사용하는 값인 deferredValue 에는 이전 값을 그대로 사용하다가 업데이트가 완료되면 변경된 값을 보여준다.

이외에도 리액트 18에서 동시성을 지원하기 위한 Streaming SSRSelective hydration 기능들이 존재하지만 나중에 다루어 보도록 하자.

Reference

profile
항상 근거를 찾는 사람이 되자

0개의 댓글