createRoot를 사용하면 브라우저 DOM 노드 안에 React 컴포넌트를 표시하기 위한 root를 생성할 수 있어요.
const root = createRoot(domNode, options?)
createRoot는 React 앱을 시작하는 첫 번째 단계예요! HTML의 특정 DOM 요소를 React가 관리하는 영역으로 만들어주는 거죠.
createRoot(domNode, options?)브라우저 DOM 요소 안에 콘텐츠를 표시하기 위한 React root를 생성하려면 createRoot를 호출하세요.
import { createRoot } from 'react-dom/client';
const domNode = document.getElementById('root');
const root = createRoot(domNode);
React는 domNode에 대한 root를 생성하고, 그 안의 DOM 관리를 맡게 될 거예요. root를 생성한 후에는 root.render를 호출해서 그 안에 React 컴포넌트를 표시해야 해요:
root.render(<App />);
React로 완전히 빌드된 앱은 보통 root 컴포넌트에 대해 단 하나의 createRoot 호출만 있어요. 페이지의 일부에만 React를 "뿌려서" 사용하는 페이지는 필요한 만큼 여러 개의 별도 root를 가질 수 있어요.
domNode: DOM 요소예요. React는 이 DOM 요소에 대한 root를 생성하고, 렌더링된 React 콘텐츠를 표시하기 위해 render 같은 root의 함수를 호출할 수 있게 해줄 거예요.
선택적 options: 이 React root에 대한 옵션을 가진 객체예요.
onCaughtError: Error Boundary에서 React가 에러를 잡았을 때 호출되는 콜백이에요. Error Boundary가 잡은 error와 componentStack을 포함하는 errorInfo 객체로 호출돼요.onUncaughtError: 에러가 던져졌지만 Error Boundary에 의해 잡히지 않았을 때 호출되는 콜백이에요. 던져진 error와 componentStack을 포함하는 errorInfo 객체로 호출돼요.onRecoverableError: React가 에러에서 자동으로 복구할 때 호출되는 콜백이에요. React가 던진 error와 componentStack을 포함하는 errorInfo 객체로 호출돼요. 일부 복구 가능한 에러는 원래 에러 원인을 error.cause로 포함할 수 있어요.identifierPrefix: useId로 생성된 ID에 React가 사용하는 문자열 접두사예요. 같은 페이지에서 여러 root를 사용할 때 충돌을 피하는 데 유용해요.createRoot는 render와 unmount 두 메서드를 가진 객체를 반환해요.
createRoot() 사용은 지원되지 않아요. 대신 hydrateRoot()를 사용하세요.createRoot 호출이 하나만 있을 가능성이 높아요. 프레임워크를 사용한다면, 이 호출을 대신해줄 수 있어요.createRoot 대신 createPortal을 사용하세요.root.render(reactNode)React root의 브라우저 DOM 노드에 JSX 조각("React 노드")을 표시하려면 root.render를 호출하세요.
root.render(<App />);
React는 root에 <App />을 표시하고, 그 안의 DOM 관리를 맡을 거예요.
reactNode: 표시하고 싶은 React 노드예요. 보통 <App /> 같은 JSX 조각이지만, createElement()로 생성한 React 요소, 문자열, 숫자, null, undefined도 전달할 수 있어요.root.render는 undefined를 반환해요.
처음 root.render를 호출하면, React는 React 컴포넌트를 렌더링하기 전에 React root 안의 모든 기존 HTML 콘텐츠를 지울 거예요.
root의 DOM 노드가 서버에서 또는 빌드 중에 React로 생성된 HTML을 포함하고 있다면, 대신 hydrateRoot()를 사용하세요. 이는 기존 HTML에 이벤트 핸들러를 연결해요.
같은 root에서 render를 여러 번 호출하면, React는 전달한 최신 JSX를 반영하기 위해 필요한 대로 DOM을 업데이트할 거예요. React는 이전에 렌더링된 트리와 "매칭"해서 DOM의 어떤 부분을 재사용할 수 있고 어떤 부분을 다시 생성해야 하는지 결정할 거예요. 같은 root에서 render를 다시 호출하는 것은 root 컴포넌트에서 set 함수를 호출하는 것과 비슷해요: React는 불필요한 DOM 업데이트를 피해요.
렌더링이 시작되면 동기적이지만, root.render(...)는 그렇지 않아요. 이는 root.render() 뒤의 코드가 특정 렌더링의 어떤 effect (useLayoutEffect, useEffect)가 실행되기 전에 실행될 수 있다는 뜻이에요. 이는 보통 괜찮고 조정이 필요한 경우가 드물어요. effect 타이밍이 중요한 드문 경우에는, root.render(...)를 flushSync로 감싸서 초기 렌더링이 완전히 동기적으로 실행되도록 할 수 있어요.
const root = createRoot(document.getElementById('root'));
root.render(<App />);
// 🚩 HTML은 아직 렌더링된 <App />을 포함하지 않아요:
console.log(document.body.innerHTML);
root.unmount()React root 안의 렌더링된 트리를 제거하려면 root.unmount를 호출하세요.
root.unmount();
React로 완전히 빌드된 앱은 보통 root.unmount 호출이 전혀 없어요.
이는 주로 React root의 DOM 노드(또는 그 조상 중 하나)가 다른 코드에 의해 DOM에서 제거될 수 있을 때 유용해요. 예를 들어, 비활성 탭을 DOM에서 제거하는 jQuery 탭 패널을 상상해보세요. 탭이 제거되면, 그 안의 모든 것(React root 포함)도 DOM에서 제거될 거예요. 그런 경우, root.unmount를 호출해서 제거된 root의 콘텐츠 관리를 "중지"하도록 React에게 알려야 해요. 그렇지 않으면, 제거된 root 안의 컴포넌트들이 구독 같은 전역 리소스를 정리하고 해제할 줄 모를 거예요.
root.unmount를 호출하면 root의 모든 컴포넌트를 언마운트하고 트리의 이벤트 핸들러나 state를 포함해서 root DOM 노드에서 React를 "분리"할 거예요.
root.unmount는 어떤 매개변수도 받지 않아요.
root.unmount는 undefined를 반환해요.
root.unmount를 호출하면 트리의 모든 컴포넌트를 언마운트하고 root DOM 노드에서 React를 "분리"할 거예요.
root.unmount를 호출한 후에는 같은 root에서 root.render를 다시 호출할 수 없어요. 언마운트된 root에서 root.render를 호출하려고 하면 "Cannot update an unmounted root" 에러가 발생할 거예요. 하지만, 해당 노드의 이전 root가 언마운트된 후에는 같은 DOM 노드에 대해 새 root를 생성할 수 있어요.
앱이 React로 완전히 빌드되었다면, 전체 앱에 대해 단일 root를 생성하세요.
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
보통 시작 시 이 코드를 한 번만 실행하면 돼요. 다음을 수행할 거예요:
<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head><title>My app</title></head>
<body>
<!-- 이것이 DOM 노드예요 -->
<div id="root"></div>
</body>
</html>
// src/index.js
import { createRoot } from 'react-dom/client';
import App from './App.js';
import './styles.css';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
// src/App.js
import { useState } from 'react';
export default function App() {
return (
<>
<h1>Hello, world!</h1>
<Counter />
</>
);
}
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
You clicked me {count} times
</button>
);
}
앱이 React로 완전히 빌드되었다면, 더 이상 root를 생성하거나 root.render를 다시 호출할 필요가 없어요.
이 시점부터 React는 전체 앱의 DOM을 관리할 거예요. 더 많은 컴포넌트를 추가하려면, App 컴포넌트 안에 중첩하세요. UI를 업데이트해야 할 때, 각 컴포넌트는 state를 사용해서 할 수 있어요. 모달이나 툴팁 같은 추가 콘텐츠를 DOM 노드 바깥에 표시해야 할 때는, portal로 렌더링하세요.
참고
HTML이 비어있으면, 앱의 JavaScript 코드가 로드되고 실행될 때까지 사용자는 빈 페이지를 보게 돼요:
<div id="root"></div>이는 매우 느리게 느껴질 수 있어요! 이를 해결하려면, 서버에서 또는 빌드 중에 컴포넌트에서 초기 HTML을 생성할 수 있어요. 그러면 방문자가 JavaScript 코드가 로드되기 전에 텍스트를 읽고, 이미지를 보고, 링크를 클릭할 수 있어요. 이 최적화를 기본적으로 제공하는 프레임워크 사용을 권장해요. 언제 실행되는지에 따라, 이를 서버 사이드 렌더링(SSR) 또는 정적 사이트 생성(SSG)이라고 해요.
⚠️ 주의
서버 렌더링이나 정적 생성을 사용하는 앱은
createRoot대신hydrateRoot를 호출해야 해요. React는 그러면 HTML의 DOM 노드를 파괴하고 다시 생성하는 대신 hydrate(재사용)할 거예요.
페이지가 React로 완전히 빌드되지 않았다면, createRoot를 여러 번 호출해서 React가 관리하는 각 최상위 UI 조각에 대한 root를 생성할 수 있어요. 각 root에서 root.render를 호출해서 다른 콘텐츠를 표시할 수 있어요.
여기서는 index.html 파일에 정의된 두 개의 DOM 노드에 두 개의 다른 React 컴포넌트가 렌더링돼요:
<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head><title>My app</title></head>
<body>
<nav id="navigation"></nav>
<main>
<p>This paragraph is not rendered by React (open index.html to verify).</p>
<section id="comments"></section>
</main>
</body>
</html>
// src/index.js
import './styles.css';
import { createRoot } from 'react-dom/client';
import { Comments, Navigation } from './Components.js';
const navDomNode = document.getElementById('navigation');
const navRoot = createRoot(navDomNode);
navRoot.render(<Navigation />);
const commentDomNode = document.getElementById('comments');
const commentRoot = createRoot(commentDomNode);
commentRoot.render(<Comments />);
// src/Components.js
export function Navigation() {
return (
<ul>
<NavLink href="/">Home</NavLink>
<NavLink href="/about">About</NavLink>
</ul>
);
}
function NavLink({ href, children }) {
return (
<li>
<a href={href}>{children}</a>
</li>
);
}
export function Comments() {
return (
<>
<h2>Comments</h2>
<Comment text="Hello!" author="Sophie" />
<Comment text="How are you?" author="Sunil" />
</>
);
}
function Comment({ text, author }) {
return (
<p>{text} — <i>{author}</i></p>
);
}
nav ul { padding: 0; margin: 0; }
nav ul li { display: inline-block; margin-right: 20px; }
document.createElement()로 새 DOM 노드를 생성하고 수동으로 문서에 추가할 수도 있어요.
const domNode = document.createElement('div');
const root = createRoot(domNode);
root.render(<Comment />);
document.body.appendChild(domNode); // 문서 어디에나 추가할 수 있어요
DOM 노드에서 React 트리를 제거하고 사용된 모든 리소스를 정리하려면, root.unmount를 호출하세요.
root.unmount();
이는 주로 React 컴포넌트가 다른 프레임워크로 작성된 앱 안에 있을 때 유용해요.
같은 root에서 render를 여러 번 호출할 수 있어요. 컴포넌트 트리 구조가 이전에 렌더링된 것과 일치하는 한, React는 state를 보존할 거예요. input에 타이핑할 수 있다는 것을 주목하세요. 이는 이 예시에서 매초 반복되는 render 호출의 업데이트가 파괴적이지 않다는 것을 의미해요:
// src/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);
// src/App.js
export default function App({counter}) {
return (
<>
<h1>Hello, world! {counter}</h1>
<input placeholder="Type something here" />
</>
);
}
render를 여러 번 호출하는 것은 흔하지 않아요. 보통 컴포넌트들이 대신 state를 업데이트할 거예요.
기본적으로 React는 모든 에러를 콘솔에 로그해요. 자체 에러 보고를 구현하려면, 선택적 에러 핸들러 root 옵션 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,
});
}
},
});
onCaughtError 옵션은 두 개의 인자로 호출되는 함수예요:
errorcomponentStack을 포함하는 errorInfo 객체onUncaughtError와 onRecoverableError와 함께, 자체 에러 보고 시스템을 구현할 수 있어요:
// src/reportError.js
function reportError({ type, error, errorInfo }) {
// 구체적인 구현은 여러분에게 달려 있어요.
// `console.error()`는 시연 목적으로만 사용돼요.
console.error(type, error, "Component Stack: ");
console.error("Component Stack: ", errorInfo.componentStack);
}
export function onCaughtErrorProd(error, errorInfo) {
if (error.message !== "Known error") {
reportError({ type: "Caught", error, errorInfo });
}
}
export function onUncaughtErrorProd(error, errorInfo) {
reportError({ type: "Uncaught", error, errorInfo });
}
export function onRecoverableErrorProd(error, errorInfo) {
reportError({ type: "Recoverable", error, errorInfo });
}
// src/index.js
import { createRoot } from "react-dom/client";
import App from "./App.js";
import {
onCaughtErrorProd,
onRecoverableErrorProd,
onUncaughtErrorProd,
} from "./reportError";
const container = document.getElementById("root");
const root = createRoot(container, {
// 개발 모드에서는 React의 기본 핸들러를 활용하거나
// 개발용 자체 오버레이를 구현하기 위해
// 이 옵션들을 제거하는 것을 잊지 마세요.
// 핸들러들은 시연 목적으로만 여기에 무조건 지정되어 있어요.
onCaughtError: onCaughtErrorProd,
onRecoverableError: onRecoverableErrorProd,
onUncaughtError: onUncaughtErrorProd,
});
root.render(<App />);
// src/App.js
import { Component, useState } from "react";
function Boom() {
foo.bar = "baz";
}
class ErrorBoundary extends Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
export default function App() {
const [triggerUncaughtError, settriggerUncaughtError] = useState(false);
const [triggerCaughtError, setTriggerCaughtError] = useState(false);
return (
<>
<button onClick={() => settriggerUncaughtError(true)}>
Trigger uncaught error
</button>
{triggerUncaughtError && <Boom />}
<button onClick={() => setTriggerCaughtError(true)}>
Trigger caught error
</button>
{triggerCaughtError && (
<ErrorBoundary>
<Boom />
</ErrorBoundary>
)}
</>
);
}
앱을 root에 실제로 렌더링하는 것을 잊지 않았는지 확인하세요:
import { createRoot } from 'react-dom/client';
import App from './App.js';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
그렇게 할 때까지 아무것도 표시되지 않아요.
흔한 실수는 createRoot의 옵션을 root.render(...)에 전달하는 거예요:
⛔ Warning: You passed a second argument to root.render(...) but it only accepts one argument.
수정하려면, root 옵션을 root.render(...)가 아니라 createRoot(...)에 전달하세요:
// 🚩 잘못됨: root.render는 한 개의 인자만 받아요.
root.render(App, {onUncaughtError});
// ✅ 올바름: 옵션을 createRoot에 전달하세요.
const root = createRoot(container, {onUncaughtError});
root.render(<App />);
이 에러는 createRoot에 전달하는 것이 DOM 노드가 아니라는 의미예요.
무슨 일이 일어나고 있는지 확실하지 않다면, 로그를 찍어보세요:
const domNode = document.getElementById('root');
console.log(domNode); // ???
const root = createRoot(domNode);
root.render(<App />);
예를 들어, domNode가 null이면, getElementById가 null을 반환했다는 의미예요. 이는 호출 시점에 문서에 주어진 ID를 가진 노드가 없으면 발생해요. 몇 가지 이유가 있을 수 있어요:
<script> 태그는 HTML에서 그 뒤에 나타나는 어떤 DOM 노드도 "볼 수 없어요".이 에러를 얻는 또 다른 흔한 방법은 createRoot(domNode) 대신 createRoot(<App />)를 작성하는 거예요.
이 에러는 root.render에 전달하는 것이 React 컴포넌트가 아니라는 의미예요.
<Component /> 대신 Component로 root.render를 호출하면 발생할 수 있어요:
// 🚩 잘못됨: App은 함수이지 컴포넌트가 아니에요.
root.render(App);
// ✅ 올바름: <App />은 컴포넌트예요.
root.render(<App />);
또는 함수를 호출한 결과 대신 함수를 root.render에 전달하면 발생해요:
// 🚩 잘못됨: createApp은 함수이지 컴포넌트가 아니에요.
root.render(createApp);
// ✅ 올바름: createApp을 호출해서 컴포넌트를 반환하세요.
root.render(createApp());
앱이 서버 렌더링되고 React로 생성된 초기 HTML을 포함하고 있다면, root를 생성하고 root.render를 호출하면 모든 HTML을 삭제하고, 모든 DOM 노드를 처음부터 다시 생성하는 것을 알 수 있어요. 이는 더 느릴 수 있고, 포커스와 스크롤 위치를 리셋하며, 다른 사용자 입력을 잃을 수 있어요.
서버 렌더링된 앱은 createRoot 대신 hydrateRoot를 사용해야 해요:
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(
document.getElementById('root'),
<App />
);
API가 다르다는 점에 주목하세요. 특히, 보통 추가 root.render 호출이 없을 거예요.