Reconciliation
Reconciliation프로세스에 대해 기술할 페이지. 어렵지만 이해하면 어디가서 리액트 작동방식 안다고 떠들고 다닐 수 있다
Last updated
Reconciliation프로세스에 대해 기술할 페이지. 어렵지만 이해하면 어디가서 리액트 작동방식 안다고 떠들고 다닐 수 있다
Last updated
리액트는 선언적 프로그래밍(declarative programing)을 지향하기 때문에 사용하기는 쉽지만 실제 작동방식에 대해서는 파악하기 어렵습니다. 빠른 개발이 목적이라면 몰라도 되겠지만, 실제 작동방식에 대해서 파악하지 못한 채로 기능만 만져보는건 리액트를 제대로 공부했다고 할 수 없으므로 리액트는 내부적으로 어떤식으로 작동하길래 렌더링이 빠르고 성능이 좋다고 불리는지에 대해 알아보도록 하겠습니다.
리액트는 Real DOM에게 수정사항을 전달하여 렌더링이 이루어지기 전에 Virtual DOM을 활용하여 렌더링상에 불필요한 부분이 있는지 검사하고 그 과정에서 최소한의 변경사항만 찾아낸 후 Real Dom에 전달하여 효율적인 렌더링작업을 수행합니다. 이 프로세스 전체를 reconciliation이라고 부르는데 실제 수행과정에 대해서 하나하나 짚고 넘어가도록 하겠습니다.
리액트는 Real Dom을 본딴 Virtual Dom을 생성합니다. 단 이 Virtual Dom은 항상 1개만 존재하지 않습니다. 리액트는 Component의 상태가 바뀌었을 때 변경사항을 적용시킨 Virtual Dom을 새롭게 생성합니다. 즉 Component의 상태가 막 바뀌었을 때는 Old Virtual Dom과 변경사항이 반영된 New Virtual Dom 2개가 동시에 존재하게 됩니다. 2개의 Virtual Dom을 비교(diff)하는 하여 변경사항을 찾아내는 알고리즘을 Diffing 알고리즘이라고 부릅니다.
아래 문서에 나와있는 Diff알고리즘의 작동방식에 대해서 정리해 보겠습니다.
Diff는 2가지 상황을 가정하고 상황별로 다른 작동방식을 가진 알고리즘을 구현하습니다.
위 코드에서 <div>
가 CounterParent
컴포넌트에서 root 요소입니다. root요소 아래에는 <Counter/>
컴포넌트가 존재하는데 어떤 상황에 의해 <Counter/>
컴포넌트를 감싸고 있는 root요소가 다음과 같이 변경되는 경우
리액트는 기존 루트요소와 하위 요소들을 전부 삭제한 후 새로운 루트요소와 하위요소를 생성시키는 방식으로 동작합니다.
Real Dom에 존재하던 <div>
노드와 <Counter>
노드는 삭제됩니다.(unmount)
New Virtual Dom에서 변경사항(<span>
노드와 <Counter>
노드)에 해당하는 노드를 새롭게 생성합니다. (mount)
생성된 노드를 삭제된 위치에 추가합니다.
작업 완료후 Old Virtual Dom은 완전히 제거된 후 , New Virtural Dom만 남습니다.
위 처리과정에서 이전 <Counter>
에서 유지중이던 상태(state)값이 있다면 소멸하게 됩니다. 따라서 루트 컴포넌트를 변경할 때는 자식 컴포넌트에게 끼칠 영향도 고려해줘야 합니다.
두 리액트 돔을 비교하는 과정에서 각 요소의 루트타입이 같은 경우 리액트는 두 요소의 속성을 검사합니다. 속성은 className
, style
등을 의미합니다.
위 코드에서 루트는 똑같은 <div>
이지만 className
이 "before"
에서 "after"
로 바뀌는 경우
리액트는 기존 요소는 그대로 유지한채로 className에만 update를 실행합니다. 또 다른 예시를 보자면
다음과 같이 style에서 color만 변경된 경우 기존요소를 유지하면서fontWeight의 변경 없이 color에만 변경을 수행합니다. 이렇게 루트 타입에 대한 변경사항을 반영 했으면 이제 자식 요소들에 대해서도 동일한 검사를 수행합니다.
두 리액트 돔은 자식요소들을 한번에 하나씩 꺼내서 검사를 수행합니다. <ul>
태그의 자식요소로 새로운 <li>
태그가 추가되었다고 가정을 해보겠습니다.
위 코드는 다음과 같은 검사를 수행합니다.
root노드가 같은 경우 이므로 root노드의 속성들이 같은지 검사합니다. 변경된 부분이 있다면 수정을 진행한 후 자식 요소들에 대해서도 동일한 검사를 수행합니다.
Old Virtual Dom의 첫번째 자식요소<li>first-child</li>
와 New Virtual Dom의 첫번째 자식요소 <li>first-child</li>
를 비교합니다. 변경점이 없으므로 넘어갑니다.
Old Virtual Dom의 첫번째 자식요소<li>second-child</li>
와 New Virtual Dom의 첫번째 자식요소 <li>second-child</li>
를 비교합니다. 변경점이 없으므로 넘어갑니다.
마지막 <li>third-child</li>
는 Old Virtual Dom에 존재하지 않는 요소이므로 새롭게 생성하여 Real Dom에 넣어줍니다.
작업이 완료된 Old Virtual Dom은 제거됩니다.
위 작업은 first-child와 second는 그대로 유지한 채 최소한의 변경점만 Real Dom에 반영했으므로 성능이 습니다. 하지만 자식요소가 부모의 맨 뒤가 아닌 장소에 추가된 경우에는 성능이 매우 떨어지는데 아래 예시를 통해 확인해 보겠습니다.
위 코드에서 <li>mkm-kh</li>
요소는 부모의 제일 첫번째 자식으로 추가 되었습니다. 이런 경우 리액트 돔은 Old Virtual Dom에서는 <li>mkm</li>
, New Virtual Dom에서는 <li>mkm-kh</li>
를 두고 비교를 수행하기 떄문에 모든 요소가 바뀌었다고 판단을 하고 변경 및 추가작업을 실행합니다. <li>mkm</li>
와<li>kh</li>
는 사실상 바뀐게 없음에도 불구하고 말이죠.
이러한 문제를 해결할수 있는 방법으로 key
속성을 사용하는 것을 권장하고 있습니다.
리액트는 자식요소들을 검사할때 자식요소에 key속성이 존재하는 경우 key속석을 기준으로 일치하는 요소들끼리 diff알고리즘을 적용합니다. 아래 코드를 통해 확인해 보도록 하겠습니다.
두 리액트 돔은 이제 key값을 기준으로 일치하는 요소들끼리 검사를 수행하게 되었습니다. 즉 Old Dom에서 <li key='1'>mkm</li>
인 요소와 검사할 New Dom의 요소는 마찬가지로 key값이 1인 요소를 검사하게 되는 거죠. 즉 리액트는 key값이 1인 요소와 2인 요소는 위치만 변경되었음을 이해하고 변경 해야할 요소는 <li key='3'>mkm-kh</li>
뿐 임을 알 수 있는 것입니다.
key값에는 해당 자식을 식별할 수 있는 고유한 식별자를 사용하는 것이 가장 좋습니다.
위에서 설명 드린 diff알고리즘은 모든 변경사항을 찾았을 때 Real Dom을 즉시 변경시키지 않습니다. 모든 변경 사항들은 하나의 저장공간안에 쌓아 두었다가 모든 자식요소들에 대한 검사를 마친 후에 일괄 업데이트(batch update)를 수행합니다. 이는 업데이트를 좀더 효율적으로 수행하기 위한 리액트의 처리방식입니다.
개요에 설명했듯 리액트는 선언형 프로그래밍을 지향하기 때문에 각종 리액트 API들을 디자인할때 구현 상세기능들에 대해서는 감춰두었습니다. 이는 개발자가 리액트의 구현 상세 기능에 집중하는 것보다 리액트를 활용하여 화면구현을 하는 것을 더 중요한 가치로 판단했기 때문입니다. 실제로 리액트는 굉장히 버전업이 많이 되고 코드가 많이 바뀌기 때문에 코드 하나하나를 뜯어 보는 건 의미 없는 일일지도 모릅니다.
하지만 정말 리액트를 고성능으로 잘 사용하기 위해서는 상세기능에 대한 이해가 필요합니다. 당연하지만 현재 문서에서 리액트의 모든 Reconciliation과정을 다룬것 도 아니고 리액트의 모든 기능에 대해 설명하진 않았습니다. 다만 진정한 리액트 고수가 되고자 한다면 리액트 메뉴얼을 하나하나 읽어보시고, 최신업데이트내용을 살펴보시고 어떤 기능이 추가되었나 꼼꼼한 확인이 필요할 겁니다. 아래 리액트 공식 커뮤니티 주소를 남기니 기회가 될때마다 잘 살펴보시길 바랍니다.
root노드가 서로 같은지 검사합니다. 서로 다르다면 를 수행합니다.