hydrateRoot

김동현·2026년 3월 17일

hydrateRoot

소개

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

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

레퍼런스

hydrateRoot(domNode, reactNode, options?)

서버 환경에서 React에 의해 이미 렌더링된 기존 HTML에 React를 "연결(attach)"하려면 hydrateRoot를 호출하세요.

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

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

React는 domNode 안에 존재하는 HTML에 연결되어, 그 안의 DOM 관리를 맡게 될 거예요. React로 완전히 빌드된 앱은 보통 루트 컴포넌트와 함께 hydrateRoot 호출을 하나만 가지고 있어요.

💡 부연 설명: "hydrate(수화)"라는 용어가 생소할 수 있는데, 이건 "마른 HTML에 물을 주어 살아있게 만든다"는 비유적 표현이에요. 서버에서 생성한 정적인 HTML을 브라우저에서 React가 받아서 상호작용 가능한 동적인 앱으로 만드는 과정을 의미해요.

일반적인 흐름은:
1. 서버에서 React 컴포넌트를 HTML 문자열로 렌더링
2. 브라우저에 HTML 전송 → 사용자가 빠르게 콘텐츠 볼 수 있음
3. JavaScript 로드 완료되면 hydrateRoot 호출
4. React가 기존 HTML에 이벤트 핸들러와 상태 관리 등을 연결
5. 이제 완전히 상호작용 가능한 앱이 됨!

아래에서 더 많은 예제를 확인하세요.

매개변수(Parameters)

  • domNode: 서버에서 루트 엘리먼트로 렌더링된 DOM 엘리먼트예요.

  • reactNode: 기존 HTML을 렌더링하는 데 사용된 "React 노드"예요. 보통 renderToPipeableStream(<App />)같은 ReactDOM Server 메서드로 렌더링된 <App /> 같은 JSX 조각일 거예요.

  • 선택적 options: 이 React 루트에 대한 옵션이 담긴 객체예요.

    • 선택적 onCaughtError: Error Boundary에서 React가 에러를 캐치했을 때 호출되는 콜백이에요. Error Boundary에 캐치된 errorcomponentStack을 포함하는 errorInfo 객체와 함께 호출돼요.
    • 선택적 onUncaughtError: 에러가 발생했지만 Error Boundary에 캐치되지 않았을 때 호출되는 콜백이에요. 발생한 errorcomponentStack을 포함하는 errorInfo 객체와 함께 호출돼요.
    • 선택적 onRecoverableError: React가 에러로부터 자동으로 복구할 때 호출되는 콜백이에요. React가 던지는 errorcomponentStack을 포함하는 errorInfo 객체와 함께 호출돼요. 일부 복구 가능한 에러는 원래 에러 원인을 error.cause로 포함할 수 있어요.
    • 선택적 identifierPrefix: useId에 의해 생성된 ID에 React가 사용하는 문자열 접두사예요. 같은 페이지에서 여러 루트를 사용할 때 충돌을 피하는 데 유용해요. 서버에서 사용된 접두사와 동일해야 해요.

반환값(Returns)

hydrateRoot는 두 개의 메서드를 가진 객체를 반환해요: renderunmount.

주의사항(Caveats)

  • hydrateRoot()는 렌더링된 콘텐츠가 서버에서 렌더링된 콘텐츠와 동일할 것으로 예상해요. 불일치를 버그로 취급하고 수정해야 해요.
  • 개발 모드에서 React는 hydration 중에 불일치에 대해 경고해요. 불일치의 경우 속성 차이가 패치된다는 보장은 없어요. 이건 성능상의 이유로 중요한데, 대부분의 앱에서 불일치는 드물기 때문에 모든 마크업을 검증하는 건 엄청나게 비용이 많이 들거든요.
  • 앱에는 hydrateRoot 호출이 하나만 있을 가능성이 높아요. 프레임워크를 사용한다면, 프레임워크가 이 호출을 대신 해줄 수도 있어요.
  • 이미 렌더링된 HTML 없이 앱이 클라이언트 렌더링되는 경우, hydrateRoot()를 사용하는 건 지원되지 않아요. 대신 createRoot()를 사용하세요.

💡 부연 설명:

  • hydrateRoot: 서버에서 만든 HTML이 이미 있을 때 사용 (SSR)
  • createRoot: 브라우저에서 처음부터 렌더링할 때 사용 (CSR)

SSR(Server-Side Rendering)을 사용하면:

  • 장점: 초기 로딩 속도 빠름, SEO 좋음
  • 단점: 서버 설정 복잡, hydration 불일치 주의 필요

CSR(Client-Side Rendering)을 사용하면:

  • 장점: 서버 부담 적음, 구현 간단
  • 단점: 초기 로딩 느림, SEO 불리

root.render(reactNode)

hydrate된 React 루트 내부의 브라우저 DOM 엘리먼트에 대한 React 컴포넌트를 업데이트하려면 root.render를 호출하세요.

root.render(<App />);

React는 hydrate된 root 안의 <App />을 업데이트할 거예요.

아래에서 더 많은 예제를 확인하세요.

매개변수(Parameters)

  • reactNode: 업데이트하려는 "React 노드"예요. 보통 <App /> 같은 JSX 조각이지만, createElement()로 생성한 React 엘리먼트, 문자열, 숫자, null, 또는 undefined를 전달할 수도 있어요.

반환값(Returns)

root.renderundefined를 반환해요.

주의사항(Caveats)

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

💡 부연 설명: 이건 성능에 좋지 않아요! SSR의 장점(빠른 초기 로딩)을 잃게 되니까요. 가능하면 hydration이 완료될 때까지 기다린 후에 render를 호출하거나, 애초에 render를 호출할 필요가 없도록 상태 관리를 하는 게 좋아요.


root.unmount()

React 루트 내부의 렌더링된 트리를 파괴하려면 root.unmount를 호출하세요.

root.unmount();

React로 완전히 빌드된 앱은 보통 root.unmount에 대한 호출이 전혀 없어요.

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

root.unmount를 호출하면 루트의 모든 컴포넌트를 언마운트하고 루트 DOM 노드에서 React를 "분리(detach)"해서, 트리의 모든 이벤트 핸들러나 상태를 제거할 거예요.

💡 부연 설명: 실제 사용 예시를 들어보면:

// jQuery로 만든 탭 UI
$('#tabs').tabs({
  beforeActivate: function(event, ui) {
    // 탭이 바뀔 때 이전 탭의 React 루트를 정리
    const oldTab = ui.oldPanel[0];
    const reactRoot = oldTab._reactRoot;
    if (reactRoot) {
      reactRoot.unmount(); // React에게 정리하라고 알림
    }
  }
});

이렇게 하지 않으면 메모리 누수가 발생할 수 있어요!

매개변수(Parameters)

root.unmount는 어떤 매개변수도 받지 않아요.

반환값(Returns)

root.unmountundefined를 반환해요.

주의사항(Caveats)

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

  • root.unmount를 호출하면 루트에서 root.render를 다시 호출할 수 없어요. 언마운트된 루트에서 root.render를 호출하려고 하면 "Cannot update an unmounted root" 에러가 발생할 거예요.


사용법

서버 렌더링된 HTML을 hydrate하기

앱의 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 스냅샷을 브라우저에서 실행되는 완전히 상호작용 가능한 앱으로 바꿔줘요.

<!-- public/index.html -->
<!--
  <div id="root">...</div> 안의 HTML 콘텐츠는
  react-dom/server에 의해 App에서 생성되었어요.
-->
<div id="root"><h1>Hello, world!</h1><button>You clicked me <!-- -->0<!-- --> times</button></div>
// src/index.js
import './styles.css';
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';

hydrateRoot(
  document.getElementById('root'),
  <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>
  );
}

hydrateRoot를 다시 호출하거나 더 많은 곳에서 호출할 필요는 없어요. 이 시점부터 React가 애플리케이션의 DOM을 관리할 거예요. UI를 업데이트하려면, 컴포넌트가 대신 state를 사용할 거예요.

💡 부연 설명:
1. 서버에서 <App />을 HTML로 렌더링 → 사용자에게 전송
2. 브라우저가 HTML 표시 → 사용자가 콘텐츠 볼 수 있음 (아직 상호작용 불가)
3. JavaScript 로드 완료 → hydrateRoot 실행
4. React가 기존 HTML과 컴포넌트 매칭 → 이벤트 핸들러 연결
5. 이제 버튼 클릭 등 상호작용 가능!

이 과정을 "hydration"이라고 부르는 이유는, 이미 있는 HTML(마른 땅)에 React의 상호작용 기능(물)을 더해서 살아있는 앱으로 만들기 때문이에요.

⚠️ 함정(Pitfall)

hydrateRoot에 전달하는 React 트리는 서버에서와 같은 출력을 생성해야 해요.

이건 사용자 경험에 중요해요. 사용자는 JavaScript 코드가 로드되기 전에 서버에서 생성된 HTML을 보면서 시간을 보낼 거예요. 서버 렌더링은 출력의 HTML 스냅샷을 보여줌으로써 앱이 더 빠르게 로드되는 것처럼 보이는 착시를 만들어요. 갑자기 다른 콘텐츠를 보여주면 그 착시가 깨져요. 이것이 서버 렌더 출력이 클라이언트의 초기 렌더 출력과 일치해야 하는 이유예요.

hydration 에러를 일으키는 가장 흔한 원인은:

  • 루트 노드 안의 React가 생성한 HTML 주변의 추가 공백(줄바꿈 같은).
  • 렌더링 로직에서 typeof window !== 'undefined' 같은 검사 사용.
  • 렌더링 로직에서 window.matchMedia 같은 브라우저 전용 API 사용.
  • 서버와 클라이언트에서 다른 데이터 렌더링.

React는 일부 hydration 에러에서 복구하지만, 다른 버그처럼 반드시 수정해야 해요. 최선의 경우에는 속도 저하로 이어지고, 최악의 경우에는 이벤트 핸들러가 잘못된 엘리먼트에 연결될 수 있어요.

💡 부연 설명: Hydration 불일치의 실제 예시:

// 🚩 잘못됨: 서버와 클라이언트가 다른 결과
function App() {
  return <div>{typeof window !== 'undefined' ? '브라우저' : '서버'}</div>;
}
// 서버에서는 "서버" 렌더링 → HTML에 "서버"
// 클라이언트에서는 "브라우저" 렌더링 → 불일치!
// ✅ 올바름: 서버와 클라이언트 모두 같은 결과
function App() {
  const [mounted, setMounted] = useState(false);
  
  useEffect(() => {
    setMounted(true);
  }, []);
  
  return <div>{mounted ? '브라우저' : '로딩 중'}</div>;
}
// 서버에서는 "로딩 중" 렌더링
// 클라이언트 hydration 시에도 "로딩 중" → 일치!
// Effect 실행 후 "브라우저"로 업데이트

전체 문서 hydrate하기

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하려면, hydrateRoot의 첫 번째 인수로 document 전역 변수를 전달하세요:

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

hydrateRoot(document, <App />);

💡 부연 설명: 이건 주로 Next.js나 Remix 같은 풀스택 React 프레임워크에서 사용하는 패턴이에요. 일반적인 경우에는 <div id="root">만 React로 관리하지만, 전체 문서를 React로 관리하면 <head> 태그 안의 메타데이터도 컴포넌트에서 선언적으로 관리할 수 있어요!


피할 수 없는 hydration 불일치 에러 억제하기

단일 엘리먼트의 속성이나 텍스트 콘텐츠가 서버와 클라이언트 사이에서 피할 수 없이 다른 경우(예: 타임스탬프), hydration 불일치 경고를 억제할 수 있어요.

엘리먼트에서 hydration 경고를 억제하려면, suppressHydrationWarning={true}를 추가하세요:

<!-- public/index.html -->
<!--
  <div id="root">...</div> 안의 HTML 콘텐츠는
  react-dom/server에 의해 App에서 생성되었어요.
-->
<div id="root"><h1>Current Date: <!-- -->01/01/2020</h1></div>
// src/index.js
import './styles.css';
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';

hydrateRoot(document.getElementById('root'), <App />);
// src/App.js
export default function App() {
  return (
    <h1 suppressHydrationWarning={true}>
      Current Date: {new Date().toLocaleDateString()}
    </h1>
  );
}

이건 한 단계 깊이에서만 작동하며, 탈출구로 의도된 거예요. 과용하지 마세요. React는 불일치하는 텍스트 콘텐츠를 패치하려고 시도하지 않을 거예요.

💡 부연 설명:

  • 서버에서 렌더링: "Current Date: 01/01/2020" (빌드 시점의 날짜)
  • 클라이언트에서 hydration: "Current Date: 12/25/2024" (현재 날짜)

이런 경우 날짜가 다른 건 당연하므로 suppressHydrationWarning을 사용해요. 하지만 이건 정말 필요한 경우에만 사용해야 해요. 남용하면 실제 버그를 놓칠 수 있거든요!


클라이언트와 서버 콘텐츠가 다른 경우 처리하기

의도적으로 서버와 클라이언트에서 다른 것을 렌더링해야 한다면, 2단계 렌더링을 할 수 있어요. 클라이언트에서 다른 것을 렌더링하는 컴포넌트는 isClient 같은 state 변수를 읽을 수 있고, Effect 안에서 이걸 true로 설정할 수 있어요:

<!-- public/index.html -->
<!--
  <div id="root">...</div> 안의 HTML 콘텐츠는
  react-dom/server에 의해 App에서 생성되었어요.
-->
<div id="root"><h1>Is Server</h1></div>
// src/index.js
import './styles.css';
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';

hydrateRoot(document.getElementById('root'), <App />);
// src/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 직후에 추가 패스가 동기적으로 발생할 거예요.

💡 부연 설명:
1. 서버: isClient가 없음 → "Is Server" 렌더링
2. 초기 hydration: isClient = false → "Is Server" 렌더링 (서버와 일치!)
3. Effect 실행: setIsClient(true) 호출
4. 재렌더링: isClient = true → "Is Client" 렌더링

이 패턴은 hydration 불일치를 피하면서도 클라이언트 전용 콘텐츠를 표시할 수 있게 해줘요.

⚠️ 함정(Pitfall)

이 접근 방식은 컴포넌트가 두 번 렌더링해야 하기 때문에 hydration을 느리게 만들어요. 느린 연결에서의 사용자 경험에 유의하세요. JavaScript 코드가 초기 HTML 렌더보다 훨씬 늦게 로드될 수 있으므로, hydration 직후에 다른 UI를 렌더링하는 것도 사용자에게 불편하게 느껴질 수 있어요.

💡 부연 설명: 가능하면 이 패턴을 피하는 게 좋아요. 대신:

  • CSS media query로 해결할 수 있는 건 CSS로 처리
  • 서버에서도 같은 데이터를 사용할 수 있게 만들기
  • 정말 필요한 경우에만 이 패턴 사용

예를 들어:

// 🚩 두 번 렌더링됨
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
  setIsMobile(window.innerWidth < 768);
}, []);
/* ✅ CSS로 해결 - 렌더링 한 번만! */
.desktop-only { display: block; }
@media (max-width: 767px) {
  .desktop-only { display: none; }
}

Hydrate된 루트 컴포넌트 업데이트하기

루트가 hydration을 완료한 후, root.render를 호출해서 루트 React 컴포넌트를 업데이트할 수 있어요. createRoot와 달리, 초기 콘텐츠가 이미 HTML로 렌더링되었기 때문에 보통 이렇게 할 필요가 없어요.

hydration 후 어느 시점에 root.render를 호출하고, 컴포넌트 트리 구조가 이전에 렌더링된 것과 일치한다면, React는 상태를 보존할 거예요. 입력에 타이핑할 수 있다는 것에 주목하세요. 이는 이 예제에서 매초 반복되는 render 호출의 업데이트가 파괴적이지 않다는 걸 의미해요:

<!-- public/index.html -->
<!--
  <div id="root">...</div> 안의 모든 HTML 콘텐츠는
  react-dom/server로 <App />을 렌더링해서 생성되었어요.
-->
<div id="root"><h1>Hello, world! <!-- -->0</h1><input placeholder="Type something here"/></div>
// src/index.js
import { hydrateRoot } from 'react-dom/client';
import './styles.css';
import App from './App.js';

const root = hydrateRoot(
  document.getElementById('root'),
  <App counter={0} />
);

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" />
    </>
  );
}

hydrate된 루트에서 root.render를 호출하는 건 흔하지 않아요. 보통은 대신 컴포넌트 중 하나 안에서 상태를 업데이트할 거예요.

💡 부연 설명: 이 예제는 매초마다 root.render를 호출하지만, <input>에 입력한 내용은 사라지지 않아요! React가 똑똑하게도 실제로 변경된 부분(counter 숫자)만 업데이트하고 나머지는 그대로 유지하기 때문이에요.

하지만 실제 앱에서는 이렇게 하지 않아요. 대신:

function App() {
  const [counter, setCounter] = useState(0);
  
  useEffect(() => {
    const interval = setInterval(() => {
      setCounter(c => c + 1);
    }, 1000);
    return () => clearInterval(interval);
  }, []);
  
  return <h1>Hello, world! {counter}</h1>;
}

이렇게 컴포넌트 내부에서 상태를 관리하는 게 React의 정석 방법이에요!

프로덕션에서 에러 로깅하기

기본적으로 React는 모든 에러를 콘솔에 로그해요. 자체 에러 보고를 구현하려면, 선택적 에러 핸들러 루트 옵션인 onUncaughtError, onCaughtError, onRecoverableError를 제공할 수 있어요:

import { hydrateRoot } from "react-dom/client";
import App from "./App.js";
import { reportCaughtError } from "./reportError";

const container = document.getElementById("root");
const root = hydrateRoot(container, <App />, {
  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 { hydrateRoot } from "react-dom/client";
import App from "./App.js";
import {
  onCaughtErrorProd,
  onRecoverableErrorProd,
  onUncaughtErrorProd,
} from "./reportError";

const container = document.getElementById("root");
hydrateRoot(container, <App />, {
  // 개발 환경에서는 React의 기본 핸들러를 활용하거나
  // 자체 개발용 오버레이를 구현하기 위해 이 옵션들을 제거하는 걸 잊지 마세요.
  // 여기서는 데모 목적으로만 무조건 지정했어요.
  onCaughtError: onCaughtErrorProd,
  onRecoverableError: onRecoverableErrorProd,
  onUncaughtError: onUncaughtErrorProd,
});

💡 부연 설명:

  • onCaughtError: Error Boundary가 캐치한 에러 (사용자가 계속 앱 사용 가능)
  • onUncaughtError: Error Boundary가 캐치하지 못한 에러 (치명적)
  • onRecoverableError: React가 자동으로 복구한 에러 (예: hydration 불일치)

실제 프로덕션에서는 이런 에러들을 Sentry, Bugsnag 같은 에러 추적 서비스로 보내요:

onUncaughtError: (error, errorInfo) => {
  Sentry.captureException(error, {
    contexts: {
      react: {
        componentStack: errorInfo.componentStack
      }
    }
  });
}

문제 해결(Troubleshooting)

"You passed a second argument to root.render" 에러가 발생해요

흔한 실수는 hydrateRoot의 옵션을 root.render(...)에 전달하는 거예요:

Warning: You passed a second argument to root.render(...) but it only accepts one argument.

수정하려면, 루트 옵션을 root.render(...)가 아니라 hydrateRoot(...)에 전달하세요:

// 🚩 잘못됨: root.render는 하나의 인수만 받아요.
root.render(App, {onUncaughtError});

// ✅ 올바름: 옵션을 createRoot에 전달하세요.
const root = hydrateRoot(container, <App />, {onUncaughtError});

💡 부연 설명:

// 옵션은 루트를 만들 때 전달
const root = hydrateRoot(container, <App />, {
  onUncaughtError: handleError  // ✅ 여기!
});

// render는 렌더링할 컴포넌트만 받음
root.render(<App />);  // ✅ 옵션 없이!

이건 createRoot에서도 마찬가지예요. 옵션은 항상 루트 생성 시에만 전달해요!

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

0개의 댓글