React 18 살펴보기

·2022년 5월 30일
0

신기한 개발 세상

목록 보기
8/11
post-thumbnail

Automatic Batching

이전에는 React Event Handlers 내부에서만 batched update가 가능했다. (이것 때문에 블로그들이나 유튜브 댓글보면 이게 왜 새로 업데이트 된 기능이냐고 하는 분들이 계신거 같다.)
React18부턴 promise 나 setTimeout, native event handlers 혹은 다른 어떤 이벤트에도 batched update가 적용된다.

// React v18 이전
setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  /**
  setCount에서 한번
  setFlag에서 한번
  총 두 번 rendering 된다.
  **/
}, 1000);

// React v18
setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // batch update가 가능하기 때문에 rendering을 한번만 진행
}, 1000);

Transitions

Transition은 이번 버전에서 새로 추가된 개념이다. 긴급 업데이트전환 업데이트를 구분하여 랜더링 성능을 튜닝하는데 개발자에게 자유도를 주었다.

  • 긴급 업데이트: 직접적인 상호 작용 반영
    즉각적으로 업데이트되지 않으면 사용자가 서비스에 문제가 있다고 느낄 수 있는 영역
  • 전환 업데이트: 하나의 뷰에서 다른 뷰로의 UI 전환
    화면에서 즉시 나타나지 않아도 문제 없는 영역

React 17까진 모든게 긴급 업데이트라 setTimeout, throttle, debounce같은 테크닉을 써서 긴급 업데이트를 우회했지만 이젠 startTransition api를 활용해 전환 업데이트를 명시적으로 구분하여 상태 업데이트를 진행할 수 있다.

기본 사용법은 아래와 같다.

import { useTransition } from 'react';

function SearchBar() {
	const [isPending, startTransition] = useTransition();

  // ...

	function handleChange(e) {
		const input = e.target.value;

		// 긴급 업데이트: 타이핑 결과를 보여준다.
		setInputValue(input);

		// 이 안의 모든 상태 업데이트는 전환 업데이트가 된다.
		startTransition(() => {
		  // 전환 업데이트: 결과를 보여준다.
		  setSearchQuery(input);
		});
	}

  // ...
}

참고로, 소수의 케이스에 대응하기 위해 Hook을 거치지 않고 React.startTransition을 사용할 수도 있다.

startTransition의 경우 크게 두 가지 Use Cases가 있다.

  • 느린 렌더링: 작업량이 많아 결과를 보여주기 위한 UI 전환까지 시간이 걸린다.
  • 느린 네트워크: 네트워크로부터 데이터를 기다리기 위한 시간이 걸림. Suspense와 연계.

Suspense

React 18 에서는 새로운 서버 사이드 렌더링(이하 SSR) 아키텍처가 적용됐다. 새롭게 pipeToNodeWritable API가 추가되었고, 이 API를 사용하면 SSR을 통해 <Suspense>를 사용할 수 있다.

기존 리액트 SSR의 단계는 아래와 같다.
1. 서버에서 전체 앱의 데이터를 받는다.
2. 그 후, 서버에서 전체 앱을 HTML로 렌더링한 후 Response로 전송
3. 클라이언트에서 전체 앱의 자바스크립트 코드를 로드
4. 클라이언트에서 서버에서 생성된 전체 앱의 HTML과 자바스크립트 로직을 연결

이 과정의 중요한 점은 앱 전체를 대상으로 각 단계가 완료되어야만 다음 단계로 넘어갈 수 있다는 것이다. 먼약 전체 컴포넌트 트리 중 일부가 나머지 부분보다 느리다면, 그 부분에서 병목 현상이 발생하여 SSR 전체 성능은 급격하게 낮아지게 된다.

React18 부턴 <Suspense>를 사용해 앱을 더 작은 독립적인 단위로 바꿀 수 있고 이를 통해 병목 현상을 막을 수 있다.<Suspense>와 사용할 수 있는 두 가지 주요 SSR 기능은 아래와 같다.

  1. HTML 스트리밍
    서버 단에서 renderToString 대신 pipeToNodeWritable API를 사용하여 HTML을 스트리밍할 수 있다.

    기존에도 renderToNodeStream API로 스트리밍을 통해 SSR이 가능했지만, Data Fetching을 기다릴 수 없는 단점이 있었다. 그래서 이제 이 API는 사라질 예정이다.

  2. Selective Hydration
    앱에서 렌더링 비용이 많이 드는 컴포넌트를 <Suspense>로 감싸서, 전체 앱의 Hydration을 방해하지 않고 별도의 Hydration을 진행 가능

pipeToNodeWritable API와 <Suspense>, 두 가지를 활용한다면 SSR의 양상은 완전히 달라진다.

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

위 예시에서 <Comments> 가 Data Fetching이 필요한 케이스라고 하면 <Comments><Suspense>로 감싸져있기 때문에 따로 독립적인 영역으로 취급된다. 이제 <Suspense> 영역 외의 나머지 부분이 <Suspense>영역에 영향받지 않고 즉시 스트리밍되며 렌더링 된다.

앞으로 회색은 상호 작용이 불가능한 영역, 초록색은 가능한 영역이다.

위의 로딩 화면에서 <Comments> 의 Data Fetching이 끝나면서 SSR이 완료되면 화면이 대체된다. 이렇게 되면 사용자에게 빈화면을 최댜한 적게보여주는 SSR의 장점을 극대화할 수 있다.

이 방법으로 특정 컴포넌트의 Data Fetching이 오래걸릴 때 생기는 문제가 해결됐다. 이제 특정 컴포넌트가 코드량이 커서 로딩이 오래걸릴 때, 특정 컴포넌트가 복잡해 랜더링 시간 자체가 오래걸릴 때 이 두 가지를 어떻게 해결했는지 살펴보자.

이제까진 Next를 사용하지 않으면 React 자체의 SSR에선 React.lazy를 사용할 수 없었다. 하지만 React18부턴 pipeToNodeWritable 덕분에 lazy 컴포넌트 사용이 가능하다.

import React, { Suspense } from 'react';

const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
      </Suspense>
    </div>
  );
}

현재 리액트 생태계의 주류 환경인 웹팩 기반의 애플리케이션에서, lazy 컴포넌트를 사용하면 코드 스플리팅(Code Splitting)이 적용되어 별도의 자바스크립트 Chunk 파일로 분리된다. 그리고 이 <Suspense> 컴포넌트 하위 트리의 렌더링 외부 트리의 렌더링 과정을 막지 않고 별도의 과정이 진행된다.

<Comments> 가 SSR의 몇 단계 과정에 있던 그것과 무관하게 바깥의 다른 컴포넌트들은 Hydration이 끝났다. 또한, React 18에서 Suspense 하위의 Hydration은 브라우저가 이벤트를 처리할 수 있도록 짧은 갭과 함께 진행된다. 덕분에 클릭은 즉시 처리되고 성능이 낮은 기기에서도 브라우저가 멈춘 것처럼 보이지 않게 된다.

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

위에 예시처럼 두개 이상의 lazy 컴포넌트를 SSR한다고 가정하면
이제 <Sidebar>컴포넌트와 <Comments> 컴포넌트는 나머지 일반 영역이 먼저 HTML로 렌더링되며 클라이언트로 스트리밍된 이후, 별도의 스트리밍이 시작된다.

먼저 두 개의 lazy 컴포넌트가 Data Fetching 이후 HTML로 렌더링 되며 스트리밍된다.
이때 다른 영역은 이미 Hydration까지 끝난 상태라고 가정해보자.

그리고 나서, <Sidebar>컴포넌트와 <Comments> 컴포넌트의 Chunk 코드가 로딩된다. 코드 로딩이 완료되면 React는 두 컴포넌트 모두 Hydration을 시도한다. 컴포넌트 트리에서 더 먼저 발견된 Suspense 영역부터 Hydration을 시작한다. 즉, 여기서는 <Sidebar> 컴포넌트가 먼저 Hydration이 진행된다.

<Sidebar> 영역이 Hydration이 진행 중에 만약 사용자가 코멘트 영역을 클릭하면 리액트는 이 클릭을 기록한다.

그리고 <Comments> 컴포넌트의 Hydration 우선순위를 높인다. 왜냐하면, 사용자가 이 컴포넌트와 상호작용을 하고자 했으므로 더 긴급한 것으로 판단했기 때문이다. 그래서 Hydration이 진행 중이었던 <SideBar> 대신 <Comments> 컴포넌트를 먼저 Hydration 하게된다.

<Comments> 영역의 Hydration이 완료되면, 리액트는 기록했던 클릭 이벤트를 다시 실행하여 이 컴포넌트에게 상호 작용에 반응하게끔 한다.
예를 들어, 이용자가 Hydration 이전에 “코멘트 자세히 보기” 버튼을 클릭했었다면 Hydration이 끝난 후 “코멘트 자세히 보기" 버튼에 연결된 이벤트 핸들러를 실행하는 식이다.

이제 리액트가 긴급하게 Hydration 해야 할 <Suspense> 영역이 없다면, 리액트는 <Sidebar>를 마저 Hydration 할 것이다.

새로운 Hooks

useId

useId는 클라이언트와 서버 사이의 hydration missmatch를 피하면서 unique ID를 생성해주는 훅이다.

useSyncExternalStore

store에 대한 업데이트를 강제로 동기화하여 External Store에 concurrent read를 지원할 수 있게한다.

External store: 우리가 subscribe 하는 무언가. (ex, redux store, 글로벌 변수, dom 상태 등등)

React18부터 렌더링이 렌더링을 잠시 중단할 수 있게 됐다. 그러면서 각 컴포넌트 별로 업데이트 속도가 달라서 발생하는 Tearing(시각적 비일치) 문제가 생겼다.

React18 이전

Tearing이 발생하는 과정

초기 그림처럼 초기 데이터는 파랑색이지만 중간에 external store가 update되면서 data가 빨간색으로 바뀌게된다. 리엑트는 이를 반영하려고 할 것이고 이 과정에서 Tearing이 발생하게 된다.

이를 해결하기 위해 도입된 hook이 useSyncExternalStore 이라고 한다.(근데 나 사실 이거 이해가 잘 안 간다. 애만 빼서 다시 공부해야할꺼 같다.)

useInsertionEffect

useInsertionEffect는 css-in-js 라이브러리가 렌더링 도중에 스타일을 삽입할 때 성능 문제를 해결할 수 있는 새로운 훅이다.

React Hydration과 Tearing에 대해선 공부가 좀 더 필요할꺼 같다.

참고자료

https://reactjs.org/blog/2022/03/29/react-v18.html
https://medium.com/naver-place-dev/react-18%EC%9D%84-%EC%A4%80%EB%B9%84%ED%95%98%EC%84%B8%EC%9A%94-8603c36ddb25
https://github.com/reactwg/react-18/discussions/37
https://github.com/reactjs/rfcs/blob/main/text/0213-suspense-in-react-18.md

profile
이제는 병아리는 벗어나야하는 프론트개발자

0개의 댓글