2. Deep Dive into React's Core Elements

리액트는 JSX를 객체화하고 파이버(Fiber) 아키텍처 기반의 가상 DOM 재조정을 통해, UI 업데이트를 효율적으로 스케줄링하고 렌더링 성능을 최적화하는 라이브러리


1. JSX란 무엇인가?

JSX는 리액트 전용 문법이 아니라, ECMAScript의 문법 확장(Syntax Extension) 입니다. HTML과 유사한 형태를 띠지만 자바스크립트 표준은 아니며, 브라우저나 JS 엔진이 직접 실행할 수 없습니다. 따라서 JSX는 반드시 Babel과 같은 트랜스파일러를 통해 일반 자바스크립트 코드로 변환된 후 실행됩니다.


1.1 JSX의 정의와 구성 요소

  • JSXElement HTML 태그 혹은 사용자 정의 컴포넌트를 표현합니다. 이때 컴포넌트 이름은 반드시 대문자로 시작해야 하며, 이는 리액트가 HTML 태그와 사용자 컴포넌트를 구분하기 위한 규칙입니다.

  • JSXAttributes 컴포넌트나 요소에 전달되는 속성입니다. 객체 전개 연산자(...props)를 사용할 수 있으며, 이는 props 전달의 핵심 메커니즘입니다.

  • JSXChildren 요소 내부에 포함되는 자식 노드입니다. 문자열, JSX, 배열, 표현식 등 다양한 형태가 가능합니다.


1.2 JSX의 변환 과정

리액트 17 이전에는 JSX가 다음과 같이 변환되었습니다.

React.createElement(type, props, children)

리액트 17 이후부터는 새로운 JSX Transform이 도입되어 react/jsx-runtime의 내부 함수 호출 형태로 변환됩니다.

중요한 점은 어떤 방식이든 JSX의 결과는 “화면”이 아니라 “자바스크립트 객체”라는 사실입니다.

JSX는 UI를 직접 그리는 문법이 아니라, “이런 UI를 원한다”는 선언을 담은 데이터 구조입니다.



2. 가상 DOM과 리액트 파이버

리액트는 UI 변경을 효율적으로 처리하기 위해 가상 DOM(Virtual DOM) 개념과 이를 관리하는 Fiber 아키텍처를 사용합니다.


2.1 DOM과 가상 DOM의 탄생 배경

브라우저는 다음과 같은 복잡한 렌더링 과정을 거칩니다. HTML 파싱 → DOM 트리 → CSSOM → 렌더 트리 → 레이아웃 → 페인팅SPA 환경에서는 상태 변화가 잦기 때문에, DOM 조작이 직접 발생하면 레이아웃 계산(Reflow)페인팅(Paint) 비용이 빈번하게 발생합니다.

가상 DOM은 이러한 문제를 해결하기 위해 등장했습니다.

  • 실제 DOM을 직접 수정하지 않고
  • 메모리 상에서 변경 사항을 먼저 계산
  • 필요한 변경만 모아 한 번에 DOM에 반영


2.2 리액트 파이버(React Fiber)

Fiber는 가상 DOM을 관리하고 비교하며, 렌더링 작업을 스케줄링하기 위한 리액트의 내부 실행 단위입니다.

(1) 동작 방식

  • 기존의 재귀 기반 스택 렌더링을 제거
  • 렌더링 작업을 작은 단위(Fiber)로 쪼갬
  • 작업을 중단, 재개, 우선순위 조절 가능

=> 사용자 입력에 더 빠르게 반응하고, 애니메이션 중 끊김 없는 렌더링을 가능하게 했습니다.

(2) 구조

Fiber는 단순한 JS 객체이며 다음과 같은 정보를 가집니다.

  • 컴포넌트 타입과 props
  • state 및 hook 정보
  • 이전 렌더 결과
  • 부모/자식/형제 Fiber (return, child, sibling)
  • 우선순위와 effect 정보

Fiber 트리는 트리 구조이면서 동시에 링크드 리스트 형태로 연결됩니다.

(3) 더블 버퍼링

리액트는 항상 두 개의 Fiber 트리를 유지합니다.

  • current : 현재 화면을 나타내는 트리
  • workInProgress : 다음 화면을 준비하는 트리

작업이 완료되면 포인터만 교체하여 화면을 갱신함으로써 불완전한 렌더링이 사용자에게 노출되는 것을 방지합니다.



3. 클래스 컴포넌트와 함수 컴포넌트

3.1 클래스 컴포넌트

클래스 컴포넌트는 초기 리액트에서 상태와 생명주기를 관리하는 핵심 수단이었습니다.

  • render를 통해 UI 정의
  • 생명주기 메서드를 통해 특정 시점에 로직 실행

그러나 다음과 같은 한계를 가졌습니다.

  • 로직 재사용이 어렵다 (HOC, render props)
  • this 바인딩 문제
  • 코드의 응집도가 낮아짐
  • 번들 크기와 복잡도 증가

3.2 함수 컴포넌트와 훅

리액트 16.8에서 훅이 도입되며 함수 컴포넌트가 주류가 되었습니다.

가장 중요한 차이는 데이터를 바라보는 방식입니다.

  • 클래스 컴포넌트 → this.props, this.state항상 최신 값으로 참조
  • 함수 컴포넌트 → 렌더링 시점의 props와 state를 클로저로 캡처

이로 인해 함수 컴포넌트는 “렌더링된 값은 불변이다”라는 개념을 더 명확히 드러냅니다.



4. 렌더링은 어떻게 일어나는가?

리액트에서 렌더링이란 DOM을 바꾸는 행위가 아니라, “현재 상태에서 UI가 어떻게 생겨야 하는지 계산하는 과정”입니다.

4.1 렌더링 발생 조건

렌더링은 다음 경우에 발생합니다.

  • 최초 마운트
  • setState, dispatch 호출
  • props 변경
  • key 변경
  • 부모 컴포넌트 렌더링

※ 단, 부모 렌더링 시 자식은 기본적으로 함께 렌더링됩니다.

4.2 렌더링 프로세스

  1. Render Phase
    • 컴포넌트 함수 실행
    • JSX 생성
    • Fiber 비교 및 변경점 계산
    • DOM 조작 ❌
  2. Commit Phase
    • 실제 DOM 변경
    • 화면 반영
    • useLayoutEffect, 이후 useEffect 실행

중요한 점은: 렌더 단계가 실행되었더라도, 변경 사항이 없다면 커밋 단계는 생략될 수 있다는 것입니다.



5. 메모이제이션: 기억해서 다시 하지 않기

리액트는 불필요한 렌더링과 연산을 줄이기 위해 다음과 같은 메모이제이션 도구를 제공합니다.

  • memo : 컴포넌트 렌더링 기억
  • useMemo : 값(계산 결과) 기억
  • useCallback : 함수 참조 기억

5.1 주장 1: 섣부른 최적화는 독이다

  • 메모이제이션 자체도 비교 비용과 메모리 비용이 든다
  • 가벼운 연산은 다시 계산하는 편이 더 빠를 수 있다
  • 실제 병목이 확인된 경우에만 적용해야 한다

5.2 주장 2: 렌더링 비용은 생각보다 크다

  • 렌더링은 단순 함수 실행이 아니다
  • 가상 DOM 생성, 비교, 하위 컴포넌트 전파가 포함된다
  • 참조 타입(props 객체, 함수)이 매번 새로 생성되면 memouseEffect 최적화가 무력화된다
// 이 글에서 살펴본 핵심 흐름은 다음과 같습니다.
JSX
→ React Element (UI 설계 데이터)
→ Fiber 노드 생성 / 재사용 (작업 단위)
→ Render Phase (계산 · 비교 · 스케줄링)
→ Commit Phase (DOM 반영)
→ Browser Rendering (Layout / Paint)



reference: 모던리액트 Deep Dive 2장. 리액트 핵심요소 깊게 살펴보기