Dev Thinking
32완료

컴포넌트 세계의 구조 - UI 트리 이해, 렌더 및 의존성 트리

2025-08-09
8분 읽기

리액트 공식문서 기반의 리액트 입문기

들어가며

바닐라 자바스크립트로 웹 개발을 해오신 우리에게 UI를 구성하는 방식은 대개 명확했습니다. HTML로 구조를 만들고, 자바스크립트로 DOM(Document Object Model) 트리를 직접 조작하여 동적인 상호작용을 구현하는 것이 익숙하죠. 하지만 리액트는 이러한 UI 구성 방식에 새로운 관점을 제시합니다. 단순히 DOM을 조작하는 것을 넘어, UI를 컴포넌트라는 독립적인 블록들의 계층 구조로 바라보고, 이들이 어떻게 상호작용하며 렌더링되는지를 이해하는 것이 중요해집니다.

이번 편에서는 리액트의 핵심 개념 중 하나인 'UI 트리'를 깊이 있게 탐색하고, 이와 함께 '렌더 트리'와 '의존성 트리'가 UI 구성에서 어떤 역할을 하는지 알아보려고 합니다. 우리가 익숙했던 DOM 트리와 리액트의 트리 개념이 어떻게 다른지 비교해보면서, 리액트가 UI를 더 효율적이고 예측 가능하게 관리하는 방식을 함께 이해해 나가는 시간이 되었으면 합니다.

리액트 고유 기능 - UI 트리, 렌더 트리, 의존성 트리 이해하기

리액트에서 UI를 구성하는 방식은 우리가 과거에 DOM을 직접 조작하던 방식과는 근본적으로 다릅니다. 리액트는 UI를 여러 컴포넌트의 조합으로 보며, 이 컴포넌트들이 부모-자식 관계를 이루며 하나의 계층 구조를 형성하는데, 이것이 바로 **UI 트리(UI Tree)**입니다. 이 UI 트리는 우리가 작성한 컴포넌트 구조를 나타내며, 화면에 무엇이 보여질지 결정하는 설계도와 같습니다.

하지만 UI 트리가 전부는 아닙니다. 리액트 내부에서는 이 UI 트리를 기반으로 두 가지 중요한 트리가 더 작동합니다.

렌더 트리(Render Tree)

렌더 트리는 UI 트리의 컴포넌트들이 실제로 렌더링될 때의 순서와 관계를 나타냅니다. 리액트는 상태(State)나 Props가 변경되면 해당 컴포넌트를 다시 렌더링해야 할지 결정하고, 이때 렌더 트리를 통해 어떤 컴포넌트들이 다시 렌더링될지 파악합니다. 이는 DOM 트리와 직접적으로 연결되지는 않지만, 최종적으로 화면에 그려질 요소들의 '가상의' 계층 구조를 나타낸다고 볼 수 있습니다. 리액트는 렌더 트리를 활용하여 실제 DOM 조작을 최소화하고 효율적인 업데이트를 수행합니다.

의존성 트리(Dependency Tree)

의존성 트리는 컴포넌트들이 서로 어떤 데이터를 주고받으며 의존하는지를 보여주는 개념입니다. 한 컴포넌트의 데이터(State, Props)가 변경되면, 해당 컴포넌트뿐만 아니라 그 데이터를 사용하는 다른 컴포넌트들(의존 관계에 있는 컴포넌트들)에게도 영향을 미치게 됩니다. 리액트는 이 의존성 트리를 통해 어떤 컴포넌트들이 변경된 데이터에 의해 재렌더링되어야 하는지를 추적하고 관리합니다. 즉, 데이터의 흐름과 파급 효과를 이해하는 데 중요한 역할을 합니다.

이 세 가지 트리 개념은 리액트 애플리케이션의 성능 최적화와 상태 관리 전략을 이해하는 데 필수적인 요소입니다. 자바스크립트에서는 개발자가 이 모든 것을 수동으로 관리해야 했지만, 리액트는 이러한 트리 구조를 내부적으로 관리함으로써 개발자가 UI의 선언적인 정의에 더 집중할 수 있도록 돕습니다. 이제 리액트가 제공하는 이러한 추상화가 바닐라 자바스크립트와 어떻게 다른지 살펴보겠습니다.

자바스크립트로 동일한 목록 UI 만들기

우리에게 익숙한 바닐라 자바스크립트 환경에서 UI는 주로 HTML 문서의 DOM 트리로 표현됩니다. 우리는 document.createElement, appendChild, querySelector 등의 DOM API를 직접 사용하여 요소를 생성하고, 계층 구조를 만들고, 내용을 업데이트했습니다. 예를 들어, 간단한 목록을 생성하는 경우를 생각해 봅시다.

<!DOCTYPE html>
<html>
  <head>
    <title>Vanilla JavaScript UI Tree Example</title>
  </head>
  <body>
    <div id="root"></div>
 
    <script>
      (function () {
        const rootElement = document.getElementById("root");
 
        function createListItem(text) {
          const li = document.createElement("li");
          li.textContent = text;
          return li;
        }
 
        function createList(items) {
          const ul = document.createElement("ul");
          items.forEach((itemText) => {
            ul.appendChild(createListItem(itemText));
          });
          return ul;
        }
 
        function renderApp() {
          const appDiv = document.createElement("div");
          appDiv.className = "app-container";
 
          const title = document.createElement("h1");
          title.textContent = "My Shopping List";
          appDiv.appendChild(title);
 
          const shoppingItems = ["Apple", "Banana", "Cherry"];
          const shoppingList = createList(shoppingItems);
          appDiv.appendChild(shoppingList);
 
          rootElement.innerHTML = ""; // 기존 내용 지우기
          rootElement.appendChild(appDiv);
        }
 
        renderApp();
      })();
    </script>
  </body>
</html>

위 자바스크립트 코드는 root 엘리먼트 안에 제목과 목록을 포함하는 UI를 동적으로 생성합니다. 각 UI 요소는 DOM API를 통해 직접 생성되고 부모 요소에 추가되는 방식으로 트리가 구성됩니다. 필요한 경우 기존 내용을 innerHTML = ''로 비우고 새로운 UI를 다시 그리는 과정이 필요합니다.

자바스크립트 방식의 특징

  1. 명령형 직접 DOM 조작: 개발자가 document.createElement, appendChild와 같은 DOM API를 사용하여 UI 요소의 생성, 변경, 삭제 과정을 직접 명령하고 실제 DOM에 접근합니다. UI의 상태가 변경될 때마다 개발자가 직접 DOM을 찾아 업데이트해야 하며, 이는 실제 브라우저의 렌더링 엔진에 직접적인 부하를 줄 수 있습니다.
  2. 수동적인 UI 업데이트와 오류 가능성: 데이터 변경이 발생했을 때, 변경된 데이터에 맞춰 UI를 어떻게 업데이트할지 모든 과정을 개발자가 수동으로 처리해야 합니다. 이 과정에서 개발자의 실수가 발생하기 쉽고, 복잡한 UI에서는 UI 불일치나 예상치 못한 버그 발생 가능성이 높아집니다.
  3. 전역 스코프 관리의 복잡성: 예제에서 IIFE(즉시 실행 함수)를 사용해 스코프를 관리했지만, 복잡한 애플리케이션에서는 전역 변수나 전역 객체에 대한 의존성이 커지면서 상태 관리가 어려워지고 예기치 않은 사이드 이펙트가 발생할 수 있습니다.
  4. 성능 최적화의 부담과 디버깅의 어려움: UI 업데이트가 빈번하게 일어날 때, 개발자가 직접 최소한의 DOM 조작만 일어나도록 로직을 최적화해야 합니다. 그렇지 않으면 불필요한 리플로우(reflow)나 리페인트(repaint)가 발생하여 성능 저하로 이어질 수 있으며, 변경 사항이 많을수록 문제 발생 지점을 찾고 수정하는 디버깅 과정이 복잡해집니다.
  5. 재사용성 및 모듈화의 한계: UI 로직과 DOM 조작 코드가 밀접하게 얽혀 있어 UI 컴포넌트를 재사용하기 어렵습니다. 또한, UI 요소를 독립적인 모듈로 분리하기보다는 전체 페이지의 관점에서 DOM을 조작하는 경우가 많아 코드의 응집도가 낮아지고 확장성이 떨어집니다.

이제 리액트 버전으로 넘어가 보겠습니다. 리액트는 이러한 문제를 어떻게 다른 방식으로 해결하는지 살펴보겠습니다.

리액트로 동일한 목록 UI 만들기

리액트에서는 UI를 컴포넌트 기반으로 선언적으로 정의합니다. 우리가 위에서 자바스크립트로 만들었던 쇼핑 목록 UI를 리액트 컴포넌트로 재구성해보겠습니다. 리액트는 우리가 정의한 컴포넌트 트리를 기반으로 '가상 DOM(Virtual DOM)'을 생성하고, 효율적인 방식으로 실제 DOM을 업데이트합니다. 개발자는 실제 DOM 조작 대신 데이터(State 또는 Props)만 관리하면 됩니다.

JSX 버전

import ReactDOM from 'react-dom/client';
 
function ShoppingListItem({ item }) {
  return <li>{item}</li>;
}
 
function ShoppingList({ items }) {
  return (
    <ul>
      {items.map(item => (
        <ShoppingListItem key={item} item={item} />
      ))}
    </ul>
  );
}
 
function App() {
  const shoppingItems = ['Apple', 'Banana', 'Cherry'];
 
  return (
    <div className="app-container">
      <h1>My Shopping List</h1>
      <ShoppingList items={shoppingItems} />
    </div>
  );
}
 
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

리액트 코드는 여러 개의 작은 컴포넌트(ShoppingListItem, ShoppingList, App)로 UI를 구성합니다. 각 컴포넌트는 자신이 렌더링할 UI 조각을 선언적으로 정의하고, props를 통해 데이터를 전달받습니다. ReactDOM.createRoot().render()를 호출함으로써 리액트는 App 컴포넌트와 그 자식 컴포넌트들을 DOM에 렌더링하고 관리합니다.

잠깐 살펴보자면, 이 예시에서 데이터는 App에서 ShoppingList로, 다시 ShoppingListItem으로 단방향으로 흐릅니다. 상태가 바뀌면 부모에서 자식으로 필요한 부분만 다시 그려지는 렌더 트리가 평가되고, 어떤 컴포넌트가 어떤 데이터에 의존하는지(의존성 트리 관점)가 자연스럽게 드러납니다.

리액트 방식의 특징 (UI 트리 이해)

  • 1. 선언형 UI 정의와 개발자의 역할 변화: 리액트에서 개발자는 UI가 특정 상태일 때 '어떻게' 보여야 할지 **선언적(Declarative)**으로 기술합니다. 이는 DOM 조작의 세부적인 '방법'(How)을 리액트에게 위임하고, '무엇'(What)을 보여줄지에만 집중할 수 있게 합니다. 이는 UI 트리, 렌더 트리, 의존성 트리와 같은 추상화된 개념들을 통해 리액트는 내부적으로 효율적인 DOM 업데이트 방식을 결정하고 실행하며, 개발자는 복잡한 DOM 조작의 부담을 덜고 애플리케이션의 핵심 로직과 UI의 논리적 구조에 더 집중할 수 있게 됩니다. 이는 개발 생산성과 코드의 가독성을 크게 향상시키는 리액트의 근본적인 철학입니다.
  • 2. 컴포넌트 기반 구조와 모듈화 및 재사용성: 리액트의 UI는 재사용 가능한 독립적인 컴포넌트 단위로 분리되어 구성됩니다. 각 컴포넌트는 자체적인 로직과 렌더링을 담당하며, 부모-자식 관계를 통해 계층적인 UI 트리를 형성합니다. 이러한 컴포넌트 기반 접근은 UI를 기능별로 모듈화하고, props를 통해 데이터를 전달받아 다양한 상황에서 재사용할 수 있게 합니다. 이는 대규모 애플리케이션에서 코드 베이스의 복잡성을 효과적으로 관리하고, 특정 기능의 변경이 다른 부분에 미치는 영향을 최소화하여 유지보수성을 크게 향상시킵니다.
  • 3. 데이터 기반 UI 업데이트와 자동 동기화: propsstate의 변경이 감지되면 리액트는 자동으로 관련 컴포넌트를 효율적으로 재렌더링하여 UI를 최신 상태로 동기화합니다. 이는 자바스크립트에서 개발자가 직접 DOM을 찾아 업데이트하거나 innerHTMLUI 전체를 다시 그려야 했던 수동적인 방식과 대조됩니다. 개발자는 DOM 조작의 번거로움 없이 데이터의 변화만 관리하면, 리액트가 렌더 트리의존성 트리를 활용하여 UI 업데이트의 '방법'을 자동으로 처리하므로, 개발자는 '어떤 데이터일 때 어떤 UI를 보여줄지'에만 집중할 수 있습니다.
  • 4. Virtual DOM을 통한 효율적인 DOM 관리: 리액트는 실제 DOM을 직접 조작하는 대신, **Virtual DOM(가상 DOM)**이라는 메모리상의 가벼운 UI 복사본을 사용하여 변경 사항을 효율적으로 비교하고 최소한의 DOM 업데이트만을 수행합니다. UI 트리의 변화가 발생하면 리액트는 새로운 Virtual DOM 트리를 생성하고 이전 Virtual DOM 트리와 비교(Diffing 알고리즘)하여 실제 DOM에서 변경이 필요한 최소한의 부분만을 찾아냅니다. 이 과정은 브라우저의 DOM 조작 비용을 최소화하여 애플리케이션의 렌더링 성능을 최적화하는 핵심 메커니즘이며, 개발자가 DOM 성능 최적화에 대한 깊은 지식 없이도 고성능 UI를 구축할 수 있게 돕습니다.
  • 5. 단방향 데이터 흐름과 예측 가능한 상태 변화: 리액트의 데이터 흐름은 항상 부모 컴포넌트에서 자식 컴포넌트로 props를 통해 단방향으로 흐릅니다. 자식 컴포넌트는 props를 직접 변경할 수 없으며, 변경이 필요할 경우 상위 컴포넌트에 콜백 함수를 통해 변경을 요청합니다. 이러한 엄격한 단방향 데이터 흐름은 UI 업데이트의 예측 가능성을 높이고, 데이터의 출처와 변경의 파급 효과를 명확하게 하여 복잡한 애플리케이션의 디버깅을 용이하게 합니다. 이는 의존성 트리의 개념과 맞닿아 있으며, 데이터 흐름을 추적하기 쉽게 만들어 애플리케이션의 안정성을 크게 향상시킵니다.

자바스크립트 vs 리액트 차이

구분자바스크립트 (DOM 직접 조작)리액트 (컴포넌트 기반)
UI 구성HTML 문서의 DOM 트리를 직접 생성/조작컴포넌트 트리를 선언적으로 정의
UI 업데이트개발자가 직접 DOM API 호출 (수동)State/Props 변경 시 리액트가 자동으로 처리 (선언적)
데이터 흐름양방향 또는 복잡한 전역 상태 관리Props를 통한 단방향 데이터 흐름
성능 최적화개발자의 수동 최적화 부담가상 DOM을 통한 효율적인 업데이트
패러다임명령형 (How)선언형 (What)

요약

이번 편에서는 UI를 구성하는 방식에 대한 우리의 관점을 바닐라 자바스크립트와 리액트 각각의 맥락에서 탐색해 보았습니다. 특히 리액트가 UI를 컴포넌트라는 독립적인 블록들의 계층 구조로 바라보고, 이를 UI 트리, 렌더 트리, 의존성 트리라는 개념으로 확장하여 관리하는 방식에 대해 깊이 있게 이해하는 시간이었기를 바랍니다.

  • UI 트리: 컴포넌트 계층 구조, 화면 UI의 설계도
  • 렌더 트리: UI 컴포넌트의 실제 렌더링 순서와 관계
  • 의존성 트리: 컴포넌트 간 데이터 의존 관계 추적
  • 자바스크립트 특징: 명령형 직접 DOM 조작, 수동적 UI 업데이트와 오류 가능성, 전역 스코프 관리의 복잡성, 성능 최적화 부담과 디버깅의 어려움, 재사용성 및 모듈화의 한계
  • 리액트 특징: 선언형 UI 정의, 컴포넌트 기반 구조, 데이터 기반 자동 UI 업데이트, Virtual DOM 활용, 단방향 데이터 흐름
  • 핵심 비교: 명령형(JS) vs 선언형(React) 패러다임 차이

다음 편(2-8편)에서는 컴포넌트를 파일로 분리하고 import/export를 통해 UI 트리를 모듈로 구성하는 방법을 살펴보겠습니다.

참고문서

– [UI 트리 이해하기 (Understanding Your UI as a Tree)]