icon

메티의 블로그

React 에서의 VirtualDOM

React 에서의 VirtualDOM

Tags
React
날짜
May 10, 2022
상태
공개
 
선행 지식
 
Critical Rendering Path
  • 브라우저가 서버에 웹 페이지에 대한 요청을 하게 되면, 서버는 response 의 header 나 data 로 html, css, js 파일을 반환한다.
  • 이후 브라우저는 받아온 html, css, js 파일을 가지고 viewport 라는 브라우저의 화면 부분에 알맞게 뿌려주게 된다. 이 받아온 html, css, js 를 가지고 viewport에 뿌려주는 과정을 CRP 라고 한다.
 
Critical Rendering Path 의 과정
  1. DOM
    1. html 의 태그들을 통해 DOM 을 구성한다. DOM은 페이지의 모든 컨텐츠를 포함한다. (실제 브라우저 화면에 보이지 않는 모든 태그를 포함한다.)
    2. notion image
  1. CSSOM
    1. DOM을 스타일링 하기 위한 스타일 정보를 포함한다. CSS 는 렌더링을 막는다. 모든 CSS를 처리하고 수신할 때 까지 페이지 렌더링을 막는다. CSSOM이 완료 될 때까지 새로운 콘텐츠를 렌더링 할 수 없다.
    2. notion image
  1. 렌더 트리
    1. DOM과 CSSOM을 결합한다. 렌더 트리는 오직 보여지는 콘텐츠만을 캡쳐한다. 그래서 display: none이 적용 되어있다면, 해당 요소 및 그 하위 요소는 포함 되지 않는다.
    2. notion image
  1. Layout
    1. 요소들이 화면에 배치되는 위치와 방법, 각 요소들의 크기와 관련된 위치를 결정하는 연산
    2. 레이아웃의 성능은 DOM 의 영향을 받는다. DOM 노드가 적어야 이 연산이 빠르게 종료된다.
    3. 가장 무거운 연산
  1. Paint
    1. 화면에 픽셀을 그리는 연산
    2. 두번째로 무거운 연산
 

 
Virtual DOM
  • 배경
    • DOM을 직접 조작하면 Layout 연산이 일어나게 되는데, 이 Layout 연산이 굉장히 무거운 연산이다.
    • React는 SPA 프레임워크이다. SPA 환경에서는 DOM 트리 노드 개수가 많고, DOM 의 변경이 자주 일어난다.
    • 이전 DOM 트리에서 새로운 DOM 트리로 최소한의 연산을 통해 변경 되는 것이 Layout 연산을 줄일 수 있는 방법이다.
  • Virtual DOM
    • 변경이 되어도 CRP 후속 연산이 따르지 않는, 메모리에만 올려놓은 DOM
    • Virtual DOM 을 이용해 재조정이라는 연산을 통해, DOM 중에서 업데이트 해야하는 element 만 찾아서 실제 DOM 을 변경
 
Virtual DOM 과 DOM 의 차이점
  • 실제 DOM 에 비해 Virtual DOM 이 더 적은 정보를 가지고 있어 가볍다.
  • Virtual DOM 은 메모리 상에서만 올라가 있으며 Virtual DOM 트리가 변경되어도, 렌더링을 하지 않는다.
 
재조정(Reconciliation)
  • React 가 Virtual DOM 을 통해 변경된 element 들을 빠르게 찾기 위해 채택한 방법
 
동기
  • 하나의 트리를 가지고 다른 트리로 변환 할 때 최소 연산 수를 구하는 알고리즘의 복잡도는
  • React는 두가지 가정을 추가하여 휴리스틱하게 알고리즘을 변경해 으로 복잡도를 낮춤
    • 서로 다른 타입의 두 element는 서로 다른 트리를 만들어 낸다.
    • 개발자가 key props 를 통해, 여러 렌더링 사이에서 어떤 자식 element 가 변경되지 않아야 할 지 표시해 줄 수 있다.
  • 비교 알고리즘 (Diffing Algorithm)
    • 두 개의 트리를 비교할 때, 두 트리의 root element 부터 비교한다.
        1. root element 타입이 다른 경우
          1. 이전 DOM의 root 이하 노드들은 모두 unmount
          2. 새로운 DOM의 root 이하 노드들은 mount
          3. 이전 트리와 연관된 state들이 사라짐 (unmount 되므로)
        1. root element 타입이 같은 경우
          1. 두 element 속성을 확인하여, 변경된 속성들만 갱신
          2. state 는 유지됨
 
  • root element 타입이 다른경우 예제 코드
import React, { useEffect, useState } from "react"; const Test = (props) => { const [list, setList] = useState([]); const [toggle, setToggle] = useState(false); useEffect(() => { setList([1, 2, 3]); }, []); const onClick = () => { console.dir(toggle); setToggle(!toggle); }; return ( <section> <button onClick={onClick}>click me</button> {toggle ? ( <div> {list.map((item, idx) => { return <TestElement key={idx} name={"div"} />; })} </div> ) : ( <section> {list.map((item, idx) => { return <TestElement key={idx} name={"section"} />; })} </section> )} </section> ); }; const TestElement = ({ name }) => { useEffect(() => { console.dir(`${name} is Mount`); return () => { console.dir(`${name} is UnMount`); }; }, []); return <div>{name}</div>; }; export default Test;
결과:
notion image
실제로 하위 노드들이 전부 unmount 되고 새 노드가 mount 된다.
 
  • root element 타입이 같은 경우 예제 코드
import React, { useEffect, useState } from "react"; const Test = (props) => { const [list, setList] = useState([]); const [toggle, setToggle] = useState(false); useEffect(() => { setList([1, 2, 3]); }, []); const onClick = () => { console.dir(toggle); setToggle(!toggle); }; return ( <section> <button onClick={onClick}>click me</button> { <div> {toggle ? list.map((item, idx) => { return <TestElement key={idx} name={"true"} />; }) : list.map((item, idx) => { return <TestElement key={idx} name={"false"} />; })} </div> } </section> ); }; const TestElement = ({ name }) => { useEffect(() => { console.dir(`${name} is Mount`); return () => { console.dir(`${name} is UnMount`); }; }, []); return <div>{name}</div>; }; export default Test;
결과:
notion image
내부적으로 엘리먼트를 재활용 하는 듯 하다. (key 값이 변하지 않았으므로, 실제로 key 값이 변하게 되면 각각 unmount 된다.) 실제로 아래와 같이 key 값을 다르게 준다면 unmount 된다.
 
<section> <button onClick={onClick}>click me</button> { <div> {toggle ? list.map((item, idx) => { return <TestElement key={idx} name={"true"} />; }) : list.map((item, idx) => { return <TestElement key={2 - idx} name={"false"} />; })} </div> } </section>
 
  1. key 속성을 통해 변경될 element 인지 아닌지를 개발자가 구분 할 수 있다.
  • child 노드에 대한 재귀적 처리
    • React 는 단순히 동시에 두 리스트를 순회하고 차이점이 있으면 변경을 생성함
notion image
  • 왼쪽의 경우 아래에서 위로 파싱하며 변경된 점이 third 노드의 추가 라고 인지하겠지만, 오른쪽의 경우 아래에서 위로 파싱하며 변경된 것을 비교하므로 전부 달라졌다고 인지 할 것이다. 사실 리스트의 앞에 push 한 것 뿐인데도 말이다.
  • key 속성
    • React 는 key를 통해 위의 경우의 문제점을 해결했다.
    • React 는 key를 통해 기존 트리와 이후 트리의 자식 노드들을 비교한다.
    • notion image
    • key를 통한 비교를 통해 Connecticut 노드가 새로 추가 되었을 뿐 나머지 두개 노드는 변경할 필요 없다는 것을 알게 되었다.
  • key 정하는 법
    • key는 엘리먼트들의 식별자를 사용 하는 것이 가장 편함
    • key는 형제 노드 사이에서 유일하면 되고, 전역적으로 유일 할 필요 없음
    • 배열의 인덱스를 key로 사용하는 것은 권장하지 않음
      • 목록들이 재배열 (sorting 같은 것) 되는 경우 비효율적으로 동작함
      • 재배열 되지 않는 경우는 사용해도 됨
 

 
결론
  • Virtual DOM 은 React 가 실제 DOM 트리의 효율적인 변경을 위해 메모리 상에만 올려놓은 가상의 DOM 이다.
  • root element 타입이 다르면 이전 트리를 완전히 버린다.
    • 버려진 노드들은 unmount, 새로 변경된 노드들은 mount 된다. 이를 통해 state가 소실 될 수 있다.
  • list node 들을 변경 할 때, key 를 사용하면 react 가 더 효율적으로 DOM 을 변경 해준다.
    • key 는 식별자를 사용하는 것이 제일 편하고, 형제들 끼리만 유일하면 된다.
 

 
참조
 
Critical Rendering Path:
 
Reconciliation:

다음 포스트

icon

JSX

JavaScript
React

이전 포스트

icon

클로저

JavaScript