[React] React의 작동원리

ds-k.fe·2021년 8월 2일
0

React

목록 보기
4/6

브라우저의 랜더링

Virtual DOM을 사용하지 않았을 때에 브라우저가 랜더링을 하는 과정을 살펴보면,

  1. 랜더링 엔진에서 HTML, CSS 파싱
    1-1. DOM(Document Object Model) 생성 - (HTML을 파싱하여 Object Model 생성)
    1-2. CSSOM(CSS Object Model) 생성 - (CSS 파일과 인라인 스타일을 파싱하여 Object Model 생성)
  2. DOM과 CSSOM을 활용해서 Render Tree를 생성
  3. Layout(flow) - Viewport 내의 각 노드들의 정확한 위치와 크기를 계산
  4. Paint - 요소들을 실제 화면에 그림

의 과정으로 진행이 된다.

이 때, 어떤 인터렉션에 의해 DOM에 변화가 발생하면, render tree를 만드는 과정부터를 다시 반복하게 된다. 즉, 전체의 노드들이 싹다 새롭게 다시 그려지는 것이였다. 이는 DOM 조작에 들어가는 비용이 크다는 것을 의미한다.

또한, 최근에는 페이지를 변경하지 않고 하나에 페이지에서 동적으로 변경이 일어난 SPA(Single Page Application)의 비중이 커지면서 DOM tree를 즉각적으로 변경할 일이 많이 생기게 되어서 Virtual DOM을 활용해 DOM의 랜더링에 들어가는 비용을 줄이는 방식을 사용한 것이다.

요약: Virtual DOM은 실제로 렌더링되지는 않았지만, Real DOM을 반영한 상태로 메모리에 있는 가상의 DOM이다. Virtual DOM의 조작은 브라우저의 랜더링에 영향을 주지 않으면서, 변경사항들을 한번에 묶어서 반영을 한다.

Virtual DOM

Virtual DOM의 작동 순서


React는 Real DOM을 조작하는 방식이 아니라, 가상의 DOM인 Virtual DOM(일종의 복제품)을 두어서

  1. 데이터 변경이 일어난다.
  2. Virtual DOM에 랜더링을 한다.
  3. 업데이트 이전의 Virtual DOM과 현재의 Virtual DOM을 비교하여, 변경된 부분을 반영한다.(재조정)
  4. 변경 사항을 Real DOM에 반영한다.

의 순서로 조작하는 방식으로 구성되어 있다.

재조정(Reconciliation)

React의 render() 메소드는 state나 props가 갱신되면, 새로운 엘리먼트 트리를 반환한다.
이때 방금 만들어진 트리를 기존의 트리와 비교해서, 효과적으로 UI를 갱신하는 방법이 필요하다.
React 공식문서에서의 설명에 따르면,
하나의 트리를 가지고 다른 트리로 변환하기 위한 최소한의 연산 수를 구하는 알고리즘 문제를 풀기 위한 일반적인 해결책들이 있지만, 이런 최첨단의 알고리즘도 n개의 엘리먼트가 있는 트리에 대해 O(n3)의 복잡도를 가진다고 한다.

즉, 1000개의 엘리먼트를 그리기 위해 10억 번의 비교 연산이 필요하다는 것인데, React는 두 가지의 가정으로 O(n) 복잡도의 휴리스틱 알고리즘을 구현했다고 한다.

  1. 서로 다른 타입의 두 엘리먼트는 서로 다른 트리를 만들어낸다.
  2. 개발자가 key prop을 통해, 여러 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 않아야 할지 표시해줄 수 있다.
    (이래서 key 지정하라고 맨날 난리를 쳤구나...)

1. 엘리먼트의 타입이 다른 경우

<div>
  <Something />
</div>

<span>
  <Something />
<span>

이런식으로 루트 엘리먼트의 타입이 다르면, 이전 트리를 버리고 완전히 새로운 트리를 구축한다.
Something의 루트 엘리먼트의 타입이 달라졌기 때문에, Something은 언마운트되고, 새로 다시 마운트가 된다.

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

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

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

같은 타입일 경우, React는 className만을 수정하고, 해당 노드의 자식들을 재귀적으로 처리한다.

3. 자식에 대한 재귀적 처리

DOM 노드의 자식들을 재귀적으로 처리할 때, React는 기본적으로 동시에 두 리스트를 순회하고 차이점이 있으면 변경을 생성한다.

<ul>
  <li>A</li>
  <li>B</li>
</ul>

<ul>
  <li>A</li>
  <li>B</li>
  <li>C</li>
</ul>

이 두개의 트리를 비교할 경우, A와 B가 일치하는 걸 확인하고, C를 추가하게 된다.

그러나, 앞에 추가할 경우 문제가 생긴다.

<ul>
  <li>A</li>
  <li>B</li>
</ul>

<ul>
  <li>C</li>
  <li>A</li>
  <li>B</li>
</ul>

A,B 앞에 C가 들어간 것이지만, 순차적으로 비교하면 C <-> A 비교, B <-> A의 순서가 일어나기 때문에, A,B를 유지하지 않은 상태로 모든 자식을 변경하게 된다.

Key!

이 문제를 해결하기 위해 고안 된것이 key 속성이다. 자식들이 key를 가지고 있다면 트리의 변환 작업이 보다 효율적으로 수행될 수 있다.

<ul>
  <li key="1">A</li>
  <li key="2">B</li>
</ul>

<ul>
  <li key="3">C</li>
  <li key="1">A</li>
  <li key="2">B</li>
</ul>

key를 통해 '3'이라는 key를 가진 C가 추가되었고, '1', '2' key를 가진 엘리먼트는 이동만 하면 되는 것을 알 수 있다.

무엇을 키로 할까?

일반적으로 그리려는 엘리먼트는 식별자를 가지고 있을 것이고, 그대로 해당 데이터를 key로 사용하면 된다.

<li key={item.id}>{item.name}</li>

위의 비교 특성을 고려했을 때, key는 오로지 형제 사이에서만 유일하면 되고, 전역에서 유일할 필요가 없다.
-> 같은 루트 안에서 자식을 비교하기 때문

배열의 인덱스는 권장하지 않는다!

배열의 인덱스를 키로 활용할 수도 있(기는 하)다.

list.map((todo, idx) => <li key={idx}>{todo.name}</li>)

하지만 이 방법은 권장되지 않는다. 왜냐면, 항목들이 재배열될 경우 key가 다시 순차적으로 매겨지기 때문에, 트리의 변환작업이 비효율적으로 일어날 것이기 때문이다.
또한, 컴포넌트의 인스턴스는 key를 기반으로 갱신되고 재사용되는데, 인덱스를 key로 사용하면 항목의 순서가 바뀌었을때 key도 변경기 때문에 state가 엉망이 되거나 의도하지 않은 방식으로 바뀔 수도 있다.
엉망이 되는 예시

레퍼런스

  1. React 적용 가이드 - React 작동 방법
  2. [10분 테코톡] 🥁 지그의 Virtual DOM
  3. React 공식 문서 - 재조정

0개의 댓글