React

[React] React 18 업데이트 내용 정리(Automatic Batching / Suspense / Transition)

킹우현 2023. 8. 2. 21:55

1. Automatic Batching

Batching 이란 ?
업데이트 대상이 되는 상태 값들을 하나로 묶어서 한번의 리렌더링으로 업데이트가 일괄적으로 진행될 수 있도록 하는 것

위처럼 하나의 함수 안에 있는 여러 setState는 비동기로 동작하며, 한번의 리렌더링으로 상태가 일괄적으로 업데이트됩니다.

 

함수의 끝에서 상태가 업데이트가 되며, 단 한번의 리렌더링이 이루어집니다.

👉🏻 불필요한 리렌더링을 방지함으로써 렌더링 성능을 개선시킬 수 있음 !

 

batch update를 통해 불필요한 리렌더링을 줄여줌으로써 성능적으로는 큰 이점을 얻을 수 있었고, 이전 버전에서도 이러한 batch update를 지원했지만 Click과 같은 React의 이벤트 핸들러 내부에서만 적용이 가능하고 Promise의 내부나 타이머 함수 내에서는 작동하지 않았습니다.

 

Auto Batching 이 적용되지 전까지는 위와 같은 상황으로 인해 의도치 않은 버그를 발생하였고, 이러한 상태를 컴포넌트의 업데이트가 'half-fininshed'된 상태라고 정의하였습니다.

 

따라서 React 18 에서는 timeout이던, promise던, native 이벤트 핸들러던 업데이트가 어느 곳에서 야기되었는지와 관계없이 Batching을 지원하는 Automatic Batching을 지원하게 되었습니다.

 

Automatic Batching 사용법

Automatic Batching을 사용하기 위해서는 React 18에서 새롭게 등장한 ReactDOMClient의 createRoot 메서드를 사용해야 합니다.

 

기존의 React의 entry 파일에서는 다음과 같이 정의했었습니다.

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

React 18부터는 createRoot를 사용해 다음과 같이 정의하면 됩니다.

import { createRoot } from "react-dom/client";
const root = createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

 

batch 처리하지 않으려면 어떻게 해야 하는가 ?

일반적으로 Batching은 안전하지만 일부 코드는 상태 변경 직후에 DOM에서 무언가 읽는데 의존할 경우가 있기 때문에, 이러한 경우에는 ReactDOM.flushSync()를 사용하여 일괄 처리를 방지할 수 있습니다.

 

정리 : 
1. React 18 이전 버전에서도 Batching을 통해 불필요한 리렌더링을 방지할 수 있었다.
2. React 18 부터는 이벤트 핸들러 내부를 제외한 Promise나 타이머 함수 등 모든 경우에서 Batching 기능을 지원하게 되었다.
3. 필요한 경우 Batching 기능을 방지할 수 있다.

2. Suspense on the Server

<Suspense fallback={<Loading />}>
  <SomeComponent />
</Suspense>

<Suspense>는 React에서 무언가를 기다릴 때 사용합니다. children이 로딩되기 전에 fallback을 보여줄 수 있습니다.

 

fallback에는 실제 UI가 로딩이 끝날 때까지 대신 보여줄 컴포넌트를 넣어줍니다. 보통 스피너나 스켈레톤을 넣습니다.

Suspense란 ? 컴포넌트가 읽어야하는 데이터가 아직 준비되지 않았다고 리액트에게 알려주는 새로운 매커니즘, 아직 렌더링이 준비되지 않은 컴포넌트가 있을때 로딩 화면을 보여주고 로딩이 완료되면 해당 컴포넌트를 보여주는 기능

Suspense라는 React의 신기술을 사용하면 컴포넌트의 렌더링을 어떤 작업이 끝날 때까지 잠시 중단시키고 다른 컴포넌트(fallback)를 먼저 렌더링할 수 있습니다.

 

즉, Component Lazy loading 이나 Data Fetching 등 비동기처리를 할 때 응답을 기다리는 동안 fallback UI(예를 들면 loading Spinner 등)를 대신 보여주고, 그 사이에 우선순위가 높은 다른 UI를 먼저 렌더링 할 수 있습니다. ⭐️

 

React 공식문서에 따르면, 현재(v18) Suspense는 공식적으로 React.lazy를 사용한 Lazy Loading Component를 기다릴 때 사용하고, Data Fetching에 사용되는 것이 가능은 하지만 아직까지 권장되지는 않는다고 이야기하고 있습니다. 

 

또한, 명령형으로 로딩 상태를 지정해주어야 했던 것을 Suspense 컴포넌트를 사용하여 선언형으로 코드를 작성할 수 있습니다.

 

서버 사이드 렌더링 과정

  1. 클라이언트에서 서버로 전체 앱에 대한 리소스를 요청합니다.
  2. 서버는 HTML문서로 페이지를 렌더링하고 응답으로 전송합니다.
  3. 클라이언트는 전체 앱에 대한 JS 파일을 로드합니다.
  4. 클라이언트에서 JS 코드와 HTML 페이지를 연동하는 Hydration을 수행합니다.

클라이언트 사이드 렌더링의 경우에는 번들링된 JS 파일이 로드될 때 까지 사용자는 아무것도 볼 수 없지만, 서버 사이드 렌더링의 경우에는 JS 파일이 로드되기 전까지 정적인 파일(HTML, CSS)들을 화면에 보여줄 수 있었습니다.

 

따라서 사용자는 보다 빠르게 화면을 볼 수 있고 SEO 측면에서도 우수한 성능을 가져올 수 있습니다.

 

하지만 서버 사이드 렌더링은 다음과 같은 단점들이 있었습니다.

  • 모든 데이터가 준비되기 전까지 화면을 표시하지 않고,
  • Hydrate를 하기 전에 모든 것을 로드해야 했고,
  • 상호작용 하기 전에 모든 것을 Hydrate해야 했습니다.

따라서 React 18은 위와 같은 단점을 해결하기 위하여 'Streaming HTML(Server)''Selective Hydration(Client)'를 지원합니다.

Streaming HTML

<Suspense>로 컴포넌트를 감싸면 React가 페이지의 나머지 부분에 대해 HTML 스트리밍을 시작할 때까지 기다릴 필요가 없다고 React에 알려줍니다. 대신에 React는 fallback에 전달된 컴포넌트를 보냅니다.

 

 

이후에 해당 컴포넌트가 서버에서 준비가 되면 React는 추가 HTML을 동일한 스트림에 보내고 해당 HTML을 넣기 위한 최소한의 인라인 <script> 태그를 보냅니다.

 

결과적으로 페이지를 표시하기 전에 모든 데이터를 가져올 필요가 없고, 서버에서 준비가 되면 추가적인 HTML을 보낼 수 있게 되었습니다. 즉, 컴포넌트 단위로 HTML 스트리밍이 가능해진 것입니다.

 

Selective Hydration

초록색으로 칠해진 부분이 Hydration이 마무리되어 인터랙션이 가능한 상태

<Layout>
  <NavBar />
  <Suspense fallback={<Spinner />}>
    <Sidebar />
  </Suspense>
  <RightPane>
    <Post />
    <Suspense fallback={<Spinner />}>
      <Comments />
    </Suspense>
  </RightPane>
</Layout>

<Suspense>로 컴포넌트를 감싸면 해당 컴포넌트(Sidebar, Comments)를 제외한 나머지 컴포넌트(NavBar, Post)는 HTML 렌더링과 Hydration이 먼저 일어나게 됩니다.

 

React는 두 컴포넌트 중 트리에서 더 빨리 찾아지는 Suspense 바운더리를 먼저 Hydrating을 시도합니다. 가령, 사이드바가 먼저 찾아졌다고 가정합시다.

 

근데 이때, 유저가 Comments 컴포넌트와 인터랙션을 시도하면 어떻게 될까요? 놀랍게도 React는 클릭 이벤트가 발생한 컴포넌트를 먼저 동기적으로 Hydrating합니다.

 

즉, Suspense는 HTML 렌더링뿐만 아니라 Hydration도 컴포넌트 단위로 가능하도록 합니다.

 

3. Transition

React 18에서는 업데이트 중에도 앱의 응답성과 사용자 상호작용을 유지하는데 도움이 되는 새로운 API를 도입하였다. 이 기능은 React에서 어떠한 상태 업데이트가 우선순위가 높은지 알려주고, 상태를 업데이트하는데 우선순위를 부여하게 된다.

 

브라우저에서 렌더링이 시작되면 이는 중단될 수 없으며 렌더링이 완료되기까지 화면은 block 상태가 된다고 할 수 있다. 이로 인하여 렌더링 도중에 발생하는 텍스트 입력과 마우스 클릭과 같은 액션이 지연되어 버벅거림을 느낀 적이 있을 것이다. 

 

React 18에서는 화면 렌더링 중에도 사용자 상호작용이 정상적으로 유지되도록 도움을 주는 API들을 제공하여 사용자 상호 작용이 정상적으로 반영되도록 개선이 되었다.

 

화면에서 버튼을 클릭하거나 텍스트를 입력하는 것과 같은 동작으로 화면에 많은 일이 발생 할 수 있으며, 이로 인하여 작업이 완료되기까지 페이지가 정지될 수 있다.

// Urgent: Show what was typed
setInputValue(input)

// Not urgent: Show the results
setSearchQuery(input)

사용자가 텍스트를 입력할 때마다 텍스트 상태 값을 업데이트하고 상태 값을 사용하여 목록을 검색하고 결과를 렌더링한다.

 

목록 결과를 렌더링하는 동안 페이지는 지연 현상이 발생하여 입력 또는 다른 인터렉션이 느려지고, 응답하지 않는 것처럼 느껴질 수 있다.

 

위 상황에서 입력 필드에 대한 업데이트목록을 렌더링하는 업데이트 두 가지로 나눌 수 있으며, 여기서 사용자 상호작용에 관한 입력 필드 업데이트를 조금 더 중요한 사항으로 볼 수 있다.

 

사용자는 브라우저에서 입력 필드에 대한 업데이트는 기본적으로 제공되는 즉각적 업데이트로 기대할 것이다. 하지만 렌더링에 대한 사항은 다소 늦어질 것으로 기대할 수 있다.

 

이러한 이유로 개발자들은 이러한 현상을 해결하기 위하여 디바운싱과 같은 기술로 렌더링 업데이트를 인위적으로 지연시켰다.

 

React 18까지는 모든 업데이트가 긴급하게 렌더링 되었다. 위의 두 가지 상태는 모두 동시에 렌더링 되고 모든 것이 렌더링 될 때까지 사용자 상호작용을 차단하게 된다. 

 

startTransition API 를 사용한 문제점 개선

import { startTransition } from 'react'

// Urgent: Show what was typed
setInputValue(input)

// Mark any state updates inside as transitions
startTransition(() => {
  // Transition: Show the results
  setSearchQuery(input)
})

startTransition API를 사용하여 긴급하지 않은 업데이트를 랩핑할 수 있다. 클릭 혹은 키 이벤트가 발생하는 경우 랩핑 된 업데이트는 지연되고 클릭과 키 이벤트와 같은 긴급한 이벤트가 먼저 처리되도록 한다. (React에게 상태 업데이트에 대한 우선순위를 부여할 수 있다)⭐️

 

그리고 완료되지 않은 오래된 렌더링을 폐기하고 최신 업데이트로 렌더링한다.

 

해당 Transitions API를 사용하면 사용자의 상호작용을 빠르게 유지할 수 있다. 또한 불필요한 렌더링을 줄일 수 있는 효과도 얻을 수 있다.

 

Transition이 보류 중인 경우

import { useTransition } from 'react'

const [isPending, startTransition] = useTransition()

작업이 진행 중임을 알리는 isPending 플래그를 사용할 수 있는 useTransition hook이 제공된다.

{
  isPending && <Spinner />
}

전환이 보류 중인 동안 isPending은 true이므로 기다리는 동안 로딩 혹은 스피너 등을 표시 할 수 있다.

 

결론 : Transition을 사용하면 불필요한 렌더링을 피하고 성능을 최적화 할 수 있다. 또한 새로운 화면을 표시할 때 사용자 상호작용을 정상적으로 유지할 수 있으며 상태변경으로 인한 불필요한 리렌더링을 방지하는 것이 가능하다.