여러분들의 React 앱은 수많은 컴포넌트들이 서로 중첩되면서 점점 형태를 갖춰가고 있을 거예요. 그렇다면 React는 이렇게 복잡해지는 앱의 컴포넌트 구조를 어떻게 추적하고 관리할까요?
React를 비롯한 많은 최신 UI 라이브러리들은 UI를 '트리(Tree, 나무)' 형태로 모델링합니다. 여러분의 앱을 하나의 트리로 상상해 보는 것은 컴포넌트들 사이의 관계를 이해하는 데 아주 유용하죠. 이 개념을 초반에 확실히 잡아두면, 나중에 렌더링 성능 최적화나 상태 관리(State Management) 같은 고급 개념들을 디버깅하고 학습할 때 정말 큰 도움이 될 거예요!
💡 강사의 추가 설명:
프로그래밍에서 '트리(Tree)'는 정말 자주 쓰이는 자료구조예요. 거꾸로 뒤집힌 나무나 회사의 조직도, 또는 가족의 족보를 떠올려 보시면 이해하기 쉬워요. 하나의 뿌리(Root)에서 시작해서 여러 갈래의 가지(Branch)를 치며 잎(Leaf)으로 뻗어나가는 형태를 띠고 있죠.
트리 구조는 항목들 간의 관계를 나타내는 아주 훌륭한 모델입니다. 그래서 UI를 표현할 때 트리 구조를 사용하는 경우가 정말 많아요. 예를 들어 볼까요? 웹 브라우저는 여러분이 작성한 HTML을 모델링하기 위해 DOM(Document Object Model)이라는 트리를 사용하고, CSS를 모델링하기 위해 CSSOM(CSS Object Model)이라는 트리를 사용합니다. 모바일 플랫폼(iOS, Android 등) 역시 화면의 뷰 계층 구조(View Hierarchy)를 표현할 때 트리를 사용한답니다.

React의 UI 트리 생성 과정
React는 여러분이 작성한 컴포넌트들을 바탕으로 자신만의 UI 트리를 만듭니다. 이 예시에서 만들어진 React의 UI 트리는 결과적으로 브라우저의 DOM에 렌더링되는 데 사용되죠.
브라우저나 모바일 플랫폼과 마찬가지로, React 또한 앱 내의 컴포넌트 간 관계를 관리하고 모델링하기 위해 트리 구조를 사용해요. 이 트리들은 React 앱 전체에서 데이터가 어떻게 흘러가는지(Data Flow) 이해하고, 렌더링 성능이나 앱의 용량을 어떻게 최적화할 수 있을지 파악하는 데 아주 유용한 도구가 되어줍니다.
React 컴포넌트가 가진 아주 강력한 특징 중 하나는, 바로 다른 컴포넌트들을 모아서(compose) 새로운 컴포넌트를 만들 수 있다는 거예요. 우리가 컴포넌트를 중첩(nesting)하다 보면, 자연스럽게 부모 컴포넌트와 자식 컴포넌트라는 개념이 생기게 됩니다. 이때 어떤 부모 컴포넌트는 그 자체로 다른 더 큰 컴포넌트의 자식일 수도 있겠죠?
우리가 React 앱을 렌더링할 때, 이러한 컴포넌트들의 관계를 트리 형태로 모델링할 수 있는데요. 이것을 바로 렌더 트리(Render Tree)라고 부릅니다.
여기에 사용자에게 영감을 주는 명언들을 렌더링하는 간단한 React 앱 예시가 있어요. 코드를 먼저 살펴볼까요?
// src/App.js
import FancyText from './FancyText';
import InspirationGenerator from './InspirationGenerator';
import Copyright from './Copyright';
export default function App() {
return (
<>
<FancyText title text="Get Inspired App" />
<InspirationGenerator>
<Copyright year={2004} />
</InspirationGenerator>
</>
);
}
// src/FancyText.js
export default function FancyText({title, text}) {
return title
? <h1 className='fancy title'>{text}</h1>
: <h3 className='fancy cursive'>{text}</h3>
}
// src/InspirationGenerator.js
import * as React from 'react';
import quotes from './quotes';
import FancyText from './FancyText';
export default function InspirationGenerator({children}) {
const [index, setIndex] = React.useState(0);
const quote = quotes[index];
const next = () => setIndex((index + 1) % quotes.length);
return (
<>
<p>Your inspirational quote is:</p>
<FancyText text={quote} />
<button onClick={next}>Inspire me again</button>
{children}
</>
);
}
// src/Copyright.js
export default function Copyright({year}) {
return <p className='small'>©️ {year}</p>;
}
// src/quotes.js
export default [
"Don’t let yesterday take up too much of today.” — Will Rogers",
"Ambition is putting a ladder against the sky.",
"A joy that's shared is a joy made double.",
];
/* css */
.fancy {
font-family: 'Georgia';
}
.title {
color: #007AA3;
text-decoration: underline;
}
.cursive {
font-style: italic;
}
.small {
font-size: 10px;
}

렌더 트리 (Render Tree)
React는 렌더링된 컴포넌트들로 구성된 UI 트리, 즉 렌더 트리를 만듭니다.
방금 보신 예시 앱을 바탕으로 우리는 위와 같은 렌더 트리를 구성할 수 있어요.
이 트리는 여러 개의 '노드(Node)'들로 이루어져 있는데, 각 노드는 하나의 컴포넌트를 나타냅니다. App, FancyText, Copyright 같은 것들이 모두 우리 트리의 노드들이죠.
React 렌더 트리에서 가장 꼭대기에 있는 루트(root) 노드는 앱의 루트 컴포넌트예요. 이 예시에서는 App이 루트 컴포넌트이고, React가 가장 먼저 렌더링하는 출발점이 됩니다. 트리에 있는 각각의 화살표는 부모 컴포넌트에서 자식 컴포넌트를 향해 뻗어나가고 있어요.
💡 강사의 추가 설명:
트리의 루트(Root)는 모든 가지가 시작되는 뿌리입니다. React에서는 보통App.js파일이 이 역할을 담당하죠. 컴포넌트 구조를 파악할 때 항상 이App에서부터 아래로 뻗어나가며 추적하는 습관을 들이는 것이 좋습니다!
위의 렌더 트리를 자세히 보셨다면, 각 컴포넌트가 화면에 그리는 실제 HTML 태그(<h1>, <p>, <button> 등)에 대한 언급이 전혀 없다는 걸 눈치채셨을 거예요. 그 이유는 렌더 트리가 오직 React 컴포넌트들로만 구성되기 때문이랍니다.
UI 프레임워크인 React는 특정 플랫폼에 종속되지 않습니다(platform agnostic). react.dev 공식 사이트에서는 기본 UI 요소로 HTML 마크업을 사용하는 '웹' 환경에서의 예시들을 주로 보여주지만, React 앱은 모바일이나 데스크톱 플랫폼에도 얼마든지 렌더링될 수 있어요! 만약 iOS라면 UIView를, Windows 데스크톱 환경이라면 FrameworkElement 같은 완전히 다른 UI 요소들을 사용하게 될 겁니다.
이러한 플랫폼별 기본 UI 요소들은 React의 핵심 구성요소가 아니에요. React의 렌더 트리는 여러분의 앱이 어떤 플랫폼(웹, 모바일, 데스크톱 등)에 렌더링되든 상관없이, React 앱 내부의 구조를 독립적으로 파악할 수 있는 통찰력을 제공해 줍니다.
렌더 트리는 React 애플리케이션의 단일 렌더링 패스(single render pass, 한 번의 렌더링 과정)를 나타냅니다. 조건부 렌더링(Conditional Rendering)을 사용하면, 전달된 데이터나 조건에 따라 부모 컴포넌트가 렌더링할 때마다 서로 다른 자식 컴포넌트를 그릴 수도 있어요.
앱을 조금 수정해서 조건에 따라 명언 글귀를 보여주거나, 혹은 색상을 보여주도록 업데이트해 보겠습니다.
// src/App.js
import FancyText from './FancyText';
import InspirationGenerator from './InspirationGenerator';
import Copyright from './Copyright';
export default function App() {
return (
<>
<FancyText title text="Get Inspired App" />
<InspirationGenerator>
<Copyright year={2004} />
</InspirationGenerator>
</>
);
}
// src/FancyText.js
export default function FancyText({title, text}) {
return title
? <h1 className='fancy title'>{text}</h1>
: <h3 className='fancy cursive'>{text}</h3>
}
// src/Color.js
export default function Color({value}) {
return <div className="colorbox" style={{backgroundColor: value}} />
}
// src/InspirationGenerator.js
import * as React from 'react';
import inspirations from './inspirations';
import FancyText from './FancyText';
import Color from './Color';
export default function InspirationGenerator({children}) {
const [index, setIndex] = React.useState(0);
const inspiration = inspirations[index];
const next = () => setIndex((index + 1) % inspirations.length);
return (
<>
<p>Your inspirational {inspiration.type} is:</p>
{inspiration.type === 'quote'
? <FancyText text={inspiration.value} />
: <Color value={inspiration.value} />}
<button onClick={next}>Inspire me again</button>
{children}
</>
);
}
// src/Copyright.js
export default function Copyright({year}) {
return <p className='small'>©️ {year}</p>;
}
// src/inspirations.js
export default [
{type: 'quote', value: "Don’t let yesterday take up too much of today.” — Will Rogers"},
{type: 'color', value: "#B73636"},
{type: 'quote', value: "Ambition is putting a ladder against the sky."},
{type: 'color', value: "#256266"},
{type: 'quote', value: "A joy that's shared is a joy made double."},
{type: 'color', value: "#F9F2B4"},
];
/* css */
.fancy {
font-family: 'Georgia';
}
.title {
color: #007AA3;
text-decoration: underline;
}
.cursive {
font-style: italic;
}
.small {
font-size: 10px;
}
.colorbox {
height: 100px;
width: 100px;
margin: 8px;
}

조건부 렌더 트리
조건부 렌더링이 있으면, 렌더링이 일어날 때마다 렌더 트리가 그리는 컴포넌트의 구성이 달라질 수 있습니다.
이 예시에서는 inspiration.type의 값에 따라 <FancyText> 컴포넌트를 렌더링할 수도 있고, <Color> 컴포넌트를 렌더링할 수도 있습니다. 즉, 렌더링이 실행될 때마다(render pass) 렌더 트리의 모양이 매번 바뀔 수 있다는 뜻이죠.
비록 렌더링 패스마다 렌더 트리가 조금씩 달라질 수는 있지만, 이 트리 구조는 전반적으로 React 앱에서 무엇이 최상위(top-level) 컴포넌트이고 무엇이 리프(leaf) 컴포넌트인지 파악하는 데 아주 큰 도움이 됩니다.
💡 강사의 추가 설명:
- 최상위 컴포넌트: 루트(Root)에 가장 가까운 컴포넌트들이에요. 보통 복잡한 상태를 가지고 있고, 이 친구들이 재렌더링되면 그 아래에 달린 모든 자식 컴포넌트들도 영향을 받기 때문에 전체적인 렌더링 성능에 아주 큰 영향을 미칩니다.
- 리프 컴포넌트: 나뭇잎처럼 트리의 가장 맨 끝부분에 위치해서 자식 컴포넌트를 가지지 않는 컴포넌트예요. 주로 UI를 그리는 역할에 집중하며 화면이 바뀔 때 아주 자주 렌더링되는 경향이 있습니다.
이런 컴포넌트의 범주(카테고리)를 식별하는 것은 여러분의 앱에서 데이터가 어떻게 흐르고 성능 최적화를 어디서 해야 할지 이해하는 데 굉장히 유용하답니다!
React 앱에서 트리 형태로 모델링할 수 있는 또 다른 관계가 있는데요, 바로 앱의 '모듈 의존성(module dependencies)'입니다. 우리가 컴포넌트와 비즈니스 로직들을 깔끔하게 별도의 파일로 분리(break up)할 때, 우리는 컴포넌트나 함수, 상수 등을 내보낼 수 있는 JS 모듈(JS Modules)을 만들게 됩니다.
이 모듈 의존성 트리에서 각 노드는 하나의 '모듈(파일)'을 의미하고, 각 가지(branch)는 해당 모듈 안에서 작성된 import 문을 나타내요.
앞에서 만든 명언 앱(Inspirations app)을 가지고 모듈 의존성 트리(줄여서 의존성 트리)를 만들어 볼 수 있습니다.

명언 앱의 모듈 의존성 트리
파일 간의import관계를 나타냅니다. 누가 누구를 필요로(의존) 하는지 보여줍니다.
이 트리의 루트 노드는 가장 근원이 되는 루트 모듈, 즉 진입점(entrypoint) 파일입니다. 보통 루트 컴포넌트(App 등)를 포함하고 있는 최상단 파일이 이 역할을 맡게 되죠.
동일한 앱의 '렌더 트리'와 이 '의존성 트리'를 비교해 보면 구조가 꽤 비슷해 보이지만, 몇 가지 눈에 띄는 차이점이 있습니다:
inspirations.js 같은 파일)도 이 트리에는 포함됩니다. 반면 렌더 트리는 오직 '컴포넌트'만 캡슐화해서 보여주죠.Copyright.js는 App.js 아래에 나타납니다. 하지만 아까 본 렌더 트리에서는 Copyright 컴포넌트가 InspirationGenerator의 자식으로 나타났었죠? 그 이유는 InspirationGenerator가 JSX를 children prop으로 받아들이기 때문이에요. 그래서 화면에 그려질 때(렌더링)는 자식 컴포넌트로 들어가지만, 파일 자체를 직접 import 한 것은 App.js이기 때문에 의존성 트리의 구조가 이렇게 다르게 나타나는 거랍니다. (이 부분 헷갈리기 쉬우니 꼭 기억해두세요!)의존성 트리는 여러분의 React 앱을 실행하는 데 도대체 "어떤 모듈(파일)들이 필요한지" 파악하는 데 매우 유용합니다. 프로덕션(실제 서비스)용 React 앱을 구축(build)할 때는, 보통 클라이언트의 브라우저에 전달해야 할 모든 필수 JavaScript 파일들을 하나로 묶어주는 빌드 단계를 거치게 돼요. 이 역할을 담당하는 도구를 번들러(bundler)라고 부르는데요. 번들러는 바로 이 의존성 트리를 사용해서 어떤 모듈들을 최종 결과물에 포함시켜야(bundle) 할지 결정하게 됩니다.
앱이 점점 커지면 번들(묶인 결과물)의 크기도 덩달아 커지는 경우가 많아요. 번들 크기가 너무 커지면 클라이언트가 이를 다운로드하고 실행하는 데 시간과 비용이 많이 듭니다. 결국 화면에 UI가 그려지는 시간까지 지연시키게 되죠. 이럴 때 여러분의 앱이 가진 의존성 트리를 전체적으로 파악하고 있으면, 이런 성능 저하 문제들을 디버깅하고 불필요한 코드를 덜어내는 데 아주 큰 힌트를 얻을 수 있습니다.
import 관계)을 보여줍니다.