Virtual Dom

골두·2024년 6월 17일
0

Frontend

목록 보기
19/30
post-thumbnail

Virtual Dom이란 UI의 이상적인 혹은 가상적인 표현(변화된 UI의 상태) 만을 메모리에 저장하고 ReactDOM 같은 라이브러리에 의해 실제 DOM과 동기화하는 프로그래밍 개념이다. (재조정 개념)

해당 방식이 리액트에게 원하는 UI의 상태를 알려준다면 DOM이 그 상태와 일치하도록 변경해주는 개발 패턴이라고 볼 수 있다.

재조정 (Reconciliation)

리액트에서 트리 구조의 DOM을 다른 트리로 변환하기 위한 최소한의 연산을 생성하는 알고리즘(가장 최신 및 최적화된)을 이용하게 된다고 한들 O(N^3)의 시간 복잡도를 가지게 된다. (N은 트리 내 요소 갯수)

이 방식을 사용하게 된다고 한들 React에서 사용하게 되면 state의 변경이 있을 때 마다 1000개의 요소가 있다면 10억 번의 비교를 해야한다는 문제가 생기기에 리액트에서는 두가지 가정에 따른 발견적 O(N)의 알고리즘을 활용해 이 문제를 해결한다.

  • 참고: React 16 이상부터는 React Fiber라는 알고리즘을 적용해 기존의 알고리즘에서 발생하는 애니메이팅에 대한 부분을 개선했음

두가지 가정

  • 다른 타입의 두 요소는 서로 다른 트리를 생성한다.
  • 개발자는 key props를 이용해 다른 렌더링 사이에서 안정적인 자식 요소에 대한 힌트를 얻을 수 있다.
    • 우리가 Map같은 동일한 컴포넌트를 여러번 렌더링 할 때 key를 고유값으로 넣어줘야 하는 이유 (DOM 탐색을 위한 고유값 입력)

비교 알고리즘

두 트리를 비교할 때 React는 두 루트 요소부터 비교하게 되는 이 동작은 루트 요소의 타입에 따라 다르게 동작하도록 설계했다.

여기서 말하는 두 루트 요소란 일반 DOM과 변화될 가상 DOM을 말하는 것이다.

다른 타입의 요소

루트 요소가 다른 element type을 가질 때 마다 React에서는 오래된 트리를 해제하고 새로운 트리를 처음부터 빌드하게 된다.

ex) <a><img>같은 태그들이 <div><button> 같이 다른 element type으로 변하게 된다면 무조건 완전히 새로 트리를 빌드하게 된다.

트리를 해체할 때 이전 DOM 노드는 제거되고 컴포넌트 인스턴스는 componentWillUnmount 즉 컴포넌트의 언마운트를 선언하게 된다. 새 트리가 만들어질 때는 새로운 DOM 노드는 DOM에 들어가기 때문에 다시 componentDidMount() 과정을 거치게 된다. 이 과정을 통해 이전 트리와 관련된 모든 state들은 손실되게 된다.

응용?

  • 반대로 추측해본다면 특정 DOM을 강제 업데이트 즉 ReRender를 시키는 방법은 루트 요소를 다른 element Type으로 바꿔버리는 것도 방법이라고 할 수 있다.

같은 타입의 DOM 요소

같은 타입의 두 React DOM 요소를 비교할 때 React는 두 요소의 속성을 보고 같은 DOM을 유지하며 변경된 속성만을 업데이트 합니다.

<div className="before" title="stuff" />

<div className="after" title="stuff" />

위의 두 Element DOM 요소를 비교할 때 className만 수정되었다라는 것을 인지하기 때문에 속성 변경에 대한 DOM 요소를 업데이트 해준다.

위 처럼 DOM 노드를 처리하고 React는 자식 요소에 대해 계속 해당 작업을 반복하게 된다.

inline style...?

<div style={{color: 'red', fontWeight: 'bold'}} />

<div style={{color: 'green', fontWeight: 'bold'}} />

스타일의 경우도 color가 변경되면서 DOM 요소를 업데이트 해주게 된다.

다만 뭔가 이상한 점이 있다. React가 아닌 JS 관점에서 생각해볼 때 JS에서 Object는 싱글톤 패턴 즉 같은 key와 value를 가지고 있는 객체일지 언정 그 객체가 같은 객체는 아니다.

그렇다면 DOM이 업데이트 될 때 style 내부의 객체를 비교하게 된다면 Object.is({color: "red"}, {color: "red"})는 다른 값이기 때문에 새롭게 DOM을 업데이트 하는 요소가 될 수 있다.

밑의 레퍼런스를 읽으면 알겠지만 인라인 스타일은 성능에 영향을 주는 것은 맞고 쓰지 말라고 권고하나 레딧을 중점적으로 본다면 누군가는 편의성 때문에 사용하기도 한다. 어느 누구도 명확하게 어느정도의 성능을 깎아먹는지에 대해 잘 모르기 때문에 논의가 계속 되고 있다라는 것은 실제로 서비스에 영향을 끼칠만큼의 영향을 주지 않는다라는 것이기에 쓰고 싶으면 써도 되나 유지 보수 측면에서는 언젠간 들어내야하는 레거시임을 기억하는 정도로 넘어가도 될 것 같다.

관련 레퍼런스

https://www.reddit.com/r/reactjs/comments/umjqb7/any_real_other_reasons_not_to_use_inline_styles/

https://www.linkedin.com/pulse/stop-using-inline-styles-react-js-azeem-aleem/

https://www.inflearn.com/questions/343506/react-%EC%9D%B8%EB%9D%BC%EC%9D%B8-%EC%8A%A4%ED%83%80%EC%9D%BC-dom-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EA%B4%80%EB%A0%A8%ED%95%B4%EC%84%9C-%EC%A7%88%EB%AC%B8%EB%93%9C%EB%A6%BD%EB%8B%88%EB%8B%A4

같은 타입의 컴포넌트 요소

// TODO: 직접 구현해본적이 없어 추후 구현해보면서 파악해보기

위에는 DOM 요소였다면 이번에는 컴포넌트 요소인데, 컴포넌트 업데이트 시 인스턴스가 동일하게 유지된다면 렌더링 간 state가 유지된다. React에서는 새 요소와 일치하도록 컴포넌트 인스턴스의 props를 업데이트 후 인스턴스에서 componenetWillReceiveProps와 componentWillUpdate를 호출하게 된다 그 후 render() 메소드가 호출되고 비교 알고리즘은 이전 결과와 새로운 결과에 반복된다.

자식에서 반복

기본적으로 DOM 노드의 자식에 대해 반복 시 React는 동시에 두 자식 목록을 반복 후 실행해 차이가 있을 때 마다 변경을 요청하게 되는 구조다.

// AS-IS
<ul>
    <li>1</li>
    <li>2</li>
</ul>

// TO-BE
<ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
</ul>

위의 예시는 문제가 되지 않는다 ul element안의 요소에서 li 요소 중 1,2는 기존과 일치하고 3만 추가되었기 때문에 새로운 트리가 추가되는 정도로 끝날 수 있다. (즉 비교는 배열 처럼 순서대로 하나씩 비교해 다른 값을 찾는 방식이라고 할 수 있다.)

하지만 순서대로 비교한다라는 것은 이런 치명적인 문제를 발생시킨다.

// AS-IS
<ul>
    <li>1</li>
    <li>2</li>
</ul>

// TO-BE
<ul>
    <li>0</li>
    <li>1</li>
    <li>2</li>
</ul>

순차적으로 비교를 한다를 알기 위해 간단하게 코드로 비유해보자면

// 위의 방식
const a = [1, 2];
const b = [1, 2, 3];

// 임의로 넣은거니 그냥 이해해주자...
for (int i = 0; i < b; i++) {
    // 1, 2는 같음을 인지하지만 3의 경우 a는 undefined, b는 3이라 값이 다르기 때문에 업데이트가 일어난다.
    if (a[i] !== b[i]) domUpdate();
}

// 아래의 방식
const a = [1, 2];
const b = [0, 1, 2];

// 임의로 넣은거니 그냥 이해해주자...
for (int i = 0; i < b; i++) {
    // 배열 앞에 값이 추가되었으므로 앞에서부터 비교할 때 모든 값이 다 틀리다.
    // ex. 0 !== 1, 1 !== 2.... 즉 3번 업데이트가 발생하게 된다.
    if (a[i] !== b[i]) domUpdate();
}

해결 방법?

위 방식에 따르면 굉장히 문제가 크다고 할 수 있다. 이런 배열 형태를 사용한다는 것은 무조건 앞이나 중간에 값을 추가하면서 리렌더링이 많이 된다라는 것이니까... 리액트에서는 이 문제를 key라는 것을 이용해 해결한다.

key?

특정 element의 자식 요소가 key를 가지게 된다면 React에서는 오리지널 트리의 자식을 후속 트리의 자식과 비교할 때 key를 사용하게 된다.

위의 비 효율적인 예제에 key를 추가하게 된다면 트리를 효율적으로 변환할 수 있다.

<ul>
  <li key="2015">1</li>
  <li key="2016">2</li>
</ul>

<ul>
  <li key="2014">0</li>
  <li key="2015">1</li>
  <li key="2016">2</li>
</ul>

위 방식으로 key를 넣는다면 트리에서는 값을 비교할 때 저 key를 기반으로 비교하고 key와 key를 매핑하는 방식으로 노드들을 비교하기 때문에 훨씬 더 효율적으로 렌더링이 될 수 있다.

그래서 보통 배열 값을 기반으로 element를 렌더링 할 때 React에서는 이 key를 무조건 적으로 넣어주게 된다. (안 넣으면 리액트에서 경고문이 뜬다)

key를 넣을 때 이 key는 보통 백엔드를 통해 내려주는 값 중 고유한 ID값을 key로 쓰는 경우가 많고 간혹가다 배열의 index값을 key로 주는 경우도 있는데 배열의 index를 key로 주게 된다면 배열의 값이 맨 앞이나 중간에 추가 되는 경우 key 값들이 전부 바뀌게 되는 현상이 발생하기 때문에 데이터 정렬이 일어나는 리스트 컴포넌트에서는 사용하지 않는 것을 권장한다.

profile
나 볼려고 만든 블로그 (블로그 이전: https://goldfrosch.tistory.com/)

0개의 댓글