React 18 톺아보기 - 01. Intro

리액트 18이 정식 배포된 지 어느덧 1년이 지났습니다. 여러분은 신규 기능을 잘 활용하고 계시는지 궁금하네요. 이전 주제와 마찬가지로 이번 주제도 리액트를 개발함에 있어 몰라도 문제 없는 내용입니다. 사용법을 알려 드리려고 하는 게 아니기 때문이죠. 하지만 구현 사항을 이해하고 있는 것과 아닌 것의 차이는 매우 다양한 곳에서 나타납니다. 보다 깊이 있게 이해하고 생각하는 데 도움이 되는 내용이라고 말씀 드릴 수 있겠습니다.

이번에 작성하게 될 주제는 리액트 18의 동시성 모델입니다. 18로 들어서면서 많은 신규 기능과 변화가 있었습니다. 글로만 접했을 땐 정말 마법 같습니다. 먼저 공식 블로그의 리액트 18 소개를 아직 읽지 않았다면 읽고 오시면 좋겠습니다.

소개글에서 가장 처음 언급되는 것이 동시성입니다. 리액트에서 동시성이란 단어는 18에서 처음 등장한 것은 아니고 리액트 팀 내부적으로 이미 오래전부터 연구하고 준비하고 있었습니다.
15에서 16으로 넘어오면서 콜스택 기반의 렌더링 아키텍쳐가 Fiber 아키텍처로 변경되었고 Suspense 컴포넌트도 도입되었습니다. 16, 17에서는 내부적으로 동시성 모델을 위한 실험적인 변화가 있었습니다. 하지만 해결 해야 할 문제가 많았고 불완전했기 때문에 표면적으로는 별다른 노출이 없었습니다. Suspense 또한 이때까지는 단지 컴포넌트 동적 로드로 활용하는 게 전부였습니다. 그리고 18로 넘어오면서 불완전했던 동시성 모델이 완성되었습니다.

18에서 완성된 동시성 모델을 한 문장으로 표현하면 “대규모 화면 전환에서도 높은 응답성을 유지할 수 있다.”라고 할 수 있습니다(사실 적절한 표현인지는 모르겠습니다.😅). 그리고 앞으로 우리가 집중적으로 분석해볼 부분이 문장에서 언급된 “화면 전환”과“높은 응답성 유지”입니다. 이를 알아보기에 위해서는 먼저 리액트가 앱에서 발생하는 업데이트를 어떻게 바라보고 있는지 이해하고 넘어가야 합니다.

리액트가 업데이트를 바라보는 관점

업데이트는 어플리케이션 생명 주기 동안 빈번하게 발생합니다. 업데이트가 발생하면 리액트는 호스트 트리(DOM)와 리액트 트리(Virtual DOM, 이하 VDOM) 사이의 변경점을 확인하고 반영하는 과정을 거칩니다. 이런 빈번한 렌더링 작업을 최적화하기 위해 업데이트를 모아서 일괄 처리하는 방식을 취할 수 있습니다. 이런 방식은 항상 이점을 가져다 줄 것 같지만 그렇지 않습니다. 왜 그런지 업데이트에 대해 좀 더 깊이 생각해봅시다.

  1. 업데이트는 모두 다 같은 종류의 업데이트라고 할 수 있는가?
  2. 업데이트 간에 중요도는 없는가?

업데이트는 모두 다 같은 종류의 업데이트라고 할 수 있는가?

자동완성 기능이 포함된 검색 앱이 있다고 가정해봅시다.

const SearchApp = () => {
  const [text, setText] = useState('');

  return (
    <>
      <input onChange={(...) => setText(...)} />
      <AsyncAutoComplete target={text} />
    </>
  )
}

위 화면에서 발생한 업데이트는 두 가지입니다.

  1. <input />으로 부터 발생한 text 상태 업데이트.
  2. 자동완성 API의 응답으로 부터 발생한 <AsyncAutoComplete />의 자동완성 리스트 상태 업데이트.

두 업데이트의 차이점은 업데이트를 발생시킨 이벤트의 시작점에 있습니다. 1번은 사용자의 물리적 행동(키보드 입력)으로 부터 발생하였고 2번은 비동기(자동 완성 API)로 부터 발생한 업데이트입니다. 시작점으로 판단하면 두 가지 정도로 분류할 수 있습니다만, 업데이트는 곧 렌더링으로 이어지므로 렌더링 관점으로 바라보면 추가로 더 분류를 할 수 있습니다. 바로 리액트 18에서 본격적으로 언급되는 전환(Transition)입니다. 다음 화면에서의 전환은 자동 완성 UI를 말합니다.

Transition은 이미 16 ~ 17에서부터 구현되어 있었습니다만 실험 기능이었기도 하고 설명하기 편하도록 기능이 완성되어 노출된 18버전 부터 추가되었다고 설명하겠습니다.

자동 완성 UI는 사용자 입력에 따라 ‘리’, ‘리액’, ‘리액트’의 UI가 순차적으로 렌더링되고 있습니다. 전환은 이와 같이 UI가 A0 -> A1로 전환되는 것을 말합니다. 그리고 이런 UI 전환 렌더링을 발생키는 업데이트를 전환 업데이트라고 합니다. UI 전환이라는 것은 리액트 입장에서는 스스로 판단할 수 없습니다. 그래서 이를 개발자가 명시적으로 알려주어야 합니다.

const SearchApp = () => {
  const [text, setText] = useState('');
  const [transitionText, setTransitionText] = useState('');
  return (
    <>
      <input onChange={(...) => {
        setText(...);
        React.startTransition(() => setTransitionText(...));      }} />
      <AsyncAutoComplete target={transitionText} />    </>
  )
}

startTransition API를 이용하여 transitionText 상태 업데이트의 시작점을 전환 이벤트로 설정하는 것입니다.

리액트는 업데이트를 다양하게 분류하여 생각하려고 합니다. 사용자의 물리적 행동, 비동기 스코프, 전환으로부터 발생한 업데이트는 다 같은 업데이트가 아니며, 처리하는 방식 또한 업데이트 성격에 따라 다르게 가져갑니다.

업데이트 간에 중요도는 없는가?

이에 대한 대답은 사용자 경험과 밀접한 관련이 있습니다. 사용자 경험은 리액트 팀이 가장 공을 들여 연구하고 있는 분야이고 동시성 모델을 도입하게 된 이유입니다. 다음은 useTransition PR의 일부 내용을 발췌한 것입니다.

What problem does this solve?의 일부 발췌
사용자는 물리적인 행위에 대해서 즉각적인 반응을 기대한다. 그렇지 않다면 사용자는 뭔가 잘못되고 있다고 느낄 수 있다. 반면 A0 -> A1의 전환은 느릴 수 있다고 무의식적으로 인지하고 있으며, 모든 전환에 대한 즉각적인 반응을 기대하지 않는다.

위 내용을 바탕으로 자동완성 앱에서의 업데이트 중요도를 두자면, 가장 중요도가 높은 업데이트는 사용자의 물리적 행위로부터 발생한 text 상태 변경 업데이트임을 유추할 수 있고 반면에 transitionText의 자동완성 전환 업데이트는 반영이 조금 느리더라도 UX를 크게 헤치지 않는다고 판단할 수 있습니다.

리액트는 발생하는 모든 업데이트에 우선순위를 두고 있으며, 동시 다발적으로 발생하는 업데이트 사이에서 렌더링 대상을 선별하고 렌더링 방식을 결정하는데에 기준으로 사용합니다.


리액트 18에서는 업데이트를 분류해서 바라보고 각각의 우선순위를 두어 다르게 처리하고 있습니다. 18 이전에도 이와 같이 처리하려고 했지만, 구조적 한계와 완성되지 않은 기능으로 인해 발생 시간을 기준으로 업데이트를 처리하는 방식을 채택하고 있었습니다. 다음 글에서 리액트 18 이전에는 어떠한 한계가 있었고 이를 해결하려는 노력은 무엇이었는지 확인해보도록 하겠습니다.


오픈소스를 톺아보며 매직 코드라 생각했던 부분들의 동작 원리와 의미, 의도를 파악해보고 서로의 생각을 나누기 위한 블로그