리액트 파이버

💡 리액트 파이버
리액트 파이버는 리액트에서 가상돔과 실제돔을 비교하기 위해 고안된 장치다. 리액트는 실제돔을 조작하는 데 드는 위험(동기적)과 비용(리플로우)을 메모리에서 먼저 수행함으로써 우회한다. 트리를 구성하기 위해 먼저 파이버 객체를 만드는데, 이 자바스크립트 객체는 트랜스파일링 된 jsx값을 읽으며 다양한 프로퍼티를 가지고 생성된다. 때문에 리렌더링이 일어나 새로운 jsx가 반환될 때마다 파이버 노드도 업데이트됨을 짐작할 수 있다. 이렇게 파이버 트리가 업데이트 되고 나면 이전의 파이버 트리와 비교, UI적인 차이가 발생했는지 확인한다. 만약 인정되면 실제돔을 업데이트한다. 리액트는 이로써 여러 번 일어날 브라우저 리플로우를 획기적으로 줄인다.
💡 파이버 트리
파이버는 하나의 리액트 요소에 1대1로 매칭되는 태그를 가지고 있다. 그런데 이게 리액트 요소와 일견 흡사해서 중복 아니야? 생각이 일기도 한다. 하지만 리액트 요소는 렌더링마다 재생성되는 데 반해(= jsx는 렌더링마다 다시 그려진다), 파이버는 재활용된다. 파이버는 변경이 생긴 정보만 수정한다. 또한 파이버는 단순히 요소에 대한 정보만 가지는 것이 아니라 컴퍼넌트에서 사용된 상태, 훅, 라이플사이클 등 더 복잡한 내용을 가지고 있다. 이ㅜ러한 파이버들이 모여 하나의 트리를 형성하게 된다.
💡 두 개의 파이버 트리
그러나 파이버 트리는 하나가 아니라 두 개다
- 작업 중인 트리
- 작업을 마친 트리
첫번째 트리는 일어나는 변화가 실질적으로 그려지는 캔버스 트리
이고, 두번째 트리는 이를 찍어 보존하는 스냅샷 트리
다. 공식적인 용어는 아니지만 그렇게 이해하는 쪽이 나는 편했다. 그렇다고 둘이 완전 별개는 아니다. 스냅샷 트리는 사실 캔버스 트리에서 실질적 변화가 인정되면 그를 포인터만 변경해 자신의 이름으로 보관할 뿐이다.
그런데 이 개념은 낯선 것이 아니다. 저번 three.js 포스팅1에서 나는 FBO(fake buffer object), 즉 비싼 렌더링을 값싸게 처리하는 가짜 버퍼 환경에 대해 공부했는데, 사실 리액트의 중추인 가상돔이 그러한 이중 버퍼링 기법의 소산 또는 연장이었던 것이다. 리액트를 안다고 생각했으면서 이를 연관짓지 못했으니 애석한 일이 아닐 수 없다
리액트는 업데이트를 인식하면 이전 스냅샷을 가져와 새로운 캔버스 트리의 토대로 사용한다. 즉 캐시된 트리를 바탕으로 실제 변경된 부분만 값을 수정한다(최초 렌더링시에는 캐시된 값이 존재하지 않으므로 모든 파이버를 새롭게 만든다). 캔버스 트리에 소묘하는 과정이 끝나면 완료된 작업은 실제돔과 비교/반영되고 또다시 스냅샷 트리로 보관된다.
큰그림은 그려진 것 같다. 파이버는 어떻게 생성되고 어떻게 트리로 뻗는지 마저 정리하고, 방금 설명한 부분도 좀더 요약해 봐야겠다.
💡 파이버의 생성
파이버는 트랜스파일된 jsx를 기반으로 하고 그를 매우 닮았다. 하지만 파이버에는 children이 없고 대신 그를 하나의 child와 sibling으로 파악해 나간다. 가령 이런 구조에서
파이버가 재귀적으로 순회하여 길을 찾는 과정은 다음과 같다.
- A1의 beginWork()이 호출된다.
- A1은 child가 있으므로 B1으로 이동해 beginWork()를 호출한다.
- B1은 child가 없으므로 completeWork()를 호출한다.
- B1은 sibling이 있으므로 B2로 이동해 beginWork()를 호출한다.
- B2는 child가 있으므로 C1로 이동해 beginWork()를 호출한다.
- C1은 child가 있으므로 D1로 이동해 beginWork()를 호4출한다.
- D1은 child가 없으므로 completeWork()를 호출한다.
- D1은 sibling이 있으므로 D2로 이동해 beginWork()를 호출한다.
- D2는 child가 없으므로 completeWork()를 호출한다.
- D2는 sibling이 없으므로 C1로 돌아가 completeWork()를 호출한다.
- C1은 sibling이 없으므로 B2로 돌아가 completeWork()를 호출한다.
- B2는 sibling이 있으므로 B3로 이동해 beginWork()를 호출한다.
- B3는 child가 없으므로 completeWork()를 호출한다.
- B3는 sibling이 없으므로 A1으로 돌아가 completeWork()를 호출한다.
- A1은 sibling이 없으므로 completeWork()를 호출한다.
- root의 모든 경로에 이 과정을 거치면 파이버 트리가 완성된다.
최초 렌더링에 이런 과정을 가쳐 하나의 파이버 트리가 완성된다. 여기서 상태 변화가 일어나면 해당 컴퍼넌트들은 리렌더링되고, 파이버는 새로 재실행된 이 컴퍼넌트들의 정보만 업데이트 한다. 나머지는 처음의 파이버 트리를 재활용한다. 이것이 캔버스 트리
의 작업 과정이다. 그다음 이어지는 과정은 reconciliation이라 불리는 재조정 또는 화해의 절차로 이를 이전의 스냅샷 트리와 비교한다. {state, props, key}
에서 변화가 확인되면 낙찰이다. 그럼 실제 DOM을 업데이트 하고, 만들어진 트리를 새 스냅샷 트리로 삼는다(=복제한다).
이 과정이 반복된다. 리액트의 최적화 프로세스다.
💡 정리: 리액트의 일반적인 렌더링 프로세스
- 루트 컴퍼넌트부터 업데이트가 필요한 컴퍼넌트를 찾아 내려간다
- 변경이 일어났다고 체크된 컴퍼넌트들을 찾는다
- 해당 컴퍼넌트를 재실행한다(클래스 컴퍼넌트의 경우 render 메소드를 재실행)
-
그 결과물인 return 값을 저장한다
- jsx로 구성된 이 값을 js(createElement)로 트랜스파일한다.
- 리액트 파이버가 이를 토대로
{ type, props, key, tag, child, state, sibling, index, ... }
등 다양한 프로퍼티를 가진 파이버 노드 객체를 생성한다. - 여기서 jsx의 모든 요소는 파이버 노드와 1대1로 대응한다.
-
업데이트 된 새로운 파이버 트리와 이전의 스냅샷 트리를 비교한다.
-
{ state, props, key }
에서 변화가 확인되면 달라진 거라 판단, 실제 DOM을 업데이트 한다. 그리고 이 새 결과물을 스냅샷 트리로 둔다(포인터만 이동). -
useLayoutEffect가 동기적으로 실행된다
- 완료될 때까지 browser block.
- 따라서 처리가 길어지는 작업 금지!
- 훅 안에서 setState가 실행되면 뒤이은 작업, 즉 useEffect를 flush하고 다음 렌더 단계로 돌입한다. 페이스북에 따르면 이는 일반적인 최적화 프로세스를 벗어나는 움직임이다. 따로 다룰 예정.
-
브라우저의 리페인팅이 실행된다
-
useEffect가 비동기적으로 실행된다
-
이펙트들에서 스케쥴링 된 상태 업데이트 큐가 실행된다.
-
리렌더링
관련문서
Footnotes
Comment ?
▾ Comment