Skip to content

Latest commit

 

History

History
357 lines (214 loc) · 9.28 KB

재조정 이해하기.md

File metadata and controls

357 lines (214 loc) · 9.28 KB

재조정 이해하기

리액트에서 렌더 사이클은 크게 2가지인 render 단계와 commit 단계가 있다.


Render 단계는 JSX를 자바스크립트 표현으로 HTML 구조로 보여지는 단계 이다. 이것을 Virtual DOM 이라고 한다.

commit 단계는 실제 보여진것을 real DOM에 적용하는 단계이다.


Render 단계

렌더단계는 2가지의 렌더가 있다.

최초 렌더와 리렌더 이다.


최초렌더링은 처음에 애플리케이션이 처음에 실행 되었을때를 말한다.

리-렌더링은 state 또는 props가 업데이트 되었을때를 말한다.


최초 렌더링은 root component부터 시작되어진다.(보통 App 컴포넌트이다.)

그리고 점점 트리 아래로 렌더링을 해나간다.

이후 JSX을 바탕으로 actual DOM 처럼 트리를 만든다.

이것을 Virtual DOM 이라고 한다.


Virtual DOM이 생성되어지면, 리액트는 비교 알고리즘을 통해 actual DOM과 Virtual DOM을 비교한다.


이후 비교한후에 변경 사항 목록을 React는 가지게 된다.

이부분이 아직까지 render 단계가 하는 역활이다.


중요한 부분은 리액트는 Virtual DOM을 비교하고 atual DOM을 만들기 위한 변경사항 목록을 만든다는 점이다.


이점은 부분적으로 변경된 점을 찾고 변경을 실행하는것이 아니라 한번의 과정으로 변경한다는것을 의미한다.

리-렌더링은 비슷하지만, key 속성을 가진것은 다르다.

이때는 모든 컴포넌트를 업데이트 하기위해 확인하지 않는다.

대신에 state나 props가 변경되었을때 리액트는 key 속성이 있는 컴포넌트을 비교한다.


다음으로 렌더함수가 실행되고 Virtual DOM과 atual DOM의 비교 알고리즘에 대해 알아보자.


두 개의 트리를 비교할 때, React는 루트 엘리먼트부터 비교한다. 이후 루트 엘리먼트의 타입에 따라 달라진다.


타입이라는 것은 <div> <Counter> 과 같은 부분을 말한다.


DOM 엘리먼트 타입이 다른경우

두 루트 엘리먼트의 타입이 다르다면, React는 자식 엘리먼트를 확인하지 않고 바로 완전히 새로운 트리를 만든다.


트리를 버릴 때는 이전 DOM 노드들은 모두 삭제한다.

해당 엘리먼트의 아래의 컴포넌트들의 인스턴스는 componentWillUnmount() 가 실행된다.


즉, 모든 인스턴스들이 새롭게 만들어진다.

새로운 트리가 만들어질 때, 새로운 DOM 노드들이 DOM에 삽입된다.

이때 componentDidMount() 가 이어서 실행된다.

따라서 당연히 인스턴스가 사라지고 다시 새로운 인스턴스를 만들었으니 그 아래의 모든 컴포넌트의 state는 사라지고 다시 state가 만들어진다.

<div>
  <Counter />
</div>

<span>
  <Counter />
</span>

위와 같은 경우 Counter 컴포넌트는 사라지고, 다시 마운트 될것이다.


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

만약 같은 타입의 엘리먼트라면, React는 두 엘리먼트의 속성을 비교한다.

동일한 속성들은 유지하고 변경된 속성들만 갱신한다.

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

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

위의 예제에서는 className만 변경하고, title은 그대로 둔다.


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

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

위의 같은 경우도 color만 다르므로 fontWeigth은 변경되지 않고 color만 변경된다.


이렇게 비교한후, React는 해당 노드의 자식들을 위와 같은 비교를 재귀적으로 처리한다.


같은 컴포넌트 타입인경우

컴포넌트가 갱신되면 인스턴스를 다시 만들지 않고 유지된다.

따라서 해당 컴포넌트가 자체적으로 가지고 있는 state는 아예 사라지지 않고 변경된 state로 갈아끼워진다.


하지만 props는 해당 컴포넌트가 자체적으로 가지고 있는 값이 아니기 때문에 props는 인스턴스에서 갱신이된다.

이때 componentDidUpdate를 호출한다.


자식에 대한 처리

이제 방금 루트 엘리먼트를 비교했다.

이후 자식 엘리먼트를 확인하는 과정에서 자식들의 형제요소들이 있을것이다.

이것에 대한 비교 알고리즘에 대해 설명하겠다.


React는 기본적으로 동시에 두 트리를 순회하면서 차이점이 있으면 변경을 한다.

즉, 각각 트리끼리 순서대로 비교하면서 다른점이 있으면 업데이트 한다.

// Virtual DOM
<ul>
  <li>first</li>
  <li>second</li>
</ul>

// Actual DOM
<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>

이때 순차적으로 확인하므로 first와 second는 같다고 확인한다.

그리고 third는 없으므로 추가한다.


이렇게 하는 비교는 성능이 좋지 못하다.

// Virtual DOM
<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

// Actual DOM
<ul>
  <li>Connecticut</li>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

순차적으로 확인하므로 Duke 와 Connetcticut을 비교한다.

Villanova와 Duke를 비교한다.

이후 Villanova가 새롭게 추가된다.


하지만 인간의 시선으로 봤을때 Duke와 Villanova는 지우고 새롭게 만드는것보다 이동시키고 Connecticut이 가장 앞에 추가하면 효율적으로 만드는것이라고 바로 알아 챌것이다.


이런 것을 해결하기 위해 key 속성이 있다.


Keys

만약 자식들이 key 속성이 있다면, React는 key를 통해서 각각의 트리를 비교한다.

// Virtual DOM
<ul>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

// Actual DOM
<ul>
  <li key="2014">Connecticut</li>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

이제 Duke와 Villanova는 새롭게 만들필요가 없다.

key를 통해 비교했으므로 이동만 시킨다.

이후 Connecticut는 앞에 추가한다.


이때 주의해야할 점이있다.

key는 항상 고유해야한다.


그렇지 않으면 올바르지 않은 두 엘리먼트를 비교해서 다르다고 판단해 업데이트를 할것이다.

예를들어 배열안의 인덱스 순서대로 key를 설정했다면 굉장히 좋지않은 효율을 발생시킬것이다.

let arr = [Duke, Villanova, Connecticut];

// Virtual DOM
<ul>
  <li key="1">Duke</li>
  <li key="2">Villanova</li>
  <li key="3">Connecticut</li>
</ul>;

let arr = [Connecticut, Villanova, Duke];
// Actual DOM
<ul>
  <li key="1">Connecticut</li>
  <li key="2">Villanova</li>
  <li key="3">Duke</li>
</ul>;

위의 예제에서 arr가 revers 한뒤, JSX를 만들었다고 했을때,

key를 인덱스로 사용했으므로, Duke를 Connecticut와 비교, Connecticut를 Duke와 비교한다.


이렇게 제대로 되지않은 비교를 하므로 비효율적으로 동작한다.

또한 input의 같은 경우 아예 엉망으로 동작하게 된다.

<tr>
  <td>
    <label>{props.id}</label>
  </td>
  <td>
    <input />
  </td>
  <td>
    <label>{props.createdAt.toTimeString()}</label>
  </td>
</tr>

출처 및 더 자세한 로직 : https://codepen.io/pen?&editors=0010

위의 코드는 한줄의 ID와 input과 시간부분이다.


index를 기준으로 key를 만들었다.

따라서 변경되어진 DOM트리가 이전 DOM트리의 첫번째 엘리먼트를 비교하게 되고 그의 자식 엘리먼트끼리 비교할때 변경된 부분은 업데이트하고 변경되지 않은 부분은 업데이트를 하지않는다.


따라서 input부분은 변경되지 않았으니 그대로 가져와서 사용한다.

따라서 이전 DOM 트리의 input안의 값이 변경되지 않고 그대로 유지한다.


따라서 이렇게 key를 인덱스별로 설정하면 업데이트가 비효율적으로 되어지고 또한 브라우저 렌더링이 원하는대로 동작하지 않을수있다.


Commint 단계

Commit 단계는 render 단계에서 비교 알고리즘을 통해 얻은 변경된 DOM 트리를 이미 React는 가지고 있다.


이것을 바탕으로 real DOM을 실제로 건드리고 수정한다.

이때 모든 엘리먼트를 변경하는 것이아니라, 부분적으로 엘리먼트를 변경한다.


정리

중요한 점은 리액트 컴포넌트는 render 단계만 겪고 commit 단계까지는 가지 않을 수 있다는 점이다.

만약 컴포넌트가 이전DOM트리와 같은 이후 DOM 트리가 같았을때 발생한다.


이런 상황은 자주 일어난다.

예를들어 부모 컴포넌트의 state가 변경되었지만, 자식 컴포넌트의 DOM이 이전과 같을때 일어난다.


이러한 과정을 겪어 리액트는 빠른 성능을 자랑한다.


참고