[React 19] 공식문서 톺아보기 - React DOM 클라이언트 API
• Study
리액트 루트를 생성하는 createRoot와 hydrateRoot에 대해 공부하였습니다.
createRoot
createRoot
로 브라우저 DOM 노드 안에 React 컴포넌트를 표시하는 루트를 생성할 수 있습니다.
const root = createRoot(domNode, options?)
const root = createRoot(domNode, options?)
import { createRoot } from 'react-dom/client';
const domNode = document.getElementById('root');
const root = createRoot(domNode);
root.render(<App />);
import { createRoot } from 'react-dom/client';
const domNode = document.getElementById('root');
const root = createRoot(domNode);
root.render(<App />);
createRoot
를 호출하면 브라우저 DOM 엘리먼트 안에 콘텐츠를 표시할 수 있는 React 루트를 생성합니다.- 루트를 생성한 후에는
root.render
를 호출해 그 안에 React 컴포넌트를 표시해야 합니다. - 일반적으로 한번만 호출하며, 페이지에서 리액트를 부분부분 사용하는 경우에는 여러번 호출할수도 있다.
주의사항
- 앱이 서버에서 렌더링 되는 경우
createRoot()
는 사용할 수 없습니다. 대신hydrateRoot()
를 사용해야합니다. - 컴포넌트의 자식이 아닌 DOM 트리의 다른 부분(예: 모달 또는 툴팁)에 JSX 조각을 렌더링하려는 경우,
createRoot
대신createPortal
을 사용합니다.
반환값 root.render(reactNode)
root.render
를 호출하여 JSX 조각(“React 노드”)을 React 루트의 브라우저 DOM 노드에 표시합니다.reactNode
: 표시하려는 React 노드. 일반적으로<App />
과 같은 JSX 조각이 되지만,createElement()
로 작성한 React 엘리먼트, 문자열, 숫자,null
,undefined
등을 전달할 수도 있습니다.root.render
를 처음 호출하면 React는 React 컴포넌트를 렌더링하기 전에 React 루트 내부의 모든 기존 HTML 콘텐츠를 지웁니다.- 동일한 루트에서
render
를 두 번 이상 호출하면, React는 필요에 따라 DOM을 업데이트하여 사용자가 전달한 최신 JSX를 반영합니다. React는 이전에 렌더링 된 트리와 “비교”해서 재사용할 수 있는 부분과 다시 만들어야 하는 부분을 결정합니다. root.render(<App />);
는 비동기적으로 동작할 수 있으므로 동작 타이밍이 중요한 경우에는flushSync
를 이용하여 동기화할 수 있다.
hydrateRoot
- 서버에서 미리 생성된 HTML이 이미 존재하는 경우 (예: Next.js, Gatsby 등의 프레임워크 사용 시), 일반적인
ReactDOM.render()
또는ReactDOM.createRoot()
를 사용하면 서버에서 생성된 기존 HTML을 무시하고 클라이언트 측에서 처음부터 새로 렌더링하게 됩니다. - 이를 해결하기 위해서
hydrateRoot()
를 사용하면- 서버에서 생성된 기존 HTML을 재사용
- 해당 HTML에 React 이벤트 핸들러만 "부착"(hydrate)
- 불필요한 초기 렌더링을 건너뛰어 성능 향상
- 장점
- 성능: 전체 앱을 다시 렌더링하지 않아 초기 로딩이 빠름
- SEO: 서버에서 생성된 HTML을 그대로 사용하므로 검색 엔진에 친화적
- 사용자 경험: 콘텐츠가 즉시 표시된 후에 상호작용 가능
반환값 root.unmount()
-
root.unmount
를 호출하면 React 루트 내부에서 렌더링된 트리를 삭제합니다. -
온전히 React만으로 작성된 앱에는 일반적으로 호출될 일이 없다.
-
React가 관리하는 DOM 노드가 React 외부의 코드(jQuery 등)에 의해 강제로 제거되는 경우, DOM에서는 제거되었지만 React의 클린업 함수는 실행되지 않고 구독중인것도 여전히 유지되는 문제가 발생한다. 이를 해결하기 위해 사용한다.
-
주의사항
root.unmount
를 한 번 호출한 후에는 같은 루트에서root.render
를 다시 호출할 수 없습니다.- 다만
createRoot(domNode, options?)
를 통해root
를 생성할때 사용한domNode
는 재사용할 수 있다.
const root1 = ReactDOM.createRoot(domNode); root1.render(<NewApp />); // 마운트 해제 root1.unmount(); // 오류 발생! (같은 루트 인스턴스 재사용 시도) // root1.render(<NewApp />); // Error: Cannot update an unmounted root // domNode 재사용 가능 const root2 = ReactDOM.createRoot(domNode); root2.render(<NewApp />);
const root1 = ReactDOM.createRoot(domNode); root1.render(<NewApp />); // 마운트 해제 root1.unmount(); // 오류 발생! (같은 루트 인스턴스 재사용 시도) // root1.render(<NewApp />); // Error: Cannot update an unmounted root // domNode 재사용 가능 const root2 = ReactDOM.createRoot(domNode); root2.render(<NewApp />);
프로덕션에서의 오류 로깅
기본적으로 React는 모든 오류를 콘솔에 기록합니다. 자체 오류 보고를 구현하려면 선택적 오류 처리기 루트 옵션인 onUncaughtError
, onCaughtError
와 를 제공할 수 있습니다 onRecoverableError
.
import { createRoot } from 'react-dom/client';
import { reportCaughtError } from './reportError';
const container = document.getElementById('root');
const root = createRoot(container, {
onCaughtError: (error, errorInfo) => {
if (error.message !== 'Known error') {
reportCaughtError({
error,
componentStack: errorInfo.componentStack,
});
}
},
});
import { createRoot } from 'react-dom/client';
import { reportCaughtError } from './reportError';
const container = document.getElementById('root');
const root = createRoot(container, {
onCaughtError: (error, errorInfo) => {
if (error.message !== 'Known error') {
reportCaughtError({
error,
componentStack: errorInfo.componentStack,
});
}
},
});
- 에러가 발생했을때 콘솔에 표기하는게 아니라 자체 로깅시스템에 보낼때 유용
hydrateRoot
hydrateRoot
는 이전에 react-dom/server
로 생성된 HTML 콘텐츠를 가진 브라우저 DOM 노드 안에 React 컴포넌트를 표시할 수 있게 해줍니다.
const root = hydrateRoot(domNode, reactNode, options?)
const root = hydrateRoot(domNode, reactNode, options?)
import { hydrateRoot } from 'react-dom/client';
const domNode = document.getElementById('root');
const root = hydrateRoot(domNode, reactNode);
import { hydrateRoot } from 'react-dom/client';
const domNode = document.getElementById('root');
const root = hydrateRoot(domNode, reactNode);
hydrateRoot()
는 렌더링된 컨텐츠가 서버에서 렌더링된 컨텐츠와 동일할 것을 기대합니다. 따라서 불일치 사항은 버그로 취급하고 수정해야 합니다.- 서버에서 렌더링된 HTML(SSR)에 React를 "붙이는"(hydrate) 과정입니다. 이때 React는 기존 DOM 구조를 재사용하면서 이벤트 핸들러 등을 연결합니다.
- 그래서 사전에 HTML을 구성하지 않고
hydrateRoot()
만 실행할 경우는 아무런 동작도 실행하지 않는다.
- 그래서 사전에 HTML을 구성하지 않고
- 앱에서
hydrateRoot
호출이 단 한번만 있을 가능성이 높습니다. 프레임워크를 사용한다면, 프레임워크가 이 호출을 대신 수행할 수도 있습니다. - 앱을 사전에 렌더링된 HTML 없이 클라이언트에서 직접 렌더링한다면(CSR),
hydrateRoot()
는 지원되지 않습니다.createRoot()
를 대신 사용해야합니다. hydrateRoot
를 호출하여 이미 서버 환경에서 렌더링된 기존 HTML에 React를 “붙여넣기” 합니다.- 반환값 root에 대해서
root.render(reactNode)
와root.unmount()
실행이 가능하다.
createRoot 와 다른 점
createRoot
에 들어가는domNode
는 비어있어야한다.hydrateRoot
에 들어가는domNode
는 서버에서 렌더링된 HTML이 있어야한다.hydrateRoot
를 호출할때는reactNode
를 인자로 반드시 전달해야하며domNode
하고 비교하여 불일치 하는 부분만 클라이언트에서 리렌더링한다.- 클라이언트에서 리렌더링하기 위해서는 추가로
root.render(<App />);
에 대한 코드가 필요하다. - 만약 ssr만 지원하는 경우는
hydrateRoot
만 실행하고, 별도의render()
호출을 하지 않아도 된다.
- 클라이언트에서 리렌더링하기 위해서는 추가로
- createRoot 는 새로 Virtual DOM 생성 후 전체 렌더링을 하고 hydrateRoot 는 기존 DOM과 Virtual DOM 비교 후 Hydration 을 진행한다.
Reconciliation 과정의 경우, hydrateRoot
와 root.render
에서 모두 거치는데
hydrateRoot
: 초기 hydration 시 서버 DOM과 클라이언트 트리의 일치 여부를 검증root.render
: 상태 변화 시 이전 Virtual DOM과 새 Virtual DOM을 비교
주의할점
- 루트가 Hydrate를 완료하기 전에
root.render
를 호출하면, React는 서버에서 렌더링된 HTML을 모두 없애고 클라이언트에서 렌더링된 컴포넌트들로 완전히 교체합니다. hydrateRoot
가 완료되고나서root.render
를 호출할때 VDOM 트리(Virtual DOM)가 이전과 완전히 동일하다면 React는 실제 DOM에 어떠한 변화도 주지 않습니다.
Hydration 오류 원인들
- React를 통해 만들어진 HTML의 루트 노드안에 공백 혹은 개행같은 추가적인 공백.
typeof window !== 'undefined'
과 같은 조건을 렌더링 로직에서 사용.window.matchMedia
같은 브라우저에서만 사용가능한 API를 렌더링 로직에 사용.- 서버와 클라이언트에서 서로 다른 데이터를 렌더링.
서로 다른 클라이언트와 서버 컨텐츠 다루기
- 의도적으로 서버와 클라이언트에서 다른 것을 렌더링 할 수 있다.
- 다만 서버에서 미리 HTML을 구성하는 경우는 두번 렌더링해야하기 때문에 성능에 문제가 생길수있다.
Hydration된 루트 컴포넌트를 업데이트하기
- Hydration이 끝난 후에,
root.render
를 호출해 React 컴포넌트를 업데이트 할 수 있습니다.createRoot
와는 다르게 HTML로 최초의 컨텐츠가 이미 렌더링 되어 있기 때문에 자주 사용할 필요는 없습니다 - Hydration 후 어떤 시점에
root.render
를 호출한다면, 그리고 컴포넌트의 트리 구조가 이전에 렌더링했던 구조와 일치한다면, React는 State를 그대로 보존합니다. 입력 창Input에 어떻게 타이핑하든지 간에 문제가 발생하지 않습니다. 즉, 아래 예시에서처럼 매초 마다 상태를 업데이트하는 반복적인render
를 문제 없이 렌더링 한다는 것을 알 수 있습니다.
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
import './styles.css';
const root = hydrateRoot(document.getElementById('root'), <App counter={0} />);
let i = 0;
setInterval(() => {
root.render(<App counter={i} />);
i++;
}, 1000);
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
import './styles.css';
const root = hydrateRoot(document.getElementById('root'), <App counter={0} />);
let i = 0;
setInterval(() => {
root.render(<App counter={i} />);
i++;
}, 1000);