React18 환경에서 ReactDOM.render
를 사용하면 아래와 같은 에러가 뜬다.
React18에서는 기존에 사용하던 ReactDOM.render
대신에 ReactDOM.createRoot
가 도입되었다.
react-dom/client
를 통해 접근할 수 있다.
import * as ReactDOMClient from 'react-dom/client';
ReactDOMClient.createRoot(/*...*/);
본 포스팅은 Replacing render with createRoot #5 글을 참고했습니다!
React에서 Root란 렌더 트리의 가장 최상위 레벨이 되는 포인터를 말한다.
구 API에서는 Root를 DOM 노드를 통해 접근하기 때문에 사용자에게 노출되지 않는다. 아래처럼 Root를 생성할 때 container를 넘겨주는 형태로 사용하기 때문에 container에 변경이 없더라도 계속해서 DOM에 접근해야 한다.
import * as ReactDOM from 'react-dom';
import App from 'App';
const container = document.getElementById('app');
// 최초 렌더링 시
ReactDOM.render(<App />, container);
// 업데이트가 발생해서 다시 렌더링해도 DOM 엘리먼트를 통해 Root에 접근한다.
ReactDOM.render(<App />, container);
반면, 새로운 API에서는 Root를 생성하고 Root에서 render 함수를 호출한다.
import * as ReactDOMClient from 'react-dom/client';
import App from 'App';
const container = document.getElementById('app');
// Root를 생성한다.
const root = ReactDOMClient.createRoot(container);
// 최초 렌더링 시
root.render(<App />);
// 업데이트가 발생했을 때, container를 다시 넘겨줄 필요가 없다.
root.render(<App />);
또 다른 차이점은 hydration 방법이다.
hydration이란?
SSR의 경우, 서버에서 정적 HTML 소스를 받아 페이지를 먼저 렌더링한다. 그 후 자바스크립트 코드가 실행되면서 동적인 엘리먼트 및 이벤트 리스너가 등록된다.
기존의 ReactDOM.render()
함수는 리액트 엘리먼트를 렌더링한다. 첫번째 인자에는 리액트 컴포넌트를 전달하는데, 기존에 이미 렌더링 된 컴포넌트가 있다면 새로 렌더링하는 것이 아니라 업데이트만 해준다.
ReactDOM.hydrate()
함수는 render()
메소드와 비슷하지만, 렌더링은 하지 않고 이벤트 핸들러 등록 등의 hydration만 수행한다. 서버사이드 렌더링의 경우 마크업이 이미 채워져 있다면 렌더링을 다시 할 필요가 없다. 따라서 SSR 프레임워크와 함께 React를 사용할 경우에는 hydrate()
메소드를 사용한다.
import * as ReactDOM from 'react-dom';
import App from 'App';
const container = document.getElementById('app');
ReactDOM.hydrate(<App />, container);
그런데 React18에서는 hydrateRoot
라는 메소드가 도입되었다.
import * as ReactDOMClient from 'react-dom/client';
import App from 'App';
const container = document.getElementById('app');
// root를 생성하고 렌더링
const root = ReactDOMClient.hydrateRoot(container, <App />);
// render 함수는 따로 사용할 필요가 없다.
hydrateRoot
함수는 두번째 인자로 초기 JSX, 즉 컴포넌트를 받는다.
문서에는 이렇게 설명되어 있다.
This is because the initial client render is special and needs to match with the server tree. (서버에서 만들어진 DOM 트리와 매칭되게 하기 위해 두번째 인자로 최초 JSX를 받는다고 이해했다. 맞는 해석인가?)
만약 hydration 이후에 Root를 다시 업데이트하고 싶다면, 변수로 저장한 Root를 통해 render()
함수를 다시 호출할 수도 있다.
const root = ReactDOMClient.hydrateRoot(container, <App />);
root.render(<App />);
기존의 ReactDOM.render()
에서는 컴포넌트가 렌더링된 후 실행될 콜백 함수를 넘겨준다.
이번에 도입된 createRoot
에서는 콜백이 제거되었다.
부분적으로 SSR이 도입된 케이스에서는 콜백이 실행되는 시점이 유저가 예상한 타이밍과 다를 수 있다. 이를 피하기 위해서 문서에서는 ref
를 사용하라고 소개하고 있다.
function App() {
return (
<div>
<h1>Hello World</h1>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOMClient.render(<App />, rootElement, () => console.log("renderered"));
(기존에 위처럼 쓰는 것 대신에.....,)
function App({ callback }) {
return (
<div ref={callback}>
<h1>Hello World</h1>
</div>
);
}
const rootElement = document.getElementById("root");
const root = ReactDOMClient.createRoot(rootElement);
root.render(<App callback={() => console.log("renderered")} />);
(이렇게 쓰세요!!)
React18이 도입되긴 했지만 기존에 사용하던 API들도 아직 사용은 가능하다.
다만 React18 환경에서 해당 메소드를 사용하면 warning을 만날 수 있으니 참고하기!!
참고 :
Replacing render with createRoot (https://github.com/reactwg/react-18/discussions/5)
리액트의 hydration이란? (https://simsimjae.tistory.com/389)