captureOwnerStack

김동현·2026년 3월 17일

captureOwnerStack

소개

captureOwnerStack은 개발 모드에서 현재 Owner Stack을 읽어서 사용 가능한 경우 문자열로 반환해줘요.

const stack = captureOwnerStack();

Owner Stack이 뭔지 간단히 설명하자면, React 컴포넌트들이 어떻게 "소유" 관계로 연결되어 있는지를 보여주는 스택이에요. 디버깅할 때 어떤 컴포넌트가 다른 컴포넌트를 "생성"했는지 추적하는 데 유용해요.


목차


레퍼런스

captureOwnerStack()

현재 Owner Stack을 가져오려면 captureOwnerStack을 호출하세요.

// 예시
import * as React from 'react';

function Component() {
  if (process.env.NODE_ENV !== 'production') {
    const ownerStack = React.captureOwnerStack();
    console.log(ownerStack);
  }
}

매개변수

captureOwnerStack은 어떤 매개변수도 받지 않아요.

반환값

captureOwnerStackstring | null을 반환해요.

Owner Stack은 다음 상황에서 사용할 수 있어요:

  • 컴포넌트 렌더링 중
  • Effects 안에서 (예: useEffect)
  • React 이벤트 핸들러 안에서 (예: <button onClick={...} />)
  • React 에러 핸들러 안에서 (React Root 옵션onCaughtError, onRecoverableError, onUncaughtError)

Owner Stack을 사용할 수 없으면 null이 반환돼요 (문제 해결: Owner Stack이 null인 경우를 참고하세요).

주의사항

  • Owner Stack은 개발 모드에서만 사용할 수 있어요. captureOwnerStack은 개발 모드 바깥에서는 항상 null을 반환해요.
**심층 탐구: Owner Stack vs Component Stack**

Owner Stack은 React 에러 핸들러에서 사용할 수 있는 Component Stack과는 달라요. 예를 들어 onUncaughtErrorerrorInfo.componentStack과는 다르다는 거죠.

예를 들어, 다음 코드를 살펴볼게요:

// src/App.js
import {Suspense} from 'react';

function SubComponent({disabled}) {
  if (disabled) {
    throw new Error('disabled');
  }
}

export function Component({label}) {
  return (
    <fieldset>
      <legend>{label}</legend>
      <SubComponent key={label} disabled={label === 'disabled'} />
    </fieldset>
  );
}

function Navigation() {
  return null;
}

export default function App({children}) {
  return (
    <Suspense fallback="loading...">
      <main>
        <Navigation />
        {children}
      </main>
    </Suspense>
  );
}
// src/index.js
import {captureOwnerStack} from 'react';
import {createRoot} from 'react-dom/client';
import App, {Component} from './App.js';
import './styles.css';

createRoot(document.createElement('div'), {
  onUncaughtError: (error, errorInfo) => {
    // 스택은 UI에 직접 표시하는 대신 로그로 출력해요.
    // 브라우저가 로그된 스택에 소스맵을 적용하기 때문이에요.
    // 소스맵핑은 이 페이지에 표시된 가짜 콘솔이 아닌 
    // 실제 브라우저 콘솔에서만 적용된다는 점에 주의하세요.
    // 실제 콘솔에서 소스맵핑된 스택을 보려면 "fork"를 누르세요.
    console.log(errorInfo.componentStack);
    console.log(captureOwnerStack());
  },
}).render(
  <App>
    <Component label="disabled" />
  </App>
);
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <p>Check the console output.</p>
  </body>
</html>

SubComponent가 에러를 던질 거예요.
그 에러의 Component Stack은 다음과 같을 거예요:

at SubComponent
at fieldset
at Component
at main
at React.Suspense
at App

하지만 Owner Stack은 다음처럼만 읽힐 거예요:

at Component

App이나 DOM 컴포넌트들(예: fieldset)은 이 Stack에서 Owner로 간주되지 않아요. 왜냐하면 SubComponent를 포함하는 노드를 "생성"하는 데 기여하지 않았거든요. App과 DOM 컴포넌트들은 단지 노드를 전달만 했어요. AppComponent<SubComponent />를 통해 SubComponent를 포함하는 노드를 생성한 것과 달리, 그냥 children 노드를 렌더링한 것뿐이에요.

Navigation이나 legend<SubComponent />를 포함하는 노드의 형제(sibling)일 뿐이라서 스택에 전혀 나타나지 않아요.

SubComponent는 이미 콜스택의 일부이기 때문에 생략돼요.

사용법

커스텀 에러 오버레이 개선하기

// 예시
import { captureOwnerStack } from "react";
import { instrumentedConsoleError } from "./errorOverlay";

const originalConsoleError = console.error;
console.error = function patchedConsoleError(...args) {
  originalConsoleError.apply(console, args);
  const ownerStack = captureOwnerStack();
  onConsoleError({
    // 실제 애플리케이션에서는 console.error가 
    // 여러 인자로 호출될 수 있다는 점을 고려해야 해요.
    consoleMessage: args[0],
    ownerStack,
  });
};

console.error 호출을 가로채서 에러 오버레이에 강조 표시하고 싶다면, captureOwnerStack을 호출해서 Owner Stack을 포함시킬 수 있어요.

/* src/styles.css */
* {
  box-sizing: border-box;
}

body {
  font-family: sans-serif;
  margin: 20px;
  padding: 0;
}

h1 {
  margin-top: 0;
  font-size: 22px;
}

h2 {
  margin-top: 0;
  font-size: 20px;
}

code {
  font-size: 1.2em;
}

ul {
  padding-inline-start: 20px;
}

label, button { display: block; margin-bottom: 20px; }
html, body { min-height: 300px; }

#error-dialog {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  background-color: white;
  padding: 15px;
  opacity: 0.9;
  text-wrap: wrap;
  overflow: scroll;
}

.text-red {
  color: red;
}

.-mb-20 {
  margin-bottom: -20px;
}

.mb-0 {
  margin-bottom: 0;
}

.mb-10 {
  margin-bottom: 10px;
}

pre {
  text-wrap: wrap;
}

pre.nowrap {
  text-wrap: nowrap;
}

.hidden {
 display: none;  
}
<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head>
  <title>My app</title>
</head>
<body>
<!--
  React 앱에서 에러가 발생하면 크래시될 수 있으므로
  에러 다이얼로그를 순수 HTML로 작성해요.
-->
<div id="error-dialog" class="hidden">
  <h1 id="error-title" class="text-red">Error</h1>
  <p>
    <pre id="error-body"></pre>
  </p>
  <h2 class="-mb-20">Owner Stack:</h4>
  <pre id="error-owner-stack" class="nowrap"></pre>
  <button
    id="error-close"
    class="mb-10"
    onclick="document.getElementById('error-dialog').classList.add('hidden')"
  >
    Close
  </button>
</div>
<!-- 이것이 DOM 노드예요 -->
<div id="root"></div>
</body>
</html>
// src/errorOverlay.js
export function onConsoleError({ consoleMessage, ownerStack }) {
  const errorDialog = document.getElementById("error-dialog");
  const errorBody = document.getElementById("error-body");
  const errorOwnerStack = document.getElementById("error-owner-stack");

  // console.error() 메시지 표시
  errorBody.innerText = consoleMessage;

  // Owner Stack 표시
  errorOwnerStack.innerText = ownerStack;

  // 다이얼로그 보이기
  errorDialog.classList.remove("hidden");
}
// src/index.js
import { captureOwnerStack } from "react";
import { createRoot } from "react-dom/client";
import App from './App';
import { onConsoleError } from "./errorOverlay";
import './styles.css';

const originalConsoleError = console.error;
console.error = function patchedConsoleError(...args) {
  originalConsoleError.apply(console, args);
  const ownerStack = captureOwnerStack();
  onConsoleError({
    // 실제 애플리케이션에서는 console.error가 
    // 여러 인자로 호출될 수 있다는 점을 고려해야 해요.
    consoleMessage: args[0],
    ownerStack,
  });
};

const container = document.getElementById("root");
createRoot(container).render(<App />);
// src/App.js
function Component() {
  return <button onClick={() => console.error('Some console error')}>Trigger console.error()</button>;
}

export default function App() {
  return <Component />;
}

문제 해결

Owner Stack이 null인 경우

captureOwnerStack 호출이 React가 제어하는 함수 바깥에서 발생했어요. 예를 들어 setTimeout 콜백 안에서, fetch 호출 후, 또는 커스텀 DOM 이벤트 핸들러 안에서요. 렌더링 중, Effects 안, React 이벤트 핸들러 안, 그리고 React 에러 핸들러 안(예: hydrateRoot#options.onCaughtError)에서는 Owner Stack을 사용할 수 있어야 해요.

아래 예시에서 버튼을 클릭하면 빈 Owner Stack이 로그될 거예요. 왜냐하면 captureOwnerStack이 커스텀 DOM 이벤트 핸들러 안에서 호출되었기 때문이에요. Owner Stack은 더 일찍 캡처해야 해요. 예를 들어 captureOwnerStack 호출을 Effect 본문 안으로 옮기면 돼요.

// 예시
import {captureOwnerStack, useEffect} from 'react';

export default function App() {
  useEffect(() => {
    // 여기서 `captureOwnerStack`을 호출해야 해요.
    function handleEvent() {
      // 커스텀 DOM 이벤트 핸들러에서 호출하면 너무 늦어요.
      // 이 시점에서 Owner Stack은 `null`이 될 거예요.
      console.log('Owner Stack: ', captureOwnerStack());
    }

    document.addEventListener('click', handleEvent);

    return () => {
      document.removeEventListener('click', handleEvent);
    }
  })

  return <button>클릭해서 커스텀 DOM 이벤트 핸들러에서 Owner Stack을 사용할 수 없다는 걸 확인하세요</button>;
}

captureOwnerStack을 사용할 수 없어요

captureOwnerStack은 개발 빌드에서만 내보내져요. 프로덕션 빌드에서는 undefined가 될 거예요. 프로덕션과 개발 모두에 번들되는 파일에서 captureOwnerStack을 사용한다면, namespace import에서 조건부로 접근해야 해요.

// 프로덕션과 개발 모두에 번들되는 파일에서는 `captureOwnerStack`의 named import를 사용하지 마세요.
import {captureOwnerStack} from 'react';
// 대신 namespace import를 사용하고 `captureOwnerStack`에 조건부로 접근하세요.
import * as React from 'react';

if (process.env.NODE_ENV !== 'production') {
  const ownerStack = React.captureOwnerStack();
  console.log('Owner Stack', ownerStack);
}

이렇게 하면 프로덕션 빌드에서 에러가 발생하지 않고, 개발 모드에서만 Owner Stack을 사용할 수 있어요.


사이트맵

모든 문서 페이지 개요

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

0개의 댓글