React 동작 이해하기 - 2

업데이트:

리액트의 엘리먼트 비교 알고리즘

React 는 O(n) 복잡도를 가진 휴리스틱 알고리즘을 구현하였습니다. 이것은 두가지 가정을 기반으로 합니다.

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

리액트의 공식문서에서 설명하고있는 리액트에 적용된 알고리즘을 말하고있는데 그래서 휴리스틱 알고리즘이 무엇인지 찾아보았다.

휴리스틱이란 불충분한 시간이나 정보로 인하여 합리적인 판단을 할 수 없거나, 체계적이면서 합리적인 판단이 굳이 필요하지 않은 상황에서 사람들이 빠르게 사용할 수 있게 보다 용이하게 구성된 간편 추론의 방법 - 위키백과

다익스트라나 동적계획법 브루트포스등 최적의 해를 찾는 알고리즘은 많이 있지만 도로네트워크나, 이런 리액트의 트리노드를 구성하는데에는 노드의 수가 커짐에 따라 검색비용도 비례해서 늘어나기때문에 실제 응용에서 이런 최적의 해를 구하는 알고리즘을 사용하기는 어렵다.

그래서 휴리스틱알고리즘을 사용하는데 이 휴리스틱을 사용하기위해서는 첫번째 과정으로 어떠한 가정이 필요로한다. 예를들어 외판원문제를 예로들 수 있는데 외판원이 방문할 수 있는 여러개의 섬이 있고 이 섬의 거리는 각각 다르게되는데 가장 최소의값으로 모든 섬을 다 돌아야만 하는 상황 이 때 출발섬이 어디냐에따라 그 결과값이 매우 달라지기때문에 출발섬을 지정하는것은 최적의 해를 구하는데에 있어서 매우 중요한 문제이다.

이 외판원문제를 예시로 휴리스틱 알고리즘방식을 적용한다면 만약 섬이 10개라면 그냥 5번째나 6번째의 섬에서 출발하는 방식인거다. 즉 섬이 몇개가 주어지든간에 출발지점에대한 고민이나 최적의 출발점을 찾기위한 알고리즘은 필요없고 그냥 대충 가운데쯤에서 시작하는거다. 리액트에서 이런 일단 대충 이럴거다 라고 시작할 추론이 필요했고 그것이 바로 처음에 가정했던 두가지인것이다.

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

이제 여기서부터 다시 시작을 해보면 리액트에서는 두개의 트리를 비교할때 두 엘리먼트의 루트엘리먼트부터 비교합니다. 이후의 동작은 루트엘리먼트의 타입에 따라 달라집니다.

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

엘리먼트의 타입이 다를경우 리액트에서는 이전트리를 버리고 완전히 새로운 트리를 구축합니다. 트리를 버릴때 이전 DOM 노드들은 모두 파괴되고 컴포넌트 인스턴스는 componentWillUnmount() 가 실행됩니다. 그리고 새로운 노드들이 DOM 에 삽입되고 componentDidMount() 가 실행되고 이전 트리와 연결되어있던 모든 state 는 사라집니다.
이때 루트 엘리먼트하위의 자식컴포넌트도 언마운트되고 state 또한 사라집니다.

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

만약 엘리먼트가 동일하다면 내부 속성중에 변경된 사항만 변경시키고 동일사항은 유지합니다. 컴포넌트가 갱신되면 인스턴스는 동일하게 유지가 되고 state 또한 유지됩니다. 그리고 새로운 엘리먼트의 내용을 반영하기위해 현재 컴포넌트 인스턴스의 props 를 갱신합니다. 이때 componentDidUpdate() 를 호출합니다.
이후 render 메서드가 호출되고 비교알고리즘이 결과를 재귀적으로 처리합니다.

자식에 대한 재귀처리

재귀적으로 자식들에 대해 처리할때는 기본적으로 두 리스트를 동시에 순회하고 차이점이 있으면 변경을 생성합니다.

<ul>
  <li>test1</li>
  <li>test2</li>
</ul>
.....
<ul>
  <li>test1</li>
  <li>test2</li>
  <li>test3</li>
</ul>

만약 자식의 끝에 엘리먼트를 추가하면 두 트리 사이의 변경은 잘 작동합니다. 하지만 위와같이 단순하게 구현하게되면 엘리먼트의 맨 앞에 추가하는경우 성능이 나빠지게됩니다.
이러한 문제를 해결하기위해 key 를 사용할 수 있습니다. 이것은 리액트에서 지원하는 속성으로 자식들이 key 를 가지고 있다면 이 키를 통해 기존트리와 이후 트리의 자식들이 일치하는지 확인합니다. 이 작업으로 인해 자식엘리먼트의 가장 첫번째에 추가하더라도 트리의 변환작업이 효율적으로 수행되도록 할 수 있습니다.

  • key 는 오로지 형제사이에서만 유일하면되고 전역에서 유일할 필요는 없음
  • 배열의 인덱스를 key로 사용할 수 있지만 만약 재배열되는 상황이 있다면 비효율적으로 동작할 수 있다.
  • 인덱스를 키로 사용중인 배열이 재배열 되면 컴포넌트의 state 와 관련된 문제가 발생할 수도있음
  • 컴포넌트 인스턴스는 키를 기반으로 갱신되고 재사용

댓글남기기