React의 선언형 프로그래밍과 Virtual DOM

dodog·2024년 9월 7일
post-thumbnail

현재 듣고 있는 멘토링 수업에서 React에 관한 2가지 질문을 받았었다.

  1. React가 Virtual DOM을 왜 사용하는지
  2. React와 선언적 프로그래밍의 관계

1번은 너무나 흔한 React 기술 면접 질문인데, 나는 복잡한 SPA에서는 실제 DOM보다 가벼운 V-DOM을 통해 변화를 감지하면 성능에 도움이 되지만, 간단한 애플리케이션에서는 V-DOM을 사용하는게 더 느릴 수 있다고 답변했었다.

얼추 맞지 않을까? 하는 생각이였지만 사실 그냥 전혀 V-DOM을 이해하지 않고 단순히 면접을 준비하기 위해 외운 수준의 답변에 지나지 않았다.

놀랍게도 "Virtual DOM을 사용하는 것은 특정한 경우가 아닌 모든 경우에서 성능이 떨어진다"는 답변을 들을 수 있었다.

그러면 React는 대체 왜 Virtual DOM을 사용하나요?

라는 당연한 의문에 대한 답은 아래와 같다.

애초에 Virtual DOM은 성능 때문에 사용하는게 아니라 오히려 그 반대로 trade off의 댓가로 성능의 저하를 지불하고 개발의 편의성을 얻기 위해 사용하는 것이다.

생각지 못한 답변이였다. 프로그래밍에서 성능보다 중요한 것이 유지보수성이라는 것을 글로 배우기는 했지만, V-DOM에 대해 깊게 공부해보지 않아서인지 이런 생각 자체를 하지 못했다.

그러면 당시 facebook 개발자들은 어떠한 어려움을 겪었기 때문에 성능을 일부 포기하고서라도 Virtual DOM을 사용하여 React를 개발하게 되었을까?

React의 등장 배경

React 다큐멘터리 유튜브 영상

다큐멘터리를 보면 facebook이 급격하게 성장하면서 개발자들이 가장 어려움을 겪은 것은 변경된 상태를 DOM에 업데이트 하는 것이고, 이 과정이 수 많은 버그를 만들어내는 가장 큰 원인이였다고 한다.

SPA 구현을 위한 단계

  1. 상태 관리 및 업데이트
  2. 업데이트 할 DOM 탐색 및 조작

이렇게 SPA를 구현하기 위해선 2가지 단계가 있는데, 서비스가 커짐에 따라 상태가 늘어나고 DOM 요소도 늘어나니 복잡도가 기하급수적으로 늘어날 것이다.

그래서 참다 못한 개발자들이 SPA의 복잡도를 낮추기 위해, 상태 관리 및 업데이트에만 집중하고 DOM 탐색 및 조작이라는 관심사를 분리하여 선언적으로 처리하기 위해 만든 기술이 바로 React이다.

동일한 동작을 수행하는 간단한 SPA 예제를 각각 Vanilla JS와 React로 직접 구현해서 비교해보면 좋을 것 같아서 매우 대표적인 SPA 예시인 counter를 구현해서 비교해보기로 했다.

Vanilla JS로 만든 Counter

<!DOCTYPE html>
<html>
<body>
  <div>
  	<p id="count">Count: 0</p>
  	<button id="incrementBtn">Increment</button>
  </div>
  <script>
    let count = 0;
    const countDisplay = document.getElementById('count');
    const incrementBtn = document.getElementById('incrementBtn');

    function incrementCount() {
      count += 1;
      countDisplay.textContent = `Count: ${count}`;
    }

    incrementBtn.addEventListener('click', incrementCount);
  </script>
</body>
</html>

React로 구현한 Counter

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  const incrementCount = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={incrementCount}>Increment</button>
    </div>
  );
}

export default Counter;

단순히 count를 1씩 늘리는 매우 간단한 예제임에도 React 코드가 더 단순하다.

그러면 React는 어떻게 DOM을 처리하는 부분에 대한 관심사를 분리했을까?

React가 Virtual DOM을 사용하게 된 이유

React는 함수형 프로그래밍을 기반으로 설계되어 많은 부분을 선언적으로 처리해준다. 그렇게 선언적인 라이브러리를 만들기 위한 핵심 아이디어중 하나가 바로 DOM을 직접 조작하지 않고 선언적으로 다루는 것이다.

이 부분에 대한 아이디어는 당시 facebook 개발팀의 Jordan Walke가 우리는 이 과정을 더 간단하게 처리할 수 있을 것 같다며, 일일이 상태를 업데이트하는게 아니라 그냥 날려버리고 새로 만들면 어떨까? 라는 단순한 발상에서 Virtual DOM이라는 아이디어가 탄생했다고 한다.

그러나 상태가 하나 바뀌었다고 모든 UI를 다시 그리는거는 성능상 매우 비효율적이므로 다른 방법을 찾아야 했다. 그래서 대신 상태가 바뀐 요소를 포함한 전체 하위 트리를 날려버리고 새로 그리는 방법을 시도했던 것이다.

그래서 상태가 바뀐 부분이 어디인지 감지하기 위해서 상태 변경 전 / 후를 비교할 필요가 있었는데, 이 때 여러 복잡한 API를 가진 실제 DOM으로 비교하는 것은 무겁기 때문에 상대적으로 가벼운 Virtual DOM이라는 것을 만들어 상태 변경 전 / 후의 스냅샷을 비교하고 어떤 노드를 날리고 새로 렌더링해줘야하는지 파악하려 했던 것이다.

이렇게 간단한 JS 객체 구조의 Virtual DOM이 탄생하게 되었다.

React가 단방향 데이터 바인딩만 지원하는 이유

V-DOM을 어떠한 목적을 위해 탄생했는지 이해하게 되자 React가 어떻게 동작하는지에 대한 이해도 크게 증가했다. 그러자 왜 React가 단방향 데이터 바인딩만 지원하게 된건지 자연스럽게 이해하게 되었다.

React는 상태 업데이트 시, 최대한 복잡도를 낮추기 위해 V-DOM 스냅샷 비교를 통해 업데이트 해야하는 부분을 감지한 뒤, 그 부분을 포함한 전체 하위 트리를 다 날려버리고 새로 만든다고 지금까지 살펴봤었다.

그런데 만약 양방향 데이터 바인딩을 지원해서 부모 컴포넌트로도 state를 전달할 수 있게 된다면, 애초에 이런 방식 자체가 맞지 않는다 것을 직관적으로 이해할 수 있다.

만약 그렇게 된다면, 상태가 변경된 노드의 하위 트리가 아니라 상위 트리까지도 업데이트 해줘야할 수도 있다는 뜻이므로, 아예 전체 DOM을 다 날리고 그리거나 아니면 결국 다시 업데이트 된 부분만 정확하게 타겟해서 새로 그려줘야하게 되는 것이다.

그렇기 때문에 React는 데이터의 흐름을 단방향으로 제한할 수 밖에 없었을 것이다.

마무리

멘토링에서 받은 짧은 2개의 질문은 내가 React 동작원리에 대한 이해가 부족하다는 것을 느끼고 깊게 탐구해보는 시간을 갖게 해준 계기가 되었다.

이번에 깊게 공부하면서 React가 함수형 프로그래밍의 원칙을 기반으로 정교하게 설계된 선언적 UI 라이브러리라는 것을 정말로 이해하고 받아들일 수 있었고, Virtual DOM에 대한 잘못된 이해를 바로 잡고, 왜 단방향 데이터 바인딩으로 제한했는지에 대해서도 스스로 생각해보고 이해할 수 있었다.

이렇게 제대로 몰랐던 React의 많은 부분에 대해서 이해하게 되었지만, 역설적으로 그렇기 때문에 내가 React에서 이해하지 못한 부분이 얼마나 더 많이 있는지 알게 되었다.

Virtual DOM은 어떻게 상태 업데이트 전 후를 비교하는 걸까? 라는 의문에서 시작된 꼬리질문을 통해 Virtual DOM은 상태를 기억하고 있지 않고, Fiber 노드가 상태의 변화를 감지하고 Virtual DOM을 생성해준다는 것을 이해할 수 있었다.

그렇기 때문에 Fiber 아키텍쳐가 뭔지 정말 궁금해졌다.
그리고 이번 포스팅에서는 생략하긴 했지만 Virtual DOM의 재조정 과정에 사용되는 Diffing 알고리즘에 대해서도 공부해보고 싶은 생각이 들게 되었다.

최근에는 계속해서 프론트엔드 기본 기술 대한 깊은 탐구를 진행하며 블로그 글을 작성하고 있는데, 고등학교 때 새로운 개념을 배우는게 즐거워서 완전히 빠져들어 공부하던 때의 느낌을 받고있다.

신입 때는 왜 그러지 못했을까 하는 아쉬움이 있지만, 앞으로라도 계속해서 기술에 대해 깊게 탐구하고, 끊임없이 꼬리질문을 통해 더 깊게 공부해보는 습관을 지속해야겠다.

profile
심리학, 사회문제해결에 관심이 많은 프론트엔드 개발자

0개의 댓글