PromleeBlog
sitemapaboutMe

posting thumbnail
React 렌더링 원리 파헤치기 Virtual DOM과 Fiber - React, 알고 쓰자 1일차
Demystifying React Rendering Virtual DOM & Fiber - React Explained Day 1

📅

🚀

들어가기 전에🔗

오늘은 React가 화면을 그리고 업데이트하는 핵심 과정인
렌더링 파이프라인
과, 그 과정을 더욱 부드럽고 빠르게 만들어주는
Fiber 아키텍처
라는 기술에 대해 알아볼 거예요.

🚀

Virtual DOM: 진짜 DOM의 똑똑한 복제본🔗

우리가 웹사이트에서 보는 모든 것은 사실
DOM(Document Object Model)
이라는 거대한 나무 구조로 이루어져 있습니다. HTML 태그 하나하나가 나무의 가지나 잎사귀처럼 연결되어 있는 모습이죠. 그런데 사용자가 어떤 행동을 할 때마다 이 거대한 나무 전체를 직접 바꾸는 것은 생각보다 느리고 번거로운 작업입니다.
그래서 React는
Virtual DOM(가상돔)
이라는 아주 영리한 방법을 사용합니다. 이름 그대로 '가짜' DOM인데요, 실제 DOM의 모습을 본떠 메모리(컴퓨터의 기억 공간)에 만들어 놓은 복제본 같은 것이라고 생각하면 쉽습니다.

Virtual DOM의 장점과 사용 이유🔗

React는 화면에 변화가 필요할 때, 실제 DOM을 바로 건드리지 않습니다. 대신, 이 Virtual DOM을 먼저 변경하고, 이전 Virtual DOM 모습과 비교해서
바뀐 부분만
찾아냅니다. 그리고 그 차이점만 골라서 실제 DOM에 딱 한 번 적용하는 거죠. 이렇게 하면 불필요한 작업을 줄여서 화면 업데이트 속도를 훨씬 빠르게 만들 수 있습니다.
이 비교 과정을
재조정(Reconciliation)
이라고 부릅니다. React는 이 재조정 과정을 통해 어떤 부분이 변했는지 알아내는 똑똑한 알고리즘(규칙과 절차)을 가지고 있습니다.

🚀

Reconciliation : 재조정🔗

React가 이전 Virtual DOM과 새로운 Virtual DOM을 비교해서 바뀐 부분을 찾아내는 과정을 '재조정'이라고 말씀드렸죠? React는 두 개의 Virtual DOM 트리를 비교하면서 다음 규칙에 따라 변경 사항을 찾아냅니다.
  1. 다른 타입의 노드
    만약 비교하는 두 노드의 태그 이름(예: <div><span>)이나 컴포넌트 타입(예: <LoginButton><LogoutButton>)이 다르면, React는 이전 트리를 완전히 버리고 새로운 트리로 완전히 교체합니다.
  2. 같은 타입의 DOM 엘리먼트
    두 노드의 태그 이름이 같다면(예: 둘 다 <div>), React는 속성(attributes)만 비교해서 바뀐 속성만 실제 DOM에 업데이트합니다. 예를 들어 className이나 style이 바뀌었다면 그것만 바꿔주는 거죠.
  3. 같은 타입의 컴포넌트 엘리먼트
    두 노드가 같은 타입의 컴포넌트라면(예: 둘 다 <Button>), React는 컴포넌트 인스턴스는 그대로 유지하고 내부 상태(state)와 속성(props)만 업데이트합니다. 그리고 이 컴포넌트에 대해서 다시 재조정 과정을 진행합니다.
  4. 리스트와 Key 속성
    여러 개의 자식 요소들을 처리할 때(예: 목록), React는 기본적으로 순서대로 비교합니다. 하지만 리스트 중간에 요소가 추가되거나 삭제되면 비효율이 발생할 수 있어요. 이때 key라는 특별한 속성을 사용하면, React가 각 요소를 고유하게 식별해서 훨씬 빠르고 정확하게 변경 사항을 찾아낼 수 있습니다. 이 key에 대해서는 잠시 후에 더 자세히 알아보겠습니다.
reconciliation flow
reconciliation flow
이런 재조정 과정 덕분에 React는 최소한의 작업으로 화면을 업데이트할 수 있습니다. 그런데 만약 화면에 바꿔야 할 내용이 아주 많거나 복잡하다면 어떻게 될까요?
예전의 React는 이 재조정 과정을 한 번에 끝내려고 했기 때문에, 가끔씩 화면이 잠시 멈추는 것처럼 보이는 문제가 있었습니다. 이 문제를 해결하기 위해 등장한 것이 바로
Fiber 아키텍처
입니다.

🚀

Fiber 아키텍처: 더 똑똑해진 리액트🔗

React 16 버전부터 도입된 Fiber 아키텍처는 React의 재조정 알고리즘을 근본적으로 재설계한 것입니다. 이전 방식(Stack Reconciler라고 불렸습니다)은 업데이트 작업을 시작하면 중간에 멈출 수 없이 끝까지 진행해야 했습니다.
그래서 복잡한 업데이트 작업 중에는 브라우저가 다른 중요한 일(애니메이션, 사용자 입력 처리 등)을 하지 못하고 화면이 버벅거리는 문제가 생길 수 있었습니다.

Fiber 노드🔗

각 컴포넌트나 DOM 엘리먼트에 해당하는 작업 단위입니다. 이전에는 단순히 컴포넌트 인스턴스 정보만 있었다면, 이제 Fiber 노드는 해당 컴포넌트의 작업 상태, 우선순위, 다른 Fiber 노드와의 관계(부모, 자식, 형제) 등을 담고 있는 더 상세한 정보 객체입니다. 마치 각 작업마다 붙어 있는 상세 작업 지시서와 같아요.
이 Fiber 노드들은
연결 리스트
(Linked List)와
트리
(Tree) 구조를 혼합한 형태로 서로 연결되어 있습니다.
업데이트 작업을 아주 작은 단위(Fiber 노드)로 쪼개고, 각 단위 작업에 우선순위를 매길 수 있게 합니다. 그리고 더 중요한 작업이 생기면 진행 중이던 작업을 잠시 멈추고 그 중요한 작업을 먼저 처리할 수 있습니다. 이걸
협력적 스케줄링(Cooperative Scheduling)
이라고 부릅니다.
Fiber는 브라우저가 너무 오랫동안 멈추지 않도록, 작업을 잘게 나누어 잠깐씩 처리하고 브라우저에게 제어권을 넘겨주는 방식으로 동작합니다. 이렇게 하면 복잡한 업데이트 중에도 애니메이션이 부드럽게 재생되고 사용자 입력에 빠르게 반응할 수 있게 됩니다.

렌더 단계와 커밋 단계🔗

Fiber 아키텍처는 크게 두 단계로 작업을 나눕니다.
➡️

렌더 단계 (Render Phase)🔗

어떤 변경사항이 필요한지 파악하는 단계입니다. Virtual DOM을 비교하고, 변경이 필요한 Fiber 노드를 찾아내는 작업을 합니다. 이 단계는
중단될 수 있습니다
. 즉, 더 높은 우선순위의 작업이 들어오면 하던 일을 멈추고 나중에 다시 시작할 수 있습니다.
실제 DOM에는 아직 아무런 변화도 적용되지 않습니다.
➡️

커밋 단계 (Commit Phase)🔗

렌더 단계에서 파악된 모든 변경사항을 실제 DOM에
한꺼번에
적용하는 단계입니다. 이 단계는
중단될 수 없습니다
. 한번 시작하면 모든 변경사항이 적용될 때까지 멈추지 않습니다.
이 Fiber 아키텍처 덕분에 React는 사용자 경험을 해치지 않으면서도 복잡한 화면 업데이트를 효율적으로 처리할 수 있게 되었습니다.

🚀

Key 속성: 리스트 업데이트의 열쇠🔗

앞서 재조정 과정에서 리스트를 처리할 때 key 속성이 중요하다고 잠깐 언급했었죠? 이제 key가 왜 중요하고 어떻게 도움이 되는지 좀 더 자세히 알아봅시다.
여러 개의 비슷한 항목들을 목록으로 보여줘야 할 때가 많습니다. 예를 들어, 친구 목록이나 할 일 목록 같은 경우죠. React에서 이런 목록을 렌더링할 때는 보통 배열의 map() 함수를 사용합니다.
const todoItems = todos.map((todo) =>
  <li>{todo.text}</li>
);
만약 이 todos 배열이 다음과 같다고 해봅시다.
[
  { id: 1, text: '장보기' },
  { id: 2, text: '운동하기' }
]
이 배열을 사용해 렌더링하면 다음과 같은 HTML 구조가 만들어질 것입니다.
<ul>
  <li>장보기</li>
  <li>운동하기</li>
</ul>
이제 여기에 새로운 할 일 '책 읽기'를 맨 앞에 추가한다고 가정해 봅시다.
[
  { id: 3, text: '책 읽기' }, // 새로 추가
  { id: 1, text: '장보기' },
  { id: 2, text: '운동하기' }
]
만약 key 속성을 사용하지 않았다면, React는 단순히 순서만 보고 비교합니다.
  1. 이전 첫 번째 <li>장보기</li>와 현재 첫 번째 <li>책 읽기</li>를 비교합니다. 내용이 다르므로 '장보기'를 '책 읽기'로 업데이트합니다.
  2. 이전 두 번째 <li>운동하기</li>와 현재 두 번째 <li>장보기</li>를 비교합니다. 내용이 다르므로 '운동하기'를 '장보기'로 업데이트합니다.
  3. 현재 세 번째 <li>운동하기</li>가 새로 생겼으므로, 마지막에 추가합니다.
결과적으로 모든 <li> 요소가 업데이트되고 하나가 추가되는 비효율적인 작업이 발생합니다.
하지만 각 <li> 요소에 고유한 key를 지정해주면 어떻게 될까요? key는 각 항목을 식별할 수 있는 고유한 이름표와 같습니다. 보통 데이터의 id를 사용합니다.
const todoItems = todos.map((todo) =>
  <li key={todo.id}>{todo.text}</li> // key 속성 추가
);
이렇게 key를 사용하면 React는 훨씬 똑똑하게 비교합니다.
  1. React는 key3인 '책 읽기' 항목이 새로 추가된 것을 바로 알아챕니다.
  2. key1인 '장보기' 항목과 key2인 '운동하기' 항목은 그대로 유지된 것을 확인합니다. 내용 변경도 없습니다.
따라서 React는 단순히 새로운 '책 읽기' 항목만 맨 앞에 삽입하고, 기존 항목들은 그대로 둡니다. 훨씬 효율적이죠?
👍
key는 형제 요소들 사이에서만 고유하면 됩니다

index를 key로 사용하면 안 되는 이유🔗

index는 배열의 순서를 나타내는 숫자이기 때문에, 리스트의 내용이나 순서가 변경되었을 때
React가 올바르게 변경 사항을 감지하지 못할 수 있습니다.
const [items, setItems] = useState(['A', 'B', 'C']);
return (
  <ul>
    {items.map((item, index) => (
      <li key={index}>{item}</li>
    ))}
  </ul>
);
위 상태에서 items 배열이 ['C', 'B', 'A']로 바뀌면 어떻게 될까요?
React는 여전히 index를 기준으로 각 항목을 렌더링하려고 하기 때문에,
그 결과
이러한 문제가 생길 수 있습니다.

적절한 key 사용법🔗

가능한 한
데이터가 가진 고유한 식별자(ID)
key로 사용하는 것이 좋습니다.
const items = [
  { id: 'a1', name: 'A' },
  { id: 'b2', name: 'B' },
  { id: 'c3', name: 'C' },
];
return (
  <ul>
    {items.map(item => (
      <li key={item.id}>{item.name}</li>
    ))}
  </ul>
);
이렇게 하면 항목의 순서가 바뀌거나 새로운 항목이 추가되더라도 React는 정확하게 어떤 항목이 어떤 것인지 파악할 수 있기 때문에
불필요한 재렌더링을 방지하고 UI 상태를 안정적으로 유지할 수 있습니다.

언제 index를 써도 괜찮을까요?🔗

일반적으로는 index 사용을 피하는 것이 좋지만, 아래와 같은 경우에는 예외적으로 사용할 수 있습니다

🚀

Strict Mode와 Concurrent Features🔗

React는 개발자들이 더 안전하고 효율적인 코드를 작성하도록 돕는 기능들도 제공합니다.

Strict Mode: 엄격 모드🔗

Strict Mode는 애플리케이션 내의 잠재적인 문제를 알아내기 위한 도구입니다. 이름처럼 '엄격한' 모드인데요, 실제 운영 환경에서는 아무런 영향을 주지 않고 개발 중에만 활성화됩니다.
Strict Mode는 불안정한 생명주기 메서드 사용, 예상치 못한 부작용 등을 감지하고 개발자에게 경고 메시지를 보여줍니다.
예를 들어, Fiber 아키텍처에서는 렌더 단계가 중단되고 재개될 수 있기 때문에, 렌더링 과정에서 부작용(side effect)이 발생하는 코드는 문제가 될 수 있습니다. Strict Mode는 이런 함수들을 의도적으로 두 번 호출해서 개발자가 문제를 미리 발견하도록 돕습니다.
Strict Mode를 사용하려면, 애플리케이션의 일부를 <React.StrictMode> 태그로 감싸주면 됩니다.
import React from 'react';
function MyApp() {
  return (
    <React.StrictMode>
      <ComponentOne />
      <ComponentTwo />
    </React.StrictMode>
  );
}

Concurrent Features: 동시성 기능🔗

React 18 버전부터 공식적으로 도입된 동시성 기능은 Fiber 아키텍처의 장점을 극대화하는 새로운 기능들의 집합입니다.
핵심은 여러 상태 업데이트의 우선순위를 다르게 처리하여, 긴급한 업데이트(예: 사용자 입력 반응)가 덜 긴급한 업데이트(예: 데이터 로딩 후 화면 표시) 때문에 지연되지 않도록 하는 것입니다.
예를 들어 startTransition API를 사용하면, 특정 상태 업데이트를 '긴급하지 않음'으로 표시할 수 있습니다. 이렇게 하면 해당 업데이트 때문에 앱의 반응성이 떨어지는 것을 방지할 수 있습니다.
import { startTransition } from 'react';
 
function handleClick() {
  startTransition(() => {
    setState(newState);
  });
}
 
// ... <button onClick={handleClick}>Update State</button>
동시성 기능은 앱의 사용자 경험을 크게 향상시킬 수 있는 강력한 도구들이지만, 사용법을 정확히 이해하고 적용하는 것이 중요합니다.

🚀

결론🔗

오늘은 React가 화면을 어떻게 그리고 업데이트하는지에 대한 첫걸음을 떼었습니다.
Virtual DOM
은 실제 DOM의 가벼운 복사본으로, 메모리에서 빠르게 변경하고 비교할 수 있게 해줍니다.
재조정(Reconciliation)
은 이전 Virtual DOM과 새 Virtual DOM을 비교하여 실제 DOM에 적용할 최소한의 변경 사항을 찾는 과정입니다.
Fiber 아키텍처
는 이 재조정 과정을 더 작은 작업 단위(Fiber 노드)로 나누고, 우선순위에 따라 작업을 관리하며, 필요시 작업을 중단할 수 있게 하여(협력적 스케줄링) 애플리케이션의 반응성을 높여줍니다. 렌더 단계(중단 가능)와 커밋 단계(중단 불가)로 나뉘어 동작합니다.
key 속성
은 리스트 항목들을 효율적으로 업데이트하기 위한 필수적인 도구로, 각 항목에 고유한 식별자를 제공하여 React가 변경 사항을 정확히 추적하도록 돕습니다.
Strict Mode
Concurrent Features
는 더 안전하고 사용자 친화적인 React 애플리케이션을 만드는 데 도움을 주는 기능들입니다.
다음 시간에는 React에서 상태를 관리하는 가장 기본적인 방법인 useState 훅이 내부적으로 어떻게 동작하는지 자세히 알아보겠습니다.

참고🔗