hydrateRoot

Chaerin Kim·2023년 12월 19일

hydrateRoot를 사용하면 이전에 react-dom/server에 의해 생성된 HTML 콘텐츠가 있는 브라우저 DOM 노드 내에 React 컴포넌트를 표시할 수 있음.

const root = hydrateRoot(domNode, reactNode, options?)

Reference

hydrateRoot(domNode, reactNode, options?)

hydrateRoot를 호출해서 서버 환경에서 React가 이미 렌더링한 기존 HTML에 React를 "첨부"할 수 있음.

import { hydrateRoot } from 'react-dom/client';

const domNode = document.getElementById('root');
const root = hydrateRoot(domNode, reactNode);

React는 domNode 내부에 존재하는 HTML에 첨부되어 그 안에 있는 DOM을 관리함. React로 완전히 빌드된 앱에는 일반적으로 하나의 루트 컴포넌트와 함께 하나의 hydrateRoot 호출만 있음.

Parameters

  • domNode: 서버에서 루트 요소로 렌더링된 DOM 요소.

  • reactNode: 기존 HTML을 렌더링하는 데 사용된 "React 노드". 일반적으로 renderToPipeableStream(<App />)과 같은 ReactDOM Server 메서드로 렌더링된 <App />과 같은 JSX 조각임.

  • options (optional): React 루트에 대한 옵션이 있는 객체.

    • onRecoverableError (optional): React가 오류로부터 자동으로 복구할 때 호출되는 callback.
    • identifierPrefix (optional): React가 useId에 의해 생성된 ID에 사용하는 문자열 접두사. 같은 페이지에서 여러 루트를 사용할 때 충돌을 피하는 데 유용함. 서버에서 사용하는 것과 동일한 접두사여야 함.

Returns

renderunmount 두 가지 메서드가 있는 객체를 반환함.

Caveats

  • hydrateRoot()는 렌더링된 콘텐츠가 서버에서 렌더링된 콘텐츠와 동일할 것으로 기대함. 불일치는 버그로 간주하고 수정해야 함.

  • 개발 모드에서 React는 hydration 중 불일치에 대해 경고함. 불일치하는 경우 attribute 차이가 패치될 것이라는 보장이 없음. 이는 성능상의 이유로 중요한데, 대부분의 앱에서 불일치가 발생하는 경우는 드물므로 모든 마크업의 유효성을 검사하는 것은 엄청나게 많은 비용이 들기 때문임.

  • 앱에 hydrateRoot 호출이 하나만 있을 가능성이 높음. 프레임워크를 사용하는 경우 프레임워크가 이 호출을 대신 수행할 수 있음.

  • 앱이 이미 렌더링된 HTML 없이 클라이언트로 렌더링되는 경우, hydrateRoot() 사용은 지원되지 않음. 대신 createRoot()를 사용할 것.


root.render(reactNode)

root.render를 호출하여 브라우저 DOM 요소에 대해 hydration 된 React 루트 내부의 React 컴포넌트를 업데이트할 수 있음.

root.render(<App />);

위 코드에서 React는 hydration 된 root에서 <App />을 업데이트함.

Parameters

  • reactNode: 업데이트할 React 노드. 일반적으로 <App />과 같은 JSX 조각이지만, createElement()로 생성된 React 엘리먼트, 문자열, 숫자, null 또는 undefined를 전달할 수도 있음.

Returns

undefined를 반환함.

Caveats

  • 루트의 hydration이 완료되기 전에 root.render를 호출하면 React는 기존에 서버에서 렌더링된 HTML 콘텐츠를 지우고 전체 루트를 클라이언트 렌더링으로 전환함.

root.unmount()

root.unmount를 호출하여 React 루트 내부에서 렌더링된 트리를 파괴할 수 있음.

root.unmount();

완전히 React로만 빌드된 앱에는 일반적으로 root.unmount 호출이 없음.

이 함수는 React 루트의 DOM 노드(또는 그 조상 노드)가 다른 코드에 의해 DOM에서 제거될 수 있는 경우에 주로 유용함. 예를 들어, DOM에서 비활성 탭을 제거하는 jQuery 탭 패널이 있다면, 탭이 제거될 때 그 안에 있는 모든 것(내부의 React 루트를 포함)도 DOM에서 제거됨. 이 경우 root.unmount를 호출하여 제거된 루트의 콘텐츠 관리를 "중지"하도록 React에 지시해야 함. 그렇지 않으면 제거된 루트 내부의 컴포넌트가 구독과 같은 전역 리소스를 정리하고 확보해야 하는 것을 모름.

root.unmount를 호출하면 루트의 모든 컴포넌트가 unmount 되고, 트리의 이벤트 핸들러나 state를 제거하는 것을 포함하여 루트 DOM 노드에서 React가 "분리"됨.

Parameters

어떤 parameter도 허용하지 않음.

Returns

undefined를 반환함.

Caveats

  • root.unmount를 호출하면 트리의 모든 컴포넌트가 unmount 되고 루트 DOM 노드에서 React가 "분리"됨.

  • root.unmount를 호출하고 나면 같은 루트에서 root.render를 다시 호출할 수 없음. Unmount 된 루트에서 root.render를 호출하려고 하면 "Cannot update an unmounted root" 오류가 발생함.


Usage

Hydrating server-rendered HTML

앱의 HTML이 react-dom/server로 생성된 경우, 클라이언트에서 이를 hydrate 해야 함.

import { hydrateRoot } from 'react-dom/client';

hydrateRoot(document.getElementById('root'), <App />);

이렇게 하면 브라우저 DOM 노드 내부의 서버 HTML이 앱의 React 컴포넌트로 hydrate 됨. 일반적으로 이 작업은 시작 시 한 번 수행함. 프레임워크를 사용하는 경우 프레임워크가 백그라운드에서 이 작업을 수행할 수 있음.

앱을 hydrate 하기 위해 React는 서버에서 생성된 초기 HTML에 컴포넌트의 로직을 "첨부"함. Hydration은 서버의 초기 HTML 스냅샷을 브라우저에서 실행되는 완전한 인터랙티브 앱으로 전환함.

<!-- index.html -->

<!-- HTML content inside <div id="root">...</div> was generated from App by react-dom/server. -->
<div id="root"><h1>Hello, world!</h1><button>You clicked me <!-- -->0<!-- --> times</button></div>
import './styles.css';
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';

hydrateRoot(
  document.getElementById('root'),
  <App />
);

hydrateRoot를 다시 호출하거나 더 많은 위치에서 호출할 필요가 없음. 이 시점부터 React는 애플리케이션의 DOM을 관리하게 됨. UI를 업데이트하기 위해 컴포넌트는 대신 state를 사용할 것.

Pitfall

hydrateRoot에 전달하는 React 트리는 서버에서와 동일한 출력을 생성해야 함.

이는 사용자 경험에 중요함. 사용자는 JavaScript 코드가 로드되기 전에 서버에서 생성된 HTML을 보는 데 시간을 소비하게 됨. 서버 렌더링은 출력의 HTML 스냅샷을 보여줌으로써 앱이 더 빨리 로드되는 착각을 불러일으킴. 갑자기 다른 콘텐츠를 표시하면 이러한 착각이 깨짐. 그렇기 때문에 서버 렌더링 출력은 클라이언트의 초기 렌더링 출력과 일치해야 함.

Hydration 오류를 유발하는 가장 일반적인 원인:

  • 루트 노드 내부의, React가 생성한 HTML 주위에 여분의 공백(예: 개행)이 있는 경우.
  • 렌더링 로직에서 typeof window !== 'undefined'와 같은 검사를 사용하는 경우.
  • 렌더링 로직에 window.matchMedia와 같은 브라우저 전용 API를 사용하는 경우.
  • 서버와 클라이언트에서 서로 다른 데이터를 렌더링하는 경우.

React는 일부 hydration 오류에서 복구되지만, 다른 버그와 마찬가지로 수정해야 함. 가장 좋은 경우에는 속도 저하로 이어지지만, 최악의 경우에는 이벤트 핸들러가 잘못된 요소에 연결될 수 있음.

Hydrating an entire document

React로 완전히 빌드된 앱은 <html> 태그를 포함하여 전체 문서를 JSX로 렌더링할 수 있음:

function App() {
  return (
    <html>
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="stylesheet" href="/styles.css"></link>
        <title>My app</title>
      </head>
      <body>
        <Router />
      </body>
    </html>
  );
}

전체 문서를 hydrate 하려면 document 글로벌을 첫 번째 인수로 hydrateRoot에 전달할 것:

import { hydrateRoot } from 'react-dom/client';
import App from './App.js';

hydrateRoot(document, <App />);

Suppressing unavoidable hydration mismatch errors

단일 요소의 attribute 또는 텍스트 콘텐츠가 서버와 클라이언트 간에 불가피하게 다른 경우(예: 타임스탬프), hydration 불일치 경고를 무음으로 처리할 수 있음.

요소에서 hydration 경고를 무음으로 설정하려면, suppressHydrationWarning={true}를 추가할 것:

<!-- index.html -->

<!-- HTML content inside <div id="root">...</div> was generated from App by react-dom/server. -->
<div id="root"><h1>Current Date: <!-- -->01/01/2020</h1></div>
// index.js

import './styles.css';
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';

hydrateRoot(document.getElementById('root'), <App />);
// App.js

export default function App() {
  return (
    <h1 suppressHydrationWarning={true}>
      Current Date: {new Date().toLocaleDateString()}
    </h1>
  );
}

이것은 한 층 깊이에서만 작동하며, 탈출구 역할을 함. 과도하게 사용하지 말 것. 텍스트 콘텐츠가 아니라면 React는 여전히 패치를 시도하지 않으므로 향후 업데이트가 있을 때까지 일관성이 유지될 수 있음.

Handling different client and server content

서버와 클라이언트에서 의도적으로 다른 것을 렌더링해야 하는 경우, two-pass 렌더링을 수행할 수 있음. 클라이언트에서 다른 것을 렌더링하는 컴포넌트는 isClient와 같은 state 변수를 읽을 수 있으며, 이 변수를 Effect에서 true로 설정할 수 있음:

// App.js

import { useState, useEffect } from "react";

export default function App() {
  const [isClient, setIsClient] = useState(false);

  useEffect(() => {
    setIsClient(true);
  }, []);

  return (
    <h1>
      {isClient ? 'Is Client' : 'Is Server'}
    </h1>
  );
}

이렇게 하면 초기 렌더링 패스는 서버와 동일한 콘텐츠를 렌더링하여 불일치를 방지하지만, 추가 패스는 hydration 직후에 동기적으로 발생함.

Pitfall

이 접근 방식은 컴포넌트를 두 번 렌더링해야 하므로 hydration 속도가 느려짐. 느린 연결 환경에서의 사용자 경험에 유의할 것. JavaScript 코드는 초기 HTML 렌더링보다 훨씬 늦게 로드될 수 있으므로, hydration 직후에 다른 UI를 렌더링하면 사용자에게 어색함을 줄 수 있음.

Updating a hydrated root component

루트의 hydrating 작업이 끝나면, root.render를 호출하여 루트 React 컴포넌트를 업데이트할 수 있음. createRoot와 달리, 초기 콘텐츠가 이미 HTML로 렌더링되었기 때문에 일반적으로는 이 작업을 수행할 필요가 없음.

Hydration 후 어느 시점에 root.render를 호출하고, 컴포넌트 트리 구조가 이전에 렌더링된 것과 일치하면, React는 sate를 유지함. 다음 예제에서 counter가 계속 증가하고 input에 타이핑한 내용이 계속 남아있는 것은, 매초마다 반복되는 render 호출로 인한 업데이트가 파괴적이지 않다는 것을 의미함:

// index.js

import { createRoot } from 'react-dom/client';
import './styles.css';
import App from './App.js';

const root = createRoot(document.getElementById('root'));

let i = 0;
setInterval(() => {
  root.render(<App counter={i} />);
  i++;
}, 1000);
export default function App({counter}) {
  return (
    <>
      <h1>Hello, world! {counter}</h1>
      <input placeholder="Type something here" />
    </>
  );
}

hydrate된 루트에서 root.render를 여러 번 호출하는 경우는 드뭄. 일반적으로 컴포넌트에서 대신 state를 업데이트함.

0개의 댓글