[React] Virtual DOM과 재조정(Reconciliation)

박수현·2023년 5월 11일
2
post-thumbnail

🧑‍💻 React 재조정(Reconciliation)

  • 재조정의 개념을 이해하기 위해서는 가상 돔(Virtual DOM)의 개념에 대한 이해가 필요하기 때문에, 가상 돔의 개념이 헷갈린다면 이전 포스팅을 읽고 오는 것을 추천합니다!

개요

  • React는 선언적 API를 제공하기 때문에 갱신이 될 때마다 무엇이 바뀌었는지 고려할 필요가 없습니다.

  • 리액트에서는 내부적으로 비교(diffing) 알고리즘으로 시간 복잡도 O(N)을 가진 휴리스틱(heuristic) 알고리즘을 구현해서 적용합니다.

  • 따라서 리액트는 비교 알고리즘을 통해 컴포넌트의 업데이트를 예측 가능하게 만들고 빠른 앱을 만들 수 있습니다.

잠깐! 선언적 API란?
우리가 리액트에게 원하는 UI의 상태를 알려주면(선언적), 그 상태를 기반으로 DOM이 자동적으로 업데이트가 되는 것입니다.
예: JSX 문법을 기반으로 컴포넌트를 작성하면 렌더링은 React에서 알아서 처리해줍니다.


  • 리액트는 다음 두 가지 가정을 기반으로 휴리스틱 알고리즘을 구현하였습니다.
1. Two elements of differnt types will produce different trees.
 : 서로 다른 타입을 가진 두 엘리먼트는 다른 트리를 만들어 낸다.

2. The developer can hit at which child elements may be stable across different renders with a key prop.
 : 개발자가 key prop를 통해 자식 엘리먼트의 변경 여부를 표시할 수 있다.

🧑‍💻 Virtual Dom 이점

스크린샷 2022-10-04 오후 5 48 05

  • React에서 state나 props가 갱신되면 render() 함수가 호출되어 새로운 엘리먼트(Virtual DOM) 트리를 반환합니다.

  • 이때, 효과적으로 UI를 갱신하기 위해서 기존의 오래된 엘리먼트(Old Virtual DOM) 트리와 새로운 엘리먼트(New Virtual DOM) 트리를 비교해 차이점을 찾아내고, 변경된 부분만 실제 돔(Real DOM)에 반영합니다.

    즉, Virtual DOM은 DOM이 변경될 때마다 전체 DOM을 Reflow하는 것이 아니라 비교를 통해 한번만 Reflow를 수행함으로써 부하를 줄입니다.


🧑‍💻 비교(diffing) 알고리즘

  • 리액트는 두 (old, new) Virtual DOM 트리를 비교할 때, 루트부터 비교합니다. 이후의 동작은 루트 엘리먼트의 타입에 따라 달라집니다.

엘리먼트 타입이 다를 경우

// div와 p는 타입이 다르기 때문에 div가 제거되고 p태그와 그 하위 엘리먼트가 추가됩니다.
<div>
  <Counter />
</div>

<p>
  <Counter />
</p>
  • 위 예제처럼 두 Virtual DOM 트리의 루트 엘리먼트 타입(div, p)이 다르면, 리액트는 이전 트리를 완전히 버리고 새로운 트리를 구축합니다.
  • 트리를 버릴 때 이전 DOM 노드들은 모두 파괴됩니다. 이때 componentWillUnmount()가 실행됩니다.
  • 새로운 DOM 노드들이 DOM에 삽입될 때는 componentWillMount()가 실행되고, componentDidMount()가 이어서 실행됩니다.
    • 이전 트리와 연관된 모든 state는 사라집니다. 또한, 루트 엘리먼트 아래의 모든 컴포넌트도 언마운트되고 그 state도 사라집니다.
    • 위 예제에서는 Counter가 사라지고 다시 마운트가 될 것입니다.
* componentWillUnmount() : DOM 노드 파괴될 때 실행.
* componentDidMount() : 새로운 DOM 노드가 삽입된 후에 실행.
componentWillMount() : 새로운 DOM 노드가 삽입되기 전에 실행.
  • 🙆🏻‍♂️ 여담으로 componentWillMount는 레거시 코드이며 현재는 피해야되는 코드입니다.
    • componentWillMount는 주로 브라우저가 아닌 서버사이드 환경에서 호출하는 용도로 사용했었으며, 더 이상 필요없게 되어 리액트 v16.3에서 deprecated 되었습니다.

DOM의 엘리먼트의 타입이 같은 경우

  • 같은 타입의 두 React DOM 엘리먼트를 비교할 때는 두 엘리먼트의 속성(Attribute)을 확인하여, 동일한 속성은 유지하고 변경된 속성들만 갱신합니다.
// className 변경
<div className="before" title="stuff" />
<div className="after" title="stuff" />
  • 위 예제에서 두 엘리먼트를 비교하면 React는 현재 DOM 노드 상에 className만 수정합니다.
// style 속성의 color 변경
<div style={{color: 'red', fontWeight: 'bold'}} />
<div style={{color: 'green', fontWeight: 'bold'}} />
  • 위 예제에서 React는 fontWeight는 수정하지 않고 color 속성만 수정하였습니다.
  • DOM 노드의 처리가 끝나면 React는 이어서 해당 노드의 자식들을 재귀적으로 처리합니다.

같은 타입의 Component Element

  • 컴포넌트가 갱신되면 인스턴스는 동일하게 유지되어 렌더링 간 state가 유지됩니다.
  • 리액트는 새로운 엘리먼트의 내용을 반영하기 위해 현재 컴포넌트 인스턴스의 props를 갱신합니다.
  • 이때 해당 인스턴스의 componentWillReceiveProps, componentWillUpdate, componentDidUpdate를 호출합니다.
  • 다음으로 render() 메서드가 호출되고 비교 알고리즘이 이전 결과와 새로운 결과를 재귀적으로 처리하게 됩니다.
* componentDidUpdate() : 컴포넌트가 업데이트된 후 호출
* render() 메소드 호출 후 이전과 비교하여 재귀적으로 처리

componentWillReceiveProps() : props 갱신 전 호출
componentWillUpdate() : 컴포넌트가 업데이트되기 전 호출
  • 🙆🏻‍♂️ 여담으로 componentWillReceiveProps와 componentWillUpdate는 레거시 코드이며, 현재는 피해야되는 코드입니다.

🧑‍💻 자식에 대한 재귀적 처리

  • DOM 노드의 자식들을 재귀적으로 처리할 때, React는 기본적으로 두 리스트를 순차적으로 순회하고 차이점이 있으면 변경합니다.
  • 예를 들어, 아래 예제와 같이 자식의 끝에 엘리먼트를 추가하면 순차적으로 비교하기 때문에 두 Virtual DOM 트리 사이의 변경은 잘 작동될 것입니다.
// 효율적 예시
<ul>
  <li>first</li>
  <li>second</li>
</ul>

<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>
  • 리액트는 두 트리에서 <li>first</li>가 일치하는 것을 확인하고, <li>second</li>가 일치하는 것을 확인합니다. 그리고 마지막으로 <li>third</li>를 트리에 추가합니다.
  • 하지만, 아래 예제와 같이 리스트의 맨 앞에 엘리먼트를 추가하는 경우에는 성능이 좋지 않습니다.
// 비 효율적 예시
<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

<ul>
  <li>Connecticut</li>
  <li>Duke</li>
  <li>Villanova</li>
</ul>
  • React는 <li>Duke</li><li>Villanova</li> 종속 트리를 그대로 유지하는 대신 모든 자식을 변경합니다. 이러한 비효율은 문제가 될 수 있습니다.

Key

  • 위와 같은 문제를 해결하기 위해 리액트는 Key를 지원합니다! 자식들이 key를 갖고 있다면 리액트는 key를 통해 기존 트리와 이후 트리의 자식들이 일치하는지 확인합니다.
  • 예를 들어, 비효율적 예시에 key를 추가하여 트리의 변환 작업이 효율적으로 수행되도록 개선할 수 있습니다.
// 비효율적 예시에 key 추가
<ul>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

<ul>
  <li key="2014">Connecticut</li>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>
  • 2014 key를 가진 엘리먼트가 새로 추가되었고, 2015, 2016 key를 가진 엘리먼트들은 그저 이동만 하면 됩니다.
  • key의 값으로는 unique한 값을 넣어줘야 합니다. 최후의 수단으로 index를 넣어 줄 수 있지만 재배열되는 경우엔 비효율적으로 동작하게 됩니다.

글을 마치며

  • 재조정을 고려하여 React 환경에서 더 효율적으로 개발하는 방법에 대해서 정리해보고자 합니다.
  1. 두 컴포넌트가 교체되는 상황에서는 그 둘을 같은 타입으로 만드는 것이 더 효율적입니다.
  2. key값을 선정할 때는, 형제 사이에서만 유일하면 되며 전역에서 유일한 value 일 필요는 없습니다.
  3. 배열의 인덱스는 key로 사용하지 않는 것이 좋습니다. 항목들이 재배열되면 key값이 변경될 수 있기 때문입니다.
  4. key는 변하지 않고, 예상이 가능하면서 유일해야 합니다.

🧑‍💻 참고 문서

profile
반갑습니다. 꾸준함과 글쓰기를 좋아하는 프론트엔드 개발자입니다. 블로그를 https://enjoydev.life로 이전했습니다 😀

0개의 댓글