React DOM 작동 원리

younghyun·2022년 12월 22일
0

React

JavaScript UI Library
상태값(state, props, redux store 등)이 변경될 때마다 UI를 자동으로 업데이트해주는 JS Library (해당 컴포넌트 함수를 자동으로 재 호출하여 재 렌더링 해줌.)
이를 위해 "Virtual DOM 을 통해 변경된 부분만 효율적으로 Update" 해주는 구조 채택.

DOM(Document Object Model)

HTML, XML 문서의 프로그래밍 interface.
텍스트 파일이기만 했던 HTML이나 XML 문서(document)에 프로그래밍 언어가 접근할 수 있도록 해주는 문서 구조를 트리 형태의 객체(Object)로 표현한 것.(문서의 구조화된 표현(structured representation)을 제공) 특히 웹 브라우저는 DOM을 활용하여 자바스크립트와 CSS를 적용함.(문서 구조, 스타일, 내용 등을 변경할 수 있게 돕는다.)

Browser WorkFlow ( Rendering 과정 )

우리는 웹브라우저를 통해 원하는 페이지로 이동을 할 때 도메인을 이용. 도메인 주소로 접속을 하면 DNS 서버로 가서 실제 주소(서버)에 요청. 요청 후 서버는 해당 웹페이지 index.html 등의 파일을 Response 해줌. 이 Response 파일들은 이제 앞서 설명한대로 브라우저와 자바스크립트가 HTML을 이해하기 쉽게 트리 구조로 표현.

  1. Response files
  2. DOM Tree( 브라우저는 서버가 보내준 HTML 파일 을 해석(Parsing)하여 DOM 트리(객체) 만듦. )
  3. CSSOM(DOM + CSS) Tree( 브라우저는 서버가 보내준 CSS 파일 을 해석(Parsing)하여 CSSOM 트리(스타일 규칙) 도 만듦. )
  4. Render Tree( DOM Tree + CSSOM Tree 합쳐서 웹 브라우저에 보여져야할 요소를 표현 )
  5. Render Tree로 각 Node 위치와 크기를 계산한 Layout 만들고(배치하고), Viewport 내에 위치와 크기를 계하고, 색을 칠하는 등의 작업을 함.
  6. Layout 계산이 완료되면 이제 요소들을 실제 화면에 그리는 페인트(Paint)를 함.

DOM은 새로운 요청, 변경사항이 있으면 위와 같은 형태를 거쳐 매번 리렌더링 하게 됨.

DOM API는 수많은 웹 브라우저에서 사용되어 왔지만, 이 DOM에는 동적 UI에 최적화되어 있지 않다는 치명적인 문제점 한 가지가 존재. 
동적으로 웹 페이지를 변경하다 보면 element의 생성, 수정, 제거 등 DOM을 변화시키는 수많은 연산이 생김.

이 과정에서 문제가 되는 경우는 현대의 웹처럼 변경해야할 대상도 많고 변경도 많은 경우.
프로그래밍에 의해 DOM을 변경해야하고 변경할 구성 요소가 100개면 위의 과정을 100번 하는 비효율적인 작업을 해왔음.

DOM 자체를 처리하는 것은 그렇게 큰 성능 이슈를 발생시키지 않지만,
(DOM 속도는 느리지 않음. DOM 자체를 읽고 쓸 때의 성능은 자바스크립트 객체를 처리할 때의 성능과 비교하여 다르지 않음.)
정확히는 DOM을 변경하는게 문제가 아니고 렌더링을 여러번 하는 게 문제.
웹 브라우저는 DOM을 활용하여 객체에 JS와 CSS를 적용하게 되는데, 웹 브라우저 엔진의 성능이 좋아졌지만, CSS 연산을 다시 하고 페이지를 리 페인트 하는 과정이 매번 새롭게 반복되면(렌더링 되면) 시간이 오래 걸리면서 성능이 안 좋아짐.

하지만, 그렇다고 DOM을 사용하지 않을 수도 없음. 결과적으로 웹 브라우저는 최종적으로 DOM을 보고 웹 페이지를 그리기 때문.

그렇다면 작업의 결과물은 동일하게 유지하되 변경되는 DOM을 최소한으로 만들기 위한 방법이 필요했고, React는 Virtual DOM을 활용해 DOM 처리과정을 최적화.

그렇기 때문에 Virtual DOM을 사용해야 하며, 이 방식을 통해 DOM 업데이트를 추상화함으로써 DOM 처리 횟수를 최소화하고 효율적으로 진행하게 만들어줌. 여기서 이제 Virtual DOM의 장점이 나오게 됨.

Ps)
Browser도 무식하게 100번 Rendering 하지는 않음.
이해를 돕기 위한 예시고 사실은 DOM을 제어하는 메서드에 따라서 다르지만 어느정도 합쳐서 배치로 작업을 할 수 있음. (Batched DOM Update)
그냥 이해를 돕기 위해 100개가 변경되면 20번 렌더링이 된다고 가정.
그래도 여전히 비효율적임.

렌더링 과정

Virtual DOM

실제 DOM에 접근하여 조작하는 대신, 이를 추상화하여 메모리에 저장하고 관리하는 자바스크립트 객체(Virtual DOM)를 구성하여 사용.(JavaScript 객체를 따로 관리하므로 메모리 사용량이 늘어난다는 단점이 있음. 일종의 DOM 캐싱으로 볼 수 있음.)
in-memory에 존재해서 실제 Render 되지 않음.

HTML은 브라우저가 문서 객체 모델(DOM)을 구성하기 위해 따라야 하는 절차. HTML 문서를 이루는 Element는 Browser가 HTML 문서를 읽어 들이면 DOM Element가 되고, 이 DOM이 화면에 사용자 인터페이스를 표시. (Render Tree)

React는 DOM 엘리먼트를 직접 조작하지 않고, Virtaul DOM(React Element. React Element는 HTML Element와 비슷하지만 실제로는 Javascript 객체)을 다룸.
따라서 HTML DOM API를 직접 다루는 것보다 Javascript 객체인 React 엘리먼트를 직접 다루는 편이 훨씬 빠름.

{
    type: 'div',
    props: {
        children: [
            {
                type: button,
                props: {
                    children: "버튼입니다",
                    onClick: ()=>{}
                }
            },
            {
                type: input,
                props: {children: "인풋입니다."}
            },
            {
                type: CountButton,
                props: {children: "리액트컴포넌트입니다"}
            }
        ]
    }
}

리액트 UI Update 단계

  • 리액트는 아래의 2단계를 통해 상태값 변경에 따른 UI 업데이트를 진행

1) 렌더 단계(Render Phase. diffing 이라고도 함.)

  • 컴포넌트 상태 값이 변경 되면, 렌더링이 되고, 리액트는 렌더링 할 때마다 매번 새로운 Virtual DOM을 만들고(데이터를 업데이트하면 전체 UI를 Virtual DOM에 리 렌더링), 변경 내역을 한 번에 Virtual DOM에 모음(buffering)
  • 이전 Virtual DOM과 비교하여 바뀐 부분을 탐색하고(diffing 알고리즘),
  • 실제 DOM에 반영할 부분을 결정.

2) 커밋 단계(Commit Phase)

  • 렌더 단계를 거쳐 바꾸기로 결정된 부분만 실제 DOM에 반영.
  • 브라우저는 변경된 실제 DOM을 화면에 paint.
  • 이 때가 didMount, didUpdate가 완료되어 useEffect가 호출되는 시점.
    ( 레이아웃 계산은 한번만 )

위의 과정을 재조정(Reconcilation) 이라고 부름. 이렇듯 동적 UI를 가진 웹 서비스라면 React를 활용해 효율적으로 더 좋은 UX를 제공할 수 있는 것.

EX) 만약 어떤 게시판에서 다음 페이지를 눌러 리스트 10개(<ul>태그안의 <li>태그)를 변경하는데 DOM에 접근하여 <li>10번을 바꾸지 않고 Virtual DOM을 통해 리스트 10개(<li>태그)를 변경하고 Virtual DOM과 실제 DOM을 비교하여 변경된 부분(<ol>통채로)을 1번만에 변경하여 렌더링도 1번만 일어나게 하는 것


위와 같은 과정을 React에서는 ReactDOM.render() 함수가 도맡아서 처리하고 있음. 만약 어떻게 render 함수가 변화를 감지하고 효과적으로 DOM을 업데이트하는지 알고 싶다면 React 공식 문서에 나와있는 Reconciliation 문서를 읽는 것을 추천.

Raect 얕은 비교(Shallow Compare)

React의 얕은 비교는 같은 레벨에서만 일어남.

  • 숫자, 문자열, Boolean과 같은 원시 자료형은 값을 비교함.
  • 배열, 객체 등 참조 자료형은 그 안의 값 혹은 attribute를 비교하지 않고, 그들의 레퍼런스(참조되는 위치)를 비교.

그러므로 배열을 직접 수정하는 방식, 예를 들어, push, pop 과 같은 메소드로 배열을 수정한 뒤 setState에 담아주어도 기본적으로 같은 참조 위치를 가지고 있기 때문에 값의 변화를 감지하지 못함.

React에서 객체 불변성을 유지하기
React에서 배열 값을 변경할 때는 객체와 마찬가지로, 불변성을 지켜주어야 함.
이는 무슨 말이냐고 하면, 배열 혹은 객체의 원본을 수정하지 않고 상태 변경을 원하는 배열과 함수를 복사(깊은 복사, 얕은 복사는 안 됨)하고 나서 사용해야 한다는 뜻.
그래서 원본을 수정하는 메소드인 push(), pop()과 같은 메소드를 사용해서 원본을 직접 수정하는 것은 React를 사용할 때 지양해야 함.
대신, assign() 메소드를 사용하거나, 전개구문을 사용해서 복사를 한 뒤 그 복사값을 수정하고 setState에 담아주면 변경을 감지할 수 있음.

리액트를 알몸으로 사용하는 방법

1) 리액트 라이브러리 불러오기

  • 리액트는 JS 라이브러리이므로, 당연히
<script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
  • react.development.js 파일을 로드하면 React 객체 생성됨
  • react-dom.development.js 파일을 로드하면 ReactDOM 객체가 생성됨.

2) 리액트 컴포넌트 생성하기

  • React 객체의 createElement(태그, 속성, 자식) 메서드로 리액트 요소를 생성함.
// 함수 컴포넌트
function CountButton() {
  // useState 사용
  const [count, setCount] = React.useState(0);

  // 리액트 요소 생성
  return React.createElement(
    'button', // button 태그 생성
    {
      onClick: () => {
        // 클릭 이벤트 핸들러 설정
        setCount(count + 1);
      }
    },
    count // children 설정
  );
}  

3) 리액트 컴포넌트를 화면에 렌더링하기

  • ReactDOM 객체의 render(컴포넌트, 컨테이너) 메서드로 컴포넌트를 렌더링함.
const domContainer = document.getElementById('root');
ReactDOM.render(React.createElement(CountButton), domContainer);
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>

    <!-- 리액트 라이브러리 로드 -->
    <script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
  </head>
  
  <body>
    <!-- 컨테이너 -->
    <div id="root"></div>


    <script>
        
      // 리액트 함수 컴포넌트
      function CountButton() {
        // useState 사용
        const [count, setCount] = React.useState(0);

        return React.createElement(
          'button', // button 태그 생성
          {
            onClick: () => {
              // 클릭 이벤트 핸들러 설정
              setCount(count + 1);
            }
          },
          count // children 설정
        );
      }

      // div#root 태그에 리액트 컴포넌트 렌더링하기
      const domContainer = document.getElementById('root');
      ReactDOM.render(React.createElement(CountButton), domContainer);

    </script>
  </body>
</html>

실제 리액트로 개발을 할 때는 JSX로 코딩하고, 여기 저기서 모듈들을 export, import 해서 불러오는데, 사실 이는 모두 바벨과 웹팩의 은총 덕분임.

1) Babel 이 JSX를 React.createElement() 로 변환해줌.
2) WebPack이 JS, CSS 파일을 번들링하여 모듈화해줌.

참고로, npx create-react-app 등을 사용하면 Babel, Webpack 등이 기본적으로 세팅되어있음.

주의사항

  • 0.1초마다 화면에 데이터가 변경된다면? Virtual DOM으로 0.5초씩 모아가지고 렌더링을 적게할 수 있을까? → 안됨. 동시에 변경되는 것에 한해서만 렌더링됨.
  • React나 Vue등을 이용해서 Virtual DOM을 쓰면 무조건 빠른가? → 아님. 똑같이 최적화를 해야함. (슬라이드를 옮기거나 무한 스크롤등의 움직임이 있을 때는 Virtual DOM을 이용해서 반복 렌더링을 하지 않도록 해줘야함.)
  • Virtual DOM은 메모리에 존재함. DOM에 준하는 무거운 객체(Virtual DOM)가 메모리에 상주(?)하고 있기 때문에 메모리의 사용이 많이 늘어날 수 밖에 없음.
  • Virtual DOM을 조작하는 것도 엄청나게 많은 컴포넌트를 조작하게 된다면 오버헤드가 생기기 마련임. Virtual DOM 제어가 DOM 직접 제어에 비해 상대적으로 비용이 적게 들 뿐임.
    • Virtual DOM은 기존 DOM의 단점을 보완. 하지만 항상 DOM보다 Virtual DOM이 빠르고 효과적이지 않음. React는 지속적으로 데이터가 변하는 대규모 어플리케이션에 적합하다고 나와있음.
      React를 사용하지 않아도 코드 최적화를 열심히 하면 DOM 작업이 느려지는 문제를 개선할 수 있고, 또 작업이 매우 간단할 때는 오히려 리액트를 사용하지 않는 편이 더 나은 성능을 보이기도 한다.

참고
https://buyandpray.tistory.com/79
https://curryyou.tistory.com/484
https://velog.io/write?id=b6d68106-75d3-499b-aed0-02d9f73467a3
https://devbirdfeet.tistory.com/219
https://jw910911.tistory.com/24
https://jeong-pro.tistory.com/210
https://ljtaek2.tistory.com/137
https://babycoder05.tistory.com/entry/React-Virtual-DOM-%EA%B3%BC-%EB%B9%84%EA%B5%90-%EC%9B%90%EB%A6%AC%EC%99%80-%EC%96%95%EC%9D%80-%EB%B9%84%EA%B5%90?category=1023016

profile
선명한 기억보다 흐릿한 메모

0개의 댓글