목차
소개
2017년 9월 26일 React v16.0의 공식 배포가 있었는데, 이번 버전의 가장 큰 업데이트는 React의 렌더링 속도 개선이 아닐까 싶습니다. React Fiber 프로젝트를 통해 React App의 속도 개선을 하였는데 도대체 React Fiber 어떤 프로젝트였고 어떻게 속도 개선을 하였는지 얘기하고자 합니다.
기존의 문제점
React Component는 아주 간단한 원리를 가지고 View를 업데이트 해줍니다. 부모 Component들로 받는 Prop 혹은 자기 자신이 갖고있는 State가 업데이트 될 경우 자기 자신과 그 자손들을 새로 렌더링 해줍니다.
그런데 만약 한 Component가 수 많은 자손 Component들을 갖고 있는 상황에서 Prop 혹은 State의 값이 업데이트 될 경우 문제가 생기게 됩니다. 이 경우 해당 Component는 수 많은 자손 Component들을 재귀적으로 순회하며 새로 렌더링해주어야 되는 상황에 놓입니다.
더욱이 이 작업은 Browser가 처리해야하는 모든 작업들 DOM, CSS, JavaScript와 같은 작업의 중간에 처리되야 되므로 Browser에게는 큰 부담감을 떠안겨 줍니다.
React Fiber
“Fiber reconciler’s main goal is to split interruptible work into chunks with the ability to assign priority to different types of updates” - Giamir Blog - What is React Fiber “Fiber reconciler의 주요한 목표는 작업을 interruptible한 작업을 여러개로 분할하고 다른 타입의 업데이트들에 관해 우선순위를 매길수 있는 능력을 주는것입니다.”
React 팀은 위와 같은 문제를 풀기위해 프로세스를 Interruptible한 작은 단위의 일들로 쪼개고, 특정 업데이트 작업에 우선순위를 놓을 수 있게 해줍니다.
이를 바탕으로 하나의 큰 Component의 업데이트가 있더라도 중간중간에 다른 작업들을 Main thread가 처리할 수 있고 스케쥴링을 통해 작업의 우선순위를 정할 수 있도록 해준 것이죠.
“It (Fiber) makes it possible for the main thread to compute a little part of the tree and then come back up and check to see if it has other work to do” - Lin Clark - A Cartoon Intro to Fiber - React Conf 2017 “이것(Fiber)는 메인 쓰레드가 Tree의 작은 일부분을 계산한 뒤 돌아와 더 해야할 다른 일이 있는지 확인할 수 있도록 만들어주었습니다.”
우선, 위와 같은 목표를 달성하기 위해 React Fiber는 총 두 단계의 Phase로 프로세스를 나누었습니다.
- Render Reconciliation
- Work-in-Progress Tree를 생성.
- State와 Prop의 변화에 따라 업데이트가 되야만 할 component를 알아내는 단계.
- Interruptible
- Commit
- 실제 DOM element들을 업데이트하여 DOM에 반영.
- Uninterruptible
Render Reconcilation Phase에서는 Component의 형제 자식 Component들의 Data structure를 담고있는 Fiber tree라고 하는 것을 이용합니다. 참고로, Fiber tree는 Component를 처음 Rendering할 때 만들어집니다.
이 단계에서는 이 Fiber tree들의 각각의 Component들을 돌며 똑같은 형태의 Work-in-Progress Tree를 생성해줍니다. 이 때, 만약 변화가 검사중인 Component가 UI 업데이트가 필요할 경우 태그를 달아주고 그렇지 않을 경우 단순히 clone하여 Work-in-Progress Tree를 만들어줍니다.
이 단계에서의 큰 특징은 각각의 Component들에게 변화가 있는지 없는지 검사를 하며 현재 작업에 할당된 시간이 얼마나 남았는지 그리고 다른 우선순위가 높은 작업이 없는지 지속적으로 검사를 하는 것입니다. 만약 할당된 시간을 모두 소진하거나 다른 우선순위가 높은 작업이 들어왔을 때에는 현재 작업의 상태를 저장하고 다른 작업에 프로세스를 양보하게 됩니다.
이러한 일렬의 과정을 모두 거쳐 Work-in-Progress Tree가 구성이 완료되면, pending commit 상태로 변화하고 두 번째 Phase인 Commit 단계에 들어가게 됩니다.
Commit Phase에서는 태그가 된 Component들을 일괄적으로 업데이트하여 DOM에 적용하는데, 이 때 중간 과정에서 Interrupt가 발생하여 일부만 업데이트 된 것과 같은 일관성 없는 UI가 렌더링되는 것을 막기위해 Uninterruptible하다고 하네요.
“With these priorities, what we want to have happen is for higher priority work to jump in front of low priority work even if the lower priority work has already been started” - Lin Clark - A Cartoon Intro to Fiber - React Conf 2017 “이러한 우선순위를 통해 우리가 원한 것은 우선순위가 낮은 작업이 이미 시작됐을지라도 우선순위가 높은 작업이 우선순위가 낮은 작업 앞에 위치할 수 있게 만드는 것입니다.”
위와 같이 Phase를 분리하고 Interruptible한 작업을 잘게 쪼갠 이점을 기반으로 Fiber는 어떠한 특정 UI 업데이트에 관해 높은 우선순위를 줄 수 있게 됐습니다. 이 때 높은 우선순위를 주기 위한 새로운 함수가 바로 ReactDOM.unstable_deferredUpdates()
입니다. setState()
구문을 주어진 함수 안쪽에서 콜을 하게 되면 해당 state 업데이트로 인한 UI 업데이트를 우선 순위로 작동하게 만들어 줄 수 있습니다.
handleUpdate() {
ReactDOMFiber.unstable_deferredUpdates(() =>
this.setState(prevState => ({ count: prevState.count + 1 }));
);
}
Example
React Fiber vs Stack demo는 React Fiber를 기존 버전과 업데이트 이후 모두의 예시를 보여주며 퍼모먼스 업데이트 예시를 현재 제공해주고 있습니다.
참고로, 위의 Git 코드의 Fiber 예시에서 ReactDOM.unstable_deferredUpdates()
부분을 없애고 setState()
를 실행할 경우 React Fiber에서 우선순위가 어떠한 이점을 가져다 주는지 확인할 수 있으니 시간이 괜찮은 분들은 꼭 한 번 시도해보길 바랍니다.
// 위 코드에서 아래와 같이 바꾸면,
// 모든 Component들의 숫자를 업데이트할 때 일정한 Delay가 발생하는 것을 발견할 수 있습니다.
tick() {
// ReactDOMFiber.unstable_deferredUpdates(() =>
this.setState(state => ({ seconds: (state.seconds % 10) + 1 }))
// );
}
주의할 점(#caution)
위와 같은 업데이트를 통해 React App들은 좀 더 좋은 퍼포먼스의 뷰 업데이트를 가질 수 있었습니다. 하지만 이러한 이점과 함께 개발자들의 주의사항들이 몇 가지 생겼습니다.
또 다른 점은 setState()
함수의 Functional programming의 중요성이 부각됐다는 점입니다.
handleTrue() {
this.setState({ flag: true });
}
handleFalse() {
this.setState({ flag: false });
}
만약 위와 같은 두 개의 이벤트가 있다고 가정할 때 handleTrue()
와 handleFalse()
가 동시 여러번 발생할 경우 추후의 flag
state를 예측하기 힘들다는 게 주요한 점입니다. 왜냐하면 React Fiber를 통해 비동기적 실행이 가능해져 어느 이벤트가 가장 마지막에 발생되는지 예측이 불가능하기 때문이죠.
이에 따라 React v.16.0 이후에는 next state에 관한 값을 직접 Object 형식으로 전달하기보다 이 다음 state는 어떻게 update될지 Function 자체를 주는 것을 추천하고 있습니다.
handleToggleFlag() {
this.setState(prevState => ({ flag: !prevState.flag }));
}
처음 코드를 위와 같이 바꿀 경우 flag
의 state는 handleToggleFlag()
가 몇 번 불리우냐에 따라 마지막 값을 예측할 수 있으므로 훨씬 더 일괄적인 state 관리가 가능해집니다.
또한 Component update의 순서에 주의할 점이 생겼다는 겁니다. React Fiber를 다른 우선순위를 통해 비동기적(Asynchronous) 실행이 가능해졌기 때문에 기존 Component Lifycycle이 프로그래머의 예상과 다른 순서로 작동할 수 있다는 점입니다.
예를 들어 우선 순위가 낮은 작업 A의 componentWillUpdate()
의 작업이 실행하는 중간 우선 위가 높은 작업 B가 생길 경우 기존의 작업 A의 update를 중간에 멈춥니다. 그 이후 작업 B를 우선적으로 업데이트 해주기 위해 B의 componentWillUpdate()
와 componentDidUpdate()
를 실행하고, 앞서 임시로 멈추었던 작업 A를 재개하게 됩니다.
이 때문에 우선순위가 적용되는 Component들의 Lifecycle과 연동되어있는 함수들은 우선순위에 따라 프로세스 순서가 바뀌어도 일관적인 결과물이 나올 수 있도록 유의해야 됩니다.
What’s next
위와 같은 변화로 렌더링에 많은 퍼포먼스를 업그레이드가 있었지만 여전히 앞으로 해결되야할 문제들이 있습니다. 만약 여러개의 우선순위가 높은 작업들이 계속해서 들어온다면 자연스레 우선순위가 낮은 작업들은 딜레이가 생기거나 혹은 영영 실행되지 못하는 상황(Starvation)에 빠지게 될 겁니다. 이러한 스케쥴링에 관한 이슈는 앞으로 React가 계속해서 해결해야될 문제가 될 듯 합니다.