남다른 개선방법을 다시 보여준 페이스북의 React Fiber

React에서 core코드를 상당히 바꾼다는 소식이 있었습니다. 뒤늦게 알게 됐지만 작년에도 Fiber에 대한 설명은 좀 있었습니다. 2017년에는 4월 F8 에서 자주 언급이 됐고요.

React의 모조리는 아니지만, 코어가 바뀐다는 소식이라, ‘왜? 얼마나 좋아지려고?’ 등의 궁금증이 생겼습니다. 개인적으로는 virtual-DOM이라는 멋진 아이디어를 냈던 페이스북인지라 그 기대감도 있었고요.

그들은 React의 어떤점을 문제 삼은 걸까?

React 개발팀의 정확한 이유는 모르겠지만, 렌더링방식에 대한 문제를 느꼈던 것 같습니다. React가 가진 단점이라고만 볼 수는 없지만, React역시 무언가 처리하는 시간을 단축시키지는 않습니다. 오히려 lifeCycle함수와 잦은 render 함수가 호출될 확률이 있는 구조라, 실제로 breakpoint를 걸거나 timeline 탭으로 call stack을 들여다보면 그 깊이에 움짤하게 되죠.

React는 DOM조작을 최소화 하는 장점은 있지만, JavaScript의 single thread라는 굴레를 벗어나게 해주진 않죠. 그러다보니 당연히 복잡한 애니메이션과 연산작업이 섞여 있다고 해서 React 의 Virtual-DOM이 이문제를 말끔히 해소해 주진 못합니다.

먼저 Fiber의효과를 볼 수 있는 영상이 공개됐네요. 아래는 페이스북이 Fiber 홍보를 위해 만든 코드의 데모영상입니다.

영상의 예제는 Sierpinski triangle 구현체 입니다. 위 데모 코드는 여기에 있습니다. 영상만 보면 Fiber없는 React로는 커다란 문제가 있어보이죠.

코드를 보면 Sierpinski triangle구현을 하면서 CSS3 transform속성이 변경되며 애니메이션이 이어집니다. 그리고 requestAnimationFrame으로 적절한 타이밍에 연속적인 렌더링이 되도록 애니메이션 처리가 되고 있죠. 여기까지는 별다른 문제가 생기지 않으나 문제는 이러한 과정 사이에 발생하는 HTML변경, 즉 DOM변화 입니다. 위 예제에서는 setInterval을 사용해 초단위로 많은 DOM의 변화를 발생합니다. (오해할 수 있으나 requestAnimationFrame을 사용해도 결과는 똑같이 느릴 겁니다) 사실 이 예제는 너무 많은 DOM의 변경이라 흔한 상황은 아니지만 어쨌든 상당히 버벅이긴 합니다. 이런 이유는 비동기로 처리되는 애니메이션 처리가 DOM처리를 하느라 스케쥴이 빼곡한 stack공간에 들어가기까지 많은 시간을 대기해야 함으로 60fps는 고사하고 적절히 끊기는 효과처럼 동작합니다.

개선지점 — 어떤 부분을 개선해야 한다고 생각했을까?

심각해보이는 위 현상은 react때문은 아닙니다. 비동기로 처리되는 애니메이션작업이 반복적으로 이뤄지는 DOM조작에 밀려서 생기는 우선순위 문제죠. single-thread 환경에서는 당연한 일이라, UI개발자라면 대게 연속적으로 처리되는 애니메이션 과정에서 복잡한 DOM조작은 본능적으로 자제해왔을 겁니다.

UI개발에서 당연하다고 생각할 수 있는 이 문제를 페이스북 개발팀은 해결하고 싶었던 것 같습니다. ‘multi-thread처럼 동작할 수는 없을까?’, ‘더 중요한 일을 먼저 처리하게 하고 보이지 않는 부분은 나중에 처리할 수 없을까?’ 와 같은 고민 말입니다. 비동기 콜백이 너무 긴 대기상태로 바쁘지 않고 어떤 상황에서도 규칙적으로 실행되거나 먼저 실행되게 하면 되는 것이죠. 그러기 위해서는 stack 을 빨리 비워주면 됩니다. 안타깝게도 이부분은 JavaScript 엔진에서 처리되는 것이라 개발자의 영역은 아닌 것처럼 생각되죠.

어디서 답을 찾았을까?

requestIdleCallback 에서 답을 찾은 듯 합니다. 사실 Fiber가 React를 통째로 다시 구현했다고 해도 겉으로 보이는 건 requestIdleCallback을 잘 활용한 점으로 보입니다. requestIdleCallback은 특정 잔여 시간을 기준으로 어떠한 일을 더 할지 안할지를 결정할 수가 있습니다. millisecond단위로 어떠한 작업시간내에 이뤄지는지를 판단해서 ‘ 계속/고만'을 결정할 수 있는 셈이죠.

이해를 돕기 위해 MDN의 기본예제를 fork해서 추가한 코드입니다. 원래는 requestIdleCallback의 브라우저 호환성은 2017년 현재 부족합니다. 하지만 아래처럼 사랑스러운 setTimeout 마법을 적당히 쓰면 polyfill 구현에는 문제가 없어 보입니다.

이러한 requestIdleCallback을 써서 시간단위로 세심한 컨트롤을 하려면 기능이 단순하고 명확해야 합니다. React는 부모/자식관계로 맺어진 Component들이 줄지어 연결되어 있죠. component life cycle 관련한 메서드를 거쳐야 렌더링이 종료되기도 하고요. 이런 구조에서 세심한 task 컨트롤을 해야 하는게 쉬울까? 생각이 듭니다. 그래서 제 생각에 이번에 Fiber에서 다시 구현하면서 task 스케쥴링을 컨트롤 할 수 있는 구조로 내부 동작을 변경한 것 아닌 가 싶습니다.

“Fiber is reimplementation of the stack. ( virtual stack frame )”

Fiber의 설명을 보면 이런 문구가 보입니다. 제 생각에 이번 변화에 대한 명확한 표현이라 인용했습니다. 미쳤죠. 요번에는 virtual 시리즈 2탄, stack frame입니다. (이름이 참 멋지죠.. virtual stack이라니…)

‘아무튼 stack을 구현했다고?

첫 느낌은 굉장하고, 부럽고, 멋져 보였습니다. (아 저걸 만들 시간이 부럽기도 하고..)

Fiber의 architecture를 설명한 글에서는 Fiber 동작을 이렇게 표현했네요.

  • pause work and come back to it later.
  • assign priority to different types of work.
  • reuse previously completed work.
  • abort work if it’s no longer needed.

참 이상적입니다.

설명만 봐서는 Fiber에서는 requestIdleCallback을 활용해서 동작중인 React 코드를 매번 부르고, 주어진 시간을 초과한다면 멈추고 더 중요한 일에 양보합니다. 더 중요한 일이 끝나면 다시 돌아와서 나머지 작업을 완료합니다.

어느 시점에서나 멈출 수 있어야 한다는 것은, task가 잘게 쪼개질 수 있어야 가능해 보입니다. Fiber에서는 그 부분의 개선이 있었을 거 같네요. 잘게 쪼개진 task는 언제나 멈출 수 있고 다시 돌아와서 재개할 수 있죠. 예를들어 A->B함수를 호출하는 전단계에서 멈추었다면, B함수에 전달할 인자와 B함수를 실행해야 한다는 정보를 남기면 될 겁니다. 그리고 다시 돌아와서 할일이 남긴 것을 보고 이어서 실행하면 되는 것이죠. 마치 남겨둔 빵조각을 찾아서 무언가 하는 것처럼 말이죠.

https://www.youtube.com/watch?v=ZCuYPiUIONs 영상화면

이 아이디어는 React말고 다른곳에도 적용될 수 있습니다. 다른 프레임워크가 따라할 수도 있겠죠. virtual DOM처럼요. 우리가 복잡한 애니메이션 컨트롤을 해야 한다면 requestIdleCallback과 requestAnimationFrame을 잘 활용해서 최적의 성능을 낼 수 있을것이라 생각합니다. React를 안쓰는 환경에서도요.

당연하지만 이와 같은 동작은 DOM에 실제 변화를 주기 전단계인 reconciliation(virtualDOM처리) 단계에서 처리된다고 하네요. 그런데 이렇게 되면 component life cycle이 완료 되기 전에 다른 life cycle이 동작되면서 좀 엉키거나 복잡해지진 않을까? 라는 우려가 좀 들긴합니다.(우려일수도있고요)

또한 stack을 컨트롤 하기 위해 Fiber에서는 여러가지 field개념을 정의하고 있는데, stack frame을 구성하는데 필요한 부분인, function, return address , arguments등에 매칭되는것을 component, parent component, props 등으로 React의 구조에 맞게 이를 filed라고 정의해서 사용합니다. 작업의 우선순위를 결정하기 위한 priority 속성을 정의하기도 하고요. 이런 것으로 중단->양보->재개를 원활하게 컨트롤 할 것 같습니다.

이렇게 동작하는 Fiber의 실체는 JavaScript 객체 형태라고 합니다. 다시 말해 위와 같이 동작되기 위해 그 정보를 객체에 담아두고 있는 것이고, 그걸 Fiber라고 일컫는 셈이죠 .

그런데 정말 효과가 있는지?

Fiber로 인한 React의 변화를 이해하고 느껴볼 겸 간단히 테스트를 돌려봤습니다. 동영상만 봐서는 진짜? 라는 의구심이 좀 들었고요.

결과적으로는 특정 상황에서는 큰 의미가 있다, 하지만 평범한 환경에서는 그닥 좋아진 점이 없을 것이다 라는 결론입니다. (예측입니다. Fiber가 어떻게 더 좋아질지는 아직모르죠)

먼저 아래 두 개의 그림은 Fiber없는 현재 렌더링 결과 입니다. call stack도 깊고, 시간도 225ms로 아주 길죠.

Fiber없는 일반 렌더링에서의 깊~은 call stack과 225ms 이라는 커다란 소요시간
상단에 붉은색으로 1200–1600ms 동안 FPS가 굉장히 낮으면 실제로 빨간색으로 표시되고 있습니다. 요시간대에 animation렌더링 처리가 되지 못하고 있는 셈이죠

Fiber를 적용한 경우(실제 Fiber를 적용하려면 unstable_deferredUpdates 메서드를 통해서 setState를 호출하면 됩니다) 아래처럼 조각조각 stack이 끊기고 사이에 다른 작업이 들어와서 처리되는 것을 볼 수 있습니다.

연산작업이 이뤄지긴 하지만, 짧은 시간만을 작업하고 (16ms정도) call stack을 비워줌으로써, 사이사이에서 animation 작업과 painting작업처리가 가능하네요.

위 결과만 봐서는 현재 데모코드의 동작의 차이는 분명히 Fiber만세네요.

그런데 궁금해서 SierpinskiTriangle 연산과정 이후 처리되는 DOM조작을 현재수준에서 절반정도로 줄여봤더니 (그저 삼각형을 그려야 하는 갯수를 줄이도록 수정) 빵빵한 노트북이라 그런지 unstable_deferredUpdates메서드의 적용과 상관없이 애니메이션이 부드럽게 동작하더군요. ^^;
여기서 알 수 있는 건, Fiber의 효과를 찐하게 느끼려면 연속적인 애니메이션과 많은 DOM조작이 필요한 rich UI라야 한다는 점입니다. 또 반대로 알 수 있는 건 React를 써서 생기는 문제라고 생각하는 부분이 Fiber를 적용한들 그닥 효과가 없을 거라는 점이죠. 예를들어 잘못된 코드패턴으로 render메서드가 너무 자주 불려지거나, 비동기로 인해 느려지는 초기 로딩 렌더링이라던가, 연산과정이 너무 복잡해서 렌더링까지 걸리는 시간이 오래 걸린다거나 하는 문제까지 해결해 준다고 볼 수 없습니다.

그래서 복잡한 애니메이션과 같은 상황이 맞물려 있지 않다면 unstable_deferredUpdates를 사용하지 않는 게 좋겠다는 생각도 듭니다. 물론 이번 Fiber 적용 버전에서 확 느껴지진 못했지만 드러나지 않은 렌더링 효율성으로 인해 reconciliation과 rendering 처리가 더 빨리졌을 수도 있습니다.

Fiber 개선의 초점 — UX

React는 view작업을 돕는 라이브러리라고 하죠. 그만큼 DOM관련 렌더링 처리는 React에서 중요한 부분입니다. React를 어떻게 개선할까? 라고 고민할때 단순하게 현재 가장 큰 문제가 뭐지? 라는 부분이 였을꺼라 생각합니다. 그렇게 생각되는 이유는 결국 Fiber의 장점은 UX를 향상시키기 위한 것으로 보이는 것이죠. 개발생산성과는 별로 상관 없어보입니다. 실제로 setState를 사용하는데 이전과 다른점은 위에 잠깐 나왔지만 setState함수를 unstable_deferredUpdates메서드의 콜백함수로 사용하는 것입니다. 그외에 이전 React버전과 완전한 하위 호환성을 보장할 예정이라고 하네요.

ReactDOM.unstable_deferredUpdates(() => {
this.setState(state => ({ seconds: (state.seconds % 10) + 1 }));
});

아무튼 React의 커다란 변화의 초점이 UX 향상이라는 것은 참 의미있는 것 같습니다. 많은 프레임워크가 대부분 개발생산성과 멋진 도구로 이를 포장하곤 하지만, React는 약간 다른 느낌입니다. 더 빠른 렌더링이라는 UX개선으로 인해 사용자는 더 쾌적하게 React기반 웹사이트를 경험하겠죠.

살짝 벗어난 이야기를 해보면, 대부분 도구나 라이브러리는 첫번째 고객인 개발자를 생각하며 만들게 되죠. 하지만 UI를 다루는 Front-end의 그것은 ‘사용자’ 와 관련된 부분도 고려해야 한다고 생각합니다. 그분이 우선하게 되면 결국 더 인기있고 오래 견디는 도구가 될 것입니다.

React가 칭찬받는 것중 하나는 이런 큰 개선을 직접 facebook 홈페이지의 테스트버전에서 동작하고 있다는 점이죠. 본인들이 직접 사용하고 적용하다보니 정말 의미있는 변화를 개선의 초점으로 생각하게 되는 것 같네요. 
국내에서도 자체적인 프레임워크를 만들고 이를 사용자와 연결지어 꾸준히 개선을 해나간다면 꽤 인기 있는 것들이 나올 것이라 생각합니다.

쓰고보니 Fiber에 대한 이해가 부족한 글이라 조심스럽네요. 이 글 이후로 더 다양한 정보가 많이 나눠지면 좋겠습니다.

코드스쿼드 윤지수

# 참고한 글들 중 기억하는 곳입니다.

설명이 잘되어 있는 글 
> https://giamir.com/what-is-react-fiber

requestIdleCallback API
> https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
 https://www.w3.org/TR/requestidlecallback/
 
Fiber의 장점을 설명한 데모코드
> https://github.com/facebook/react/blob/master/fixtures/fiber-triangle/index.html

Fiber Architecture
> https://github.com/acdlite/react-fiber-architecture?utm_source=hashnode.com#react-fiber-architecture

Fiber가 적용된 버전을 테스트하기 위해서는 React 16.0.0 이상을 사용하면 됩니다.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.