Selective Hydration

이전 글에서 SSR이 어떻게 사용자 경험을 향상시킬 수 있는지에 대해 알아보았다. React는 react-dom/serverrenderToString메서드를 통해 컴포넌트 트리를 빠르게 만들어낼 수 있고 만들어진 트리는 클라이언트에 전송된다. 이 때 렌더링된 HTML은 자바스크립트 번들이 다운로드되어 실행되기 전 까지는 아직 인터렉션이 불가능하다. React가 트리를 확인하고 hydrate하여 핸들러를 바인딩 한다.

그러나 이런 방법은 구현 방식의 제약 사항으로 인해 성능 이슈를 만들 수 있다.

서버가 렌더링 한 HTML트리가 클라이언트에게 전송되려면 모든 컴포넌트는 그럴 준비가 되어 있어야 한다. 만약 컴포넌트가 외부 API호출이나 기타 다른 처리로 인해 기다려야 하면 그로 인해 빠르게 렌더링하는것을 막게 된다.

이렇게 트리가 늦게 생성되는것 외에도. 다른 문제는 React가 트리를 한 번만 hydrate한다는 점이다. 따라서 React가 hydrate하기 전에 모든 컴포넌트에 대한 자바스크립트를 다운로드 받아야 한다. 이는 작은 코드를 가진 작은 컴포넌트도 큰 컴포넌트의 코드가 다운로드되고 hydrate될 때까지 기다려야 하게 된다. 이 시간 동안 웹사이트는 인터렉션이 불가한 채로 보여지게 된다.

React 18에서는 서버 사이드 렌더링 스트리밍과 새로운 hydration 방법을 조합하여 해결할 수 있다. 그것이 바로 선택적 Hydration이다.


이전에 다루었던 renderToString을 사용하는 대신, 서버에서 pipeToNodeStream을 사용하여 HTML렌더링을 스트림 할 수 있다.

이 메서드를 createRootSuspense와 같이 사용하면 큰 컴포넌트가 준비되기까지 기다리지 않고도 HTML을 스트리밍 처리할 수 있다. 따라서 SSR을 사용할때에도 컴포넌트들을 지연로딩 할 수 있게 되었다. 이전에는 불가능했던 것이다.

// server.js
import { pipeToNodeStream } from 'react-dom/server'

export function render(res) {
  const data = createServerData()
  const { startWriting, abort } = pipeToNodeWritable(
    <DataProvider data={data}>
      <App assets={assets} />
    </DataProvider>,
    res,
    {
      onReadyToStream() {
        res.setHeader('Content-type', 'text/html')
        res.write('<!DOCTYPE html>')
        startWriting()
      },
    }
  )
}
// app.js
import { Suspense, lazy } from 'react'
import Loader from './Loader'

const Comments = lazy(() => import('./Comments'))

function App() {
  return (
    <main>
      <Header />
      <Suspense fallback={<Loader />}>
        <Comments />
      </Suspense>
      <Footer />
    </main>
  )
}
// index.js
import { hydrateRoot } from 'react-dom'
import App from './App'

hydrateRoot(document, <App />)

트리 생성이 느렸고 TTI도 좋지 않았던 Comments 컴포넌트는 이제 Suspense 컴포넌트로 래핑되었다. 이제 React에게 이 컴포넌트가 트리 생성 시간을 지연시키지 않게 하고 최초 렌더링 시 폴백 컴포넌트를 렌더링하게 하여 나머지 부분을 빨리 렌더링해 클라이언트에게 전송하도록 하였다.

그러는 동안 서버는 Comments 컴포넌트에 대한 데이터를 외부로부터 받아오고 있다.

선택적 hydration을 사용하면 Commments 컴포넌트가 아직 클라이언트에 전송되지 않은 시점에도 미리 전송한 클라이언트 컴포넌트들을 hydrate할 수 있다.

Comments컴포넌트의 데이터 로드가 끝나면 React는 해당 컴포넌트를 클라이언트에 추가적으로 스트리밍한다. 작은 <script>태그가 이전에 미리 렌더링했던 폴백 로더를 교체한다.

이어서 React는 스트리밍되어 클라이언트 페이지에 삽입된 Comments 컴포넌트를 hydrate한다.


React 18 에서는 React와 SSR을 함께 사용할 때 자주 겪을 수 있는 문제들을 수정했다.

스트리밍 렌더링은 컴포넌트들이 준비될 때 클라이언트측에 스트리밍으로 전달할 수 있게 해 준다. 서버 렌더링하는데 오래 걸려 FCP와 TTI를 느리게 만드는 문제를 회피할 수 있다.

클라이언트에 스트리밍 된 컴포넌트는 곧 hydrate된다. 더 이상 모든 컴포넌트의 hydrate를 위해 모든 javascript가 로드되기를 기다릴 필요가 없고. 그 전에 앱과 상호작용도 가능하게 된다.

참조