createRoot

김동현·2026년 3월 17일

createRoot

소개

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가 잡은 errorcomponentStack을 포함하는 errorInfo 객체로 호출돼요.
    • 선택적 onUncaughtError: 에러가 던져졌지만 Error Boundary에 의해 잡히지 않았을 때 호출되는 콜백이에요. 던져진 errorcomponentStack을 포함하는 errorInfo 객체로 호출돼요.
    • 선택적 onRecoverableError: React가 에러에서 자동으로 복구할 때 호출되는 콜백이에요. React가 던진 errorcomponentStack을 포함하는 errorInfo 객체로 호출돼요. 일부 복구 가능한 에러는 원래 에러 원인을 error.cause로 포함할 수 있어요.
    • 선택적 identifierPrefix: useId로 생성된 ID에 React가 사용하는 문자열 접두사예요. 같은 페이지에서 여러 root를 사용할 때 충돌을 피하는 데 유용해요.

반환값

createRootrenderunmount 두 메서드를 가진 객체를 반환해요.

주의사항

  • 앱이 서버 렌더링되면, createRoot() 사용은 지원되지 않아요. 대신 hydrateRoot()를 사용하세요.
  • 앱에 createRoot 호출이 하나만 있을 가능성이 높아요. 프레임워크를 사용한다면, 이 호출을 대신해줄 수 있어요.
  • 컴포넌트의 자식이 아닌 DOM 트리의 다른 부분에 JSX 조각을 렌더링하고 싶을 때(예: 모달이나 툴팁), 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.renderundefined를 반환해요.

주의사항

  • 처음 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.unmountundefined를 반환해요.

주의사항

  • root.unmount를 호출하면 트리의 모든 컴포넌트를 언마운트하고 root DOM 노드에서 React를 "분리"할 거예요.

  • root.unmount를 호출한 후에는 같은 root에서 root.render를 다시 호출할 수 없어요. 언마운트된 root에서 root.render를 호출하려고 하면 "Cannot update an unmounted root" 에러가 발생할 거예요. 하지만, 해당 노드의 이전 root가 언마운트된 후에는 같은 DOM 노드에 대해 새 root를 생성할 수 있어요.


사용법

React로 완전히 빌드된 앱 렌더링하기

앱이 React로 완전히 빌드되었다면, 전체 앱에 대해 단일 root를 생성하세요.

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

const root = createRoot(document.getElementById('root'));
root.render(<App />);

보통 시작 시 이 코드를 한 번만 실행하면 돼요. 다음을 수행할 거예요:

  1. HTML에 정의된 브라우저 DOM 노드를 찾아요.
  2. 그 안에 앱의 React 컴포넌트를 표시해요.
<!-- 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로 부분적으로 빌드된 페이지 렌더링하기

페이지가 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 컴포넌트 업데이트하기

같은 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 옵션은 두 개의 인자로 호출되는 함수예요:

  1. 던져진 error
  2. 에러의 componentStack을 포함하는 errorInfo 객체

onUncaughtErroronRecoverableError와 함께, 자체 에러 보고 시스템을 구현할 수 있어요:

// 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를 생성했지만 아무것도 표시되지 않아요

앱을 root에 실제로 렌더링하는 것을 잊지 않았는지 확인하세요:

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

const root = createRoot(document.getElementById('root'));
root.render(<App />);

그렇게 할 때까지 아무것도 표시되지 않아요.


"root.render에 두 번째 인자를 전달했습니다" 에러가 발생해요

흔한 실수는 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 />);

"Target container is not a DOM element" 에러가 발생해요

이 에러는 createRoot에 전달하는 것이 DOM 노드가 아니라는 의미예요.

무슨 일이 일어나고 있는지 확실하지 않다면, 로그를 찍어보세요:

const domNode = document.getElementById('root');
console.log(domNode); // ???
const root = createRoot(domNode);
root.render(<App />);

예를 들어, domNodenull이면, getElementByIdnull을 반환했다는 의미예요. 이는 호출 시점에 문서에 주어진 ID를 가진 노드가 없으면 발생해요. 몇 가지 이유가 있을 수 있어요:

  1. 찾고 있는 ID가 HTML 파일에서 사용한 ID와 다를 수 있어요. 오타를 확인하세요!
  2. 번들의 <script> 태그는 HTML에서 그 뒤에 나타나는 어떤 DOM 노드도 "볼 수 없어요".

이 에러를 얻는 또 다른 흔한 방법은 createRoot(domNode) 대신 createRoot(<App />)를 작성하는 거예요.


"Functions are not valid as a React child" 에러가 발생해요

이 에러는 root.render에 전달하는 것이 React 컴포넌트가 아니라는 의미예요.

<Component /> 대신 Componentroot.render를 호출하면 발생할 수 있어요:

// 🚩 잘못됨: App은 함수이지 컴포넌트가 아니에요.
root.render(App);

// ✅ 올바름: <App />은 컴포넌트예요.
root.render(<App />);

또는 함수를 호출한 결과 대신 함수를 root.render에 전달하면 발생해요:

// 🚩 잘못됨: createApp은 함수이지 컴포넌트가 아니에요.
root.render(createApp);

// ✅ 올바름: createApp을 호출해서 컴포넌트를 반환하세요.
root.render(createApp());

서버 렌더링된 HTML이 처음부터 다시 생성돼요

앱이 서버 렌더링되고 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 호출이 없을 거예요.


사이트맵

모든 문서 페이지 개요

profile
프론트에_가까운_풀스택_개발자

0개의 댓글