본문 바로가기
javascript & Node.js/React.js

재조정 (Reconciliation)

by V_L 2020. 10. 29.

출처원본: ko.reactjs.org/docs/reconciliation.html

 

재조정 (Reconciliation) – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

배열의 요소들을 렌더링하다가 성능이 저하되고, 원하지 않는 동작들이 발생할 때가 생겨서 

공식문서를 다시 훑어보다가 좀 더 정확히 알아야 되겠다 싶은 개념이라 글을 작성하기로 했다.

 

 

 

React는 선언적 API 를 제공하기 때문에 갱신이 될 때마다 매번 무엇이 바뀌었는지 걱정할 필요가 없다.

이는 애플리케이션 개발을 쉽게 만들어주지만, React 내부에서 어떤일이 일어나고 있는지 명확히 보이지 않는다.

React의 비교(diffing) 알고리즘을 만들 때 어떤 선택을 했는지 알아보자.

 

 

동기

React를 사용하다보면, 'render() 함수는 React 엘리먼트 트리를 만드는 것' 이라고 생각이 들게 된다.

state나 props가 갱신되면 render() 함수는 새로운 React 엘리먼트트리를 반환한다. 이 때 React는 방금 만들어진 트리에 맞게 가장 효과적으로 UI 를 갱신할 수 있어야 한다.

하나의 트리를 가지고 다른 트리로 변환하기 위한 최소한의 연산 수를 구하는 알고리즘 문제를 풀기 위한 일반적인 해결책들이 있다. 하지만 이런 알고리즘도 n개의 엘리먼트가 있는 트리에 대해 O(n3)의 복잡도를 가진다.

 

만약 React에 이 알고리즘을 적용한다면, 1000개의 엘리먼트를 그리기 위해 10억번의 비교 연살을 수행해야한다.

React는 대신, 두가지 가정을 기반하여 O(n) 복잡도의 휴리스틱 알고리즘을 자체구현했다.

 

1. 서로 다른 타입의 두 엘리먼트는 서로 다른 트리를 만들어낸다.
2. 개발자가 key prop을 통해 여러 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 않아야 할 지 표시해 줄 수 있다.

 

비교 알고리즘 (Diffing Algorithm)

두 개의 트리를 비교할 때, React는 두 엘리먼트의 루트(root) 엘리먼트부터 비교한다.

이후의 동작은 루트 엘리먼트의 타입에 따라 달라진다.

 

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

두 루트 엘리먼트의 타입이 다르면, React는 이전 트리를 버리고 완전히 새로운 트리를 구축한다.

<a>에서 <img>로, <Article>에서 <Comment>로, ...

모두 트리 전체를 재구축하는 경우이다.

트리를 버릴 때 이전 DOM 노드들은 모두 파괴된다.

컴포넌트 인스턴스는 componentWillUnmount() 가 실행된다.

쌔로운 트리가 만들어질 때 새로운 DOM 노드들이 DOM 에 삽입된다. 그에따라 컴포넌트 인스턴스는 componentWillMount() 가 실행되고, componentDidMount()가 이어서 실행된다.

이전 트리와 연관된 모든 state는 사라진다.

 

예를들어, 아래와 같은 비교가 일어나면

<div>
  <Counter />
</div>

<span>
  <Counter />
</span>

이전 Counter는 사라지고, 새로 다시 마운트가 된다.

 

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

같은 타입의 두 React DOM 엘리먼트를 비교할 때, React는 두 엘리먼트의 속성을 확인하여, 동일한 내역은 유지하고 변경된 속성들만 갱신한다.

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

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

이 두 엘리먼트를 비교하면, React는 현재 DOM 노드 상에 className 만 수정한다.

 

 

style이 갱신될 때, React는 변경된 속성만을 갱신한다.

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

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

이 두 엘리먼트 사이에서 변경될때, fontWeight 는 수정하지 않고 color 속성만을 수정한다.

 

DOM 노드의 처리가 끝나면, React는 이어서 해당 노드의 자식들을 재귀적으로 처리해 나간다.

 

 

같은 타입의 컴포넌트 엘리먼트

컴포넌트가 갱신되면 인스턴스는 동일하게 유지되어 렌더링 간 state가 유지된다.

React는 새로운 엘리먼트의 내용을 반영하기 위해 현재 컴포넌트 인스턴스의 props를 갱신한다.

이 때 해당 인스턴스의 componentWillReceiveProps() 와 componentWillUpdate() 를 호출한다.

 

다음으로 render() 메소드가 호출되고 비교 알고리즘이 이전 결과와 새로운 결과를 재귀적으로 처리한다.

 

 

자식의 대한 재귀적 처리

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

 

예를들어, 자식의 끝에 엘리먼트를 추가하면, 두 트리 사이의 변경은 잘 작동할 것이다.

<ul>
  <li>first</li>
  <li>second</li>
</ul>

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

React는 두 트리에서 <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> 종속트리를 그대로 유지하는 대신 모든 자식을 변경한다.

비효율적이다.

 

Keys

이러한 문제를 해결하기 위해, React는 key 속성을 지원한다.

자식들이 key를 가지고 있다면, React는 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>

이제 React는 '2014' key를 가진 엘리먼트가 새로 추가되었고, '2015'와 '2016' key를 가진 엘리먼트는 이동만 하면 되는 것을 알 수 있게 된다.

 

배열에서 key 식별자를 어떤 요소로 주느냐는 고민을 잘 해봐야한다.

재배열 할 필요가 없는 배열이라면 인덱스 값을 key 로 설정해도 괜찮겠지만, 

그게 아니라면 배열 요소의 그 요소만의 primary 값을 설정해줘야 사용자가 예측가능한 렌더링을 할 수 있다.

예시: 

- 재배열되는 배열에서 인덱스를 key값으로 줄 때: codepen.io/pen?&editable=true&editors=0010

- 재배열되는 배열에서 요소의 ID값을 key값으로 줄 때: codepen.io/pen?&editable=true&editors=0010

 

 

고려사항

재조정 알고리즘은 구현상의 세부사항이다.

React는 항상 전체 앱을 재렌더링 할 수도 있지만, 최종적으로 출력되는 결과는 항상 같을 것이다.

여기서 말하는 재렌더링은 모든 컴포넌트의 render를 호출하는 것이다. (React가 언마운트 시키고 다시 마운트 하는게 아님). 즉, 앞서 설명했던 규칙에 따라 렌더링 전후에 변경된 부분만을 적용하는 것이다.

 

React 개발진은 일반적인 사용사례에서 더 빠르게 작동할 수 있도록 계속해서 휴리스틱 알고리즘을 개선하고 있다.

현재 구현체에서는 한 종속 트리가 그 형제 사이에서 이동했다는 사실을 표현할 수는 있지만, 

아예 다른 곳으로 이동했다는 사실은 표현할 수 없다고 한다. 알고리즘은 전체 종속트리를 재 렌더링 하게 된다.

 

React는 휴리스틱에 의존하고 있기에, 휴리스틱이 기반하고 있는 가정에 부합하지 않는 경우, 성능이 나빠질 수 있다.

 

 

정리

1. 알고리즘은 다른 컴포넌트 타입을 갖는 종속 트리들의 일치 여부를 확인하지 않는다.

만약 매우 비슷한 결과물을 출력하는 두 컴포넌트를 교체하고 있다면, 그 둘을 같은 타입으로 만드는것이 더 나을 수 있다. 그리고 실제 사용사례에서 이 가정이 문제가 되는 경우를 아직까진 발견하지 못했다.

 

2. key는 반드시 변하지 않고, 예상 가능하며, 유일해야한다.

변하는 key(Math.random()으로 생성된 값 등)를 사용하면 많은 컴포넌트 인스턴스와 DOM 노드를 불필요하게 재생성하여 성능이 나빠지거나 자식컴포넌트의 state가 유실 될 수 있다.

'javascript & Node.js > React.js' 카테고리의 다른 글

Reactjs + express + webpack4  (0) 2020.09.23
리액트의 기본구조, 그에 따른 디자인 패턴  (0) 2020.05.17
React.js  (0) 2020.05.13