리액트는 JSX를 기본으로 사용한다.
JSX는 JavaScript 파일 내에서 HTML의 태그 등을 사용할 수 있게 만드는 확장 문법이다.
JSX로 작성한 코드가 어떻게 화면에 나타나는가?
JSX를 사용해 React.Element를 리턴하는 컴포넌트를 출력해보면, JavaScript Object가 출력된다.
JSX를 출력하면 나오는 Object, 즉 UI의 구성을 설명하는 객체가 React Element이다.
클래스형 컴포넌트의 경우 render() 메소드를 통해 나온 결과물이고, 함수형 컴포넌트의 함수의 반환값이다.
어떻게 렌더링할지를 정의하는 함수나 클래스를 의미한다.
실제로 React Component가 렌더링되어 생성된 구체적인 객체를 의미한다.
컴포넌트의 상태, 생명주기가 관리되는 독립적인 객체를 의미한다.
React의 목표는 React Element 들의 Tree 구조를 만들어, 실제 DOM에 반영하는 것이다.
React Element는 단순 Object이기 때문에, 이 작업이 매우 빠르게 이루어진다.
이때, 이 React Element들로 이루어진 Tree를 Virtual DOM이라고 하며, 메모리 영역에 저장된다.
처음에는 해당 트리를 전부 DOM에 그려내지만, 이후에는 그렇지 않다.
DOM에 실제로 그리는 비용은 메모리단에서 JavaScript Object 를 조정하는 비용에 비해 매우 비싸다.
하지만, 특정 부분은 다시 렌더링 해야할 필요도 있다.
React 는 이렇게 다시 렌더링 해야할 부분을 찾기 위해 아래 두가지 가정을 한다.
key
가 변경되면 React는 해당 Element를 이전과 다른 새로운 요소로 인식하고, 새롭게 마운트한다.key
의 존재 자체만이 아닌, 같은 위치에서의 key
가 다르다면, 두 요소가 서로 다르다고 판단한다.key
가 어떤 요소를 지칭하는지에 따라 결정된다.위 Diffing 알고리즘을 통해 변경된 부분을 찾아내, 실제 DOM에 최소한의 작업만 적용할 수 있게 한다.
여러 상태 변경이 일어난 후, React는 한번에 변경사항을 찾아내고, 실제 DOM에 반영한다.
즉, DOM 조작 횟수를 줄이는 최적화가 되어 있다.
React는 자체적으로 브라우저의 DOM이나 네이티브 UI를 직접 다루지 않는다.
대신, React Core 와 Renderer(react-dom, react-native)로 역할을 분리해놓았다.
React Core는 UI의 구조, 상태 관리, 그리고 컴포넌트의 생명주기와 관련된 로직을 담고 있다.
순수한 JavaScript 라이브러리로 특정 플랫폼에 의존하지 않는다.
앞서 살펴본, Reconciliation 과정을 통해 가상 DOM을 업데이트한다.
또, 이전 상태와 새로운 상태의 차이를 계산해 어떤 부분이 변경되어야 하는지를 결정한다.
setState, Hooks 등의 요청이 발생하면, React Core에서 여러 업데이트를 모아서 한번에 처리한다.
이를 통해 렌더링을 줄이고 성능을 최적화한다.
React Core가 생성한 가상 DOM을 실제 사용자 인터페이스로 변환하는 역할을 한다.
브라우저 환경에서는 DOM API를 사용해 실제 HTML 요소를 생성하고 업데이트한다.
모바일 환경이라면, 네이티브 UI 컴포넌트를 생성해, iOS, Android의 네이티브 뷰로 렌더링한다.
React 16 이전의 Reconciler 알고리즘은 스택 구조로 이루어져 있었다.
하나의 스택에 작업이 쌓이고, 동기적으로 작업이 이루어진다.
이 스택 기반 Reconciler의 특징은 다음과 같다.
이러한 특징으로, 다른 우선순위가 높은 작업을 먼저 처리할 수 없고,
중단하고 싶어도 중단할 수 없다.
즉, 앱이 무반응 상태가 되거나 프레임 드랍이 발생할 수 있다. 아래에서 확인할 수 있다.
https://claudiopro.github.io/react-fiber-vs-stack-demo/
이를 개선하기 위해 Fiber가 등장한다.
React Element가 JavaScript Object인 것 처럼, Fiber 역시 JavaScript Object다.
Fiber는 애니메이션과 반응성에 초점을 두고 아래와 같은 특징이 있다.
Fiber는 React의 작업 단위로서, 각 Fiber 노드에 대해 렌더링 작업(즉, reconciliation)을 진행한 후, "완료된 작업" 형태로 준비되면 commit 단계에서 실제 DOM에 반영된다.
즉, React는 두 단계로 작업을 수행한다.
Fiber는 작업의 단위다.
상태 변화, 생명주기 메소드의 호출, DOM 조작 등 모두 작업이고, 즉시 혹은 미래에 실행될 것이다.
Time Slicing을 이용해 위 작업을 작은 chunk단위로 나눌 수 있다.
우선순위가 높은 작업은 requestAnimationFrmae()
을 이용해 빠르게 실행하도록 스케줄링할 수 있고,
반대로 낮은 작업은 requestIdleCallback()
을 이용해 스케줄링할 수 있다.
Fiber와 Element 모두 JavaScript Object 이므로, 굉장히 유사하다.
실제로, Fiber는 Element로부터 생성되는 경우가 많고, 많은 속성을 공유하기도 한다.
하지만 Element가 매번 새로 생성되는 것과 달리, Fiber는 최대한 많이 재사용된다.
Element는 UI를 묘사하지만, Fiber는 상태, 생명주기 메서드, hook 들까지 관리하는 역할을 한다.
두 개의 Tree가 존재한다.
현재 화면에 보이는 current
트리, 즉, DOM 과 동기화 되어 있는 current
트리와
실제 비동기 작업이 반영된 workInProgress
트리가 있다.
비동기 작업들이 모두 완료되면 current
와 workInProgress
의 포인터를 변경하는 방식으로 작업을 수행해 나간다.
이때 중요한 점은 일반적인 트리 탐색처럼 재귀로 동작하는 것이 아닌 하나의 While Loop 으로 동작한다.
하지만, 트리를 스왑하는 것만으로는 모든 작업을 해결할 수 없다.
동기적으로 작업을 처리하는 commiit 단계에서도 DOM 조작, 특정 생명주기 메서드의 처리 등이 필요하다.
Render 단계에서는 Fiber Tree만 만들어내는 것이 아니라 Effect 목록을 만들어낸다.
Effect는 DOM 조작, 특정 생명주기 메소드의 호출등을 의미한다.
이 작업들은 다른 Component에 영향을 줄 수 있기 때문에 render 단계에서 실행될 수 없다.
Commit 단계에서, React는 모든 Effect를 확인하며 Component Instance에 반영한다.
이 변화들은 화면에 반영되어야 하기 때문에, 동기적으로, 하나의 연속적인 변경사항으로 이뤄진다.
즉, render 단계에서 생성한 Effect 목록에 의해 결정된다.
Fiber Tree는 두 개다.
alternate
속성은 Fiber Tree의 반대 Fiber 요소를 가리킨다.
이를 통해 Fiber 의 재사용을 극대화한다.
업데이트가 발생하면, React는 current
트리의 각 Fiber 노드에 대응하는 workInProgress
Fiber를 준비한다.
이미 alternate
가 있고, 변경이 없다면, 해당 Fiber를 그대로 사용하고, 변경이 있다면, 변경이 필요한 부분에 한해 복사를 한다.
항상 깊은 복사가 이루어지지 않는다.
주의할 점은 두 Tree의 Fiber가 서로를 가리킨다는 점이다.
이전 상태를 가리키는 것이 아닌, 두 Fiber가 서로 번갈아가며 current
와 workInProgress
역할을 하는 것이다.
React 소스 코드의
ReactFiber.js
362번째 줄에서 확인할 수 있다.
이를 통해 Fiber의 재사용을 극대화하면서도, 필요한 경우만 업데이트를 수행하게 된다.
React Fiber의 도입으로 활용할 수 있는 기능들이다.
Concurrent Mode에 대해서는 더 자세히 살펴볼 예정이다.
다음엔 컴포넌트가 업데이트 되는 과정을 조금 더 살펴볼 예정이다.
How Does React Actually Work? React.js Deep Dive #1
How Does React Actually Work? React.js Deep Dive #2
React 파이버 아키텍처 분석