React 왜 이렇게 어려울까..? by Vue 개발자(End)
React 왜 이렇게 어려울까..? by Vue 개발자(End)
저는 React 개발을 1년 Vue 개발을 2년을 했습니다. 처음 접한 React는 정말 어려웠습니다. 특히나 렌더링이 자주 발생하는 상황을 대처하는 것이 가장 어려웠습니다. 어려웠던 1년을 보내고 회사의 권유로 인해 Vue를 접하게 되었습니다. Vue는 React와는 전혀 달랐습니다. 렌더링은 제가 의도한 부분에서만 작동하고 코드가 렌더링의 방해를 받지 않았습니다.
분명히 둘 다 Virtual Dom 메커니즘을 사용하는데 왜 이렇게 많이 다른 것인지 궁금했고 Vue가 쉬운 이유는 “양방향 바인딩” 덕분이다.라는 글을 많이 보았지만 제가 느끼기에 “양방향 바인딩”은 Vue가 쉬운 이유의 10%정도에 불가하다고 생각하였습니다. 그래서 무엇 때문에 Vue는 쉽게 느껴지고 React는 어렵게 느껴지는 지에 대해 알아보겠습니다.
Vue가 쉬운 이유, React가 어려운 이유
vue의 실제 동작
랜더링 순서
Create
App → Parent → Child → StaticChild
onMounted
Child → StaticChild → Parent → App
onUpdated (count가 증가할 경우)
Parent → App
특징
- onMounted은 create과정 이후 재귀하며 root로 다시 돌아오는 과정입니다.
- onUpdated : 렌더링을 감지할 수 있습니다. (count 증가)
- 반대로 onUpdated 외부는 렌더링의 영향을 받지 않습니다.
Vue 확장프로그램을 통해 렌더링을 감지해본다면?
결론
- vue는 직접 렌더링을 조작할 필요가 없습니다. VDOM이 조작될 경우에만 렌더링이 발생하고 자식 컴포넌트에게 영향을 주지 않습니다.
- 마치 자식 부모 컴포넌트라기 보다는 서로가 독립적이고 각자가 데이터를 바인딩하고 있는 느낌입니다.
- 렌더링이 코드에 영향을 주는 공간을 onUpdate hook 메소드에 제한해 놓음으로서 기본적인 코드들이 렌더링에 영향을 받고 여러번 호출될 일이 없습니다.
- 마치 컴포넌트들이 서로 대등한 위치에 있는 느낌을 줍니다.
- 개발자는 렌더링이 코드에 영향을 주지 않아 마치 기존의 이벤트 기반의 JS를 개발을 하는 것과 동일한 느낌을 줍니다.
- 그리고 상위 컴포넌트의 렌더링을 신경쓰지 않아도 되기 때문에 Vue가 쉽다고 생각합니다.
react의 실제 동작
- 리액트는 일단 자식 컴포넌트는 무조건 랜더합니다.
- 변화하지 않는 컴포넌트의 랜더를 막기 위해서는 여러가지 방법을 사용해야 합니다.
- useRef : 랜더하지 않는 변수를 만들 수 있다.
- useMemo : 변수를 메모이제이션한다.
- useCallback : 함수를 메모이제이션한다.
- React.memo : 컴포넌트를 메모이제이션한다.
- useEffect를 사용한다면 랜더에 영향을 받지 않을 수 있습니다.
- 단점 : 변화하는 값을 추적할 수 없습니다.
- 단점을 보완하기 위해 useEffect에 추적하고싶은 변수를 넣어주어 그 변수만 추적가능한 기능이 있다.
App 컴포넌트에서 Count가 증가할 경우를 React 확장 프로그램을 사용하여 렌더링을 감지해 보았습니다.
React.memo를 사용하지 않을 경우
React.memo를 사용할 경우
확실히 React.memo를 사용할 경우 불필요한 렌더링을 하지 않는 것을 볼 수 있습니다. count가 아무리 늘어나도 MemorizeComponent는 변화가 없기 때문에 렌더링하지 않습니다.
결론
- React는 직접 렌더링을 조작해야 하기 때문에 어렵다고 생각합니다.
- 자식컴포넌트는 무조건 부모컴포넌트의 렌더에 영향을 받기 때문에 페이지 구조 혹은 컴포넌트 구조를 미리 어느정도 생각하고 만들게 됩니다.
- 렌더를 막기 위해 여러가지 방법이 존재하고 잘 활용한다면 필요할 때에만 렌더하도록 만들 수 있습니다. 하지만 그만큼 러닝커브가 존재합니다.
- JSX,TSX 파일 전체가 렌더의 영향을 받습니다. 이러한 영향은 기존의 JS를 개발했던 이벤트 기반과는 다르게 작동하기 때문에 더 어렵게 느껴집니다.
왜? 어떻게? 이렇게 동작이 되는 걸까?
이러한 동작을 하는 이유는 공통 컨셉인 Virtual DOM과 연관이 있습니다.
Concepts
Virtual DOM(공통)
간단한 DOM Tree의 모습입니다.
DOM Tree가 만들어지는 순서 즉 브라우저 렌더링은 간단하게 5단계로 진행됩니다.
- DOM Tree 생성 : code를 해석하여 html을 파싱 후 만듭니다.
- CSSOM(css object model) 생성 : css를 파싱하여 CSSOM 생성
- Render Tree : DOM Tree, CSSOM 결합.
- Reflow : Viewport에 맞게 위치 조정
- Repaint : 실제 픽셀로 그리기
우리는 getElementById, querySelector를 사용하여 DOM의 Element를 수정할 수 있습니다.
하지만 DOM Element를 수정할 경우 매번 브라우저 랜더링이 진행됩니다. 이는 웹이 복잡해 질 수록 성능저하에 큰 원인이 되었습니다.
Virtual DOM이란?
- Object형태로 메모리에 저장한 DOM의 복사본입니다.
- 실제 DOM을 복사한 것이기 때문에 같은 정보를 가지고 있습니다.
Virtual DOM을 사용하는 이유?
우리는 getElementById, querySelector를 사용하여 DOM을 매번 직접 수정하여 매번 브라우저 렌더링을 발생시켰습니다. 그래서 Virtual DOM을 수정하고 일정 수준의 작업이 쌓이면 그때 Virtual DOM을 DOM으로 교체하여 브라우저 렌더링의 횟수를 줄이고자 하는 것입니다.
위의 그림으로 간단히 설명해 보겠습니다.
- props or state가 변경될 경우 Dirty List(대기열)에 추가합니다.
- 새로운 변경 사항이 Dirty List에 추가될 수 있도록 조금 기다립니다.(debounce time)
- debounce time이 끝나면 새로운 VDOM Tree를 만듭니다.
- VDOM과 DOM을 비교하는 조정(Reconciliation 혹은 Diffing) 알고리즘이 작동합니다.
- Dom Tree와 VDOM Tree를 비교하여 최소한의 변경횟수와 변경사항을 찾아냅니다. 이 과정에서 DFS알고리즘을 통해 순회하게 됩니다.
- Dirty List의 모든 변경 사항을 일괄로 처리되고 실제 DOM의 렌더링은 1번 진행됩니다. 이러한 과정을 조정 프로세스라고 합니다.
Virtual DOM으로 인해 4가지의 변경사항에도 한번의 렌더링을 하여 렌더링 횟수를 크게 줄일 수 있었습니다.
React와 Vue에서 Virtual DOM 매커니즘을 사용하고 있고 각자 사용하는 방식은 어떨지 비교해 보겠습니다.
Vue 와React의 VDOM
Vue는 어떻게 독립적인 컴포넌트를 가질 수 있을까요? React는 왜 부모 컴포넌트가 모든 자식 컴포넌트에게 영향을 줄까요? 서로의 동작 방식이 다름은 VDOM을 사용하는 방식이 다르기 때문입니다. 구체적으로 이들이 어떻게 VDOM을 사용하는 지 알아보겠습니다.
Vue의 VDOM
Vue의 구조에 대해 알아야 Vue의 VDOM을 이해하기 쉽기 때문에 SFC를 소개하고 넘어가겠습니다.
SFC란?
SFC는 HTML, CSS 및 JavaScript 3가지를 하나로 자연스럽게 합친 것입니다.
Vue는 프론트엔드의 웹이 점점 복잡해짐에 따라서 파일유형(html,js,css)로 분리할 경우 코드베이스 유지 관리가 더욱 어려울 것으로 판단하여 “동일한 사용 목적”으로 하나의 파일로 결합한 SFC를 개발했고 컴포넌트의 응집력과 유지 관리가 용이해졌다고 합니다.
Single File Component인 *.vue의 구조
- template
- 용도 : html코드를 입력할 수 있습니다. vue 문법을 이용할 수 있습니다.
- 실행 :
@vue/compiler-dom으로 전달되고, JavaScript 렌더 함수로 사전 컴파일됩니다.
- script
- 용도 : js코드를 입력할 수 있습니다.
- 실행 : ES 모듈로 실행됩니다.
- style : css 코드입니다.
SFC의 template은 렌더함수로 사전 컴파일 되어 DOM 렌더링에 합류하게 됩니다. 이러한 과정을 Compiler-Informed Virtual DOM 이라 부릅니다.
VUE는 컴파일러와 런타임을 모두 제어합니다.
(Compiler-Informed Virtual DOM)
Vue는 Element의 정보들이 모여있는 template를 컴파일합니다.
컴파일 시에 정보를 추론하여 변경할 요소를 미리알고 그 요소만 추적할 수 있기 때문에 부분 렌더링이 가능합니다. 심지어 변경되지 않을 요소도 미리 알기 때문에 필요없는 렌더링을 하지 않습니다.
vnode
- VDOM은 하나의 엘리먼트의 정보를 vnode(virtual node)하나에 저장하여 여러노드가 되고 그 노드들이 하나의 트리를 만들고 그 트리가 VDOM이 됩니다.
Vue의 컴파일 단계
- Static Hoisting
- 정적인 노드는 비교하는 것이 불필요하기 때문에 Vue 컴파일러가 호이스트하여 모든 렌더에서 동일한 vnode를 재사용하도록 합니다.
- Patch Plag
- 동적 바인딩이 있는 단일 엘리먼트의 정보를 추론할 수 있습니다.
- Class : 2, Props : 8 등 마지막 숫자에 따라 추론합니다. 이것이 패치플래그 인자입니다.
- Tree Flattening (트리 병합)
- root 블록 내부의 동적인 하위 노드만 포함하는 flattened Array를 생성합니다.
- 트리 병합 : 패치 플래그가 있는 모든 하위 노드를 추적합니다. 다시 렌더링 할 경우 병합된 트리만 탐색합니다.
CI-VDOM 랜더 과정
- SFC의 template구간 내부 코드가 빌드 시 컴파일 되어 렌더 함수 코드로 변환됩니다.
ex) 템플릿 코드 ⇒ 렌더 함수 코드 (템플릿 탐색기로 확인 가능합니다.)
- 렌더 함수 코드는 런타임에서 Patch Plag 와 Tree Flattening을 통해 바인딩 해놓은 노드를 추적하고 변경될 경우 다시 렌더하여 실행합니다.
- 변경된 렌더 함수는 VDOM Tree에 반환되고 마운트 혹은 패치되어 DOM에 적용됩니다.
결론
일반적인 Virtual DOM은 런타임 성능을 저하시킨다.
- 일반적인 VDOM은 런타임 과정에서 조정 프로세스를 통해 변경사항을 추적합니다. 그 방법은 DFS 알고리즘을 사용하여 VDOM Tree를 순회하고 변경사항을 찾아내는 것입니다.
- Vue는 이러한 런타임의 조정 프로세스가 비효율적이라고 생각했습니다. 런타임 과정에서 매번 VDOM Tree를 순회하는 것은 런타임 성능을 저하시키기 때문입니다.
Vue가 선택한 방법
- 변경이 예정된 vnode와 변경되지 않을 vnode를 컴파일단계에서 미리 추적하여 VDOM Tree를 굳이 순회하지 않고 추적중인 vnode만 감시하면 되도록 설계하였습니다.
- 이는 런타임에서 최소한의 루트로 변경될 컴포넌트에 바로 접근할 수 있으며 vue의 실제 동작을 보았던 것 처럼 부모컴포넌트가 렌더링 되더라도 자식 컴포넌트가 정적인 컴포넌트라면 변경되지 않는 이유가 성립됩니다.
- 즉 해당 설계방식으로 인해 컴포넌트의 독립적인 것처럼 보일 수 있게 되었습니다.
React의 VDOM
React의 Reconciliation(재조정)
Stack Reconciliation
React의 조정 프로세스는 16버전 이전과 이후로 나뉩니다. 16버전 이전에는 Stack기반 Reconciliation 방식으로 위에서 설명한 VDOM과 같습니다.
동작 과정
- 업데이트
- vdom변경사항으로 new vdom생성
- new vdom ⇒ 재조정 ⇒dom 적용
Stack Reconciliation의 두가지 특징이 있습니다.
- 조정 알고리즘 : DFS 알고리즘을 사용하여 VDOM Tree를 순회하고 변경사항을 찾아내어 DOM을 업데이트합니다.
- 유저 인터렉션의 우선순위 : 조정 알고리즘이 끝난 후 (DFS 트리 순회) 유저 인터렉션을 받습니다.
이 특징은 여러가지 문제점을 만들어냅니다.
- 거의 모든 트리 순회합니다.
- 동기적 실행 : 메인 스레드를 사용하여 UI가 끊기는 현상이 있을 수 있습니다. 특히 디바이스가 느리거나 대규모 가상 트리를 처리할 경우 심해질 수 있습니다.
- 우선순위 없음 : 중요한 업데이트를 먼저 할 수 없습니다.
- 무중단성 : 업데이트가 실행 중에는 다른 업데이트가 실행될 수 없습니다.
- 유저 인터렉션이 두번 생긴다면 인터렉션 - 조정 - 인터렉션 - 조정 순으로 중단없이 작동됩니다. 첫번째 이벤트가 꼭 동작해야한다는 것은 UX에 좋지 않을 수 있습니다.
이러한 단점들로 인해 React v16부터는 Fiber Architecture가 등장하였습니다.
Fiber Architecture (v16)
Stack Reconciliation의 문제점을 돌아보며 React는 해결해야할 부분들을 정리합니다.
- 작업을 일시 중지하고 나중에 다시 돌아 올수 있어야 합니다.
- 다양한 유형의 작업에 우선순위를 할당할 수 있어야 합니다.
- 이전에 완료한 작업을 재사용합니다.
- 더 이상 필요하지 않은 작업은 중단합니다.
이러한 문제를 해결하기 위해서 FIber는 두가지 개념을 도입합니다.
Double Buffering : 두개의 청사진을 비교하는 기법
VDOM 두개를 사용하여 VDOM을 바로 DOM에 적용하는 기존의 방식과는 달리 업데이트 전 VDOM과 업데이트 후 VDOM을 비교하는 기법입니다.
바로 DOM에 적용하지 않음으로서 생기는 이득은 무엇일까요?
- DOM 적용 전에 VDOM을 적용해도 될지 확인이 가능합니다. 만약 우선순위가 더 높은 인터랙션이 들어왔을 때 더블 버퍼링 개념은 가상 렌더링 중이므로 새로운 업데이트를 받아들이거나 그러지 않는 선택을 할 수 있습니다.
Fiber Data Structure :
Double Buffering 과 Fiber Data Structure를 도입하여 만들어진 프로세스는 다음과 같습니다.
재조정 프로세스가 두개로 나뉘고 두개의 재조정 프로세스 중간에 우선순위가 맞는 업데이트인지 확인하는 것을 볼 수 있습니다. 아마 이 기능으로 인해 우선순위가 없고 무중단이었던 이전의 단점을 보완한 것 같습니다. 그러면 이러한 동작은 어떻게 작동하는 걸까요?
Fiber Reconciliation
파이버 재조정은 workLoop 내부에서 이루어지며 workLoop에는 두가지 단계가 존재합니다.
- Render Phase
- Commit Phase
Render Phase
beginWork
workInProgress Tree에서 파이버 노드의 업데이트 여부에 대한 변경사항을 표시하는 역할을 합니다.
completeWork
업데이트를 적용하고 메모리(VDOM)에 저장합니다. 우선순위가 더 높은 업데이트가 예약될 경우 버려질 수 있습니다.
beginWork에서의 “dirty”표시를 보고 업데이트를 진행합니다.
completeWork가 root에 도달하고 업데이트 된 새로운 DOM Tree를 구성한다면 랜더링 단계는 끝나고 커밋 단계로 넘어갑니다.
Commit Phase
커밋단계는 두 단계로 나뉩니다.
Commit Phase가 끝난다면 즉 모든 랜더링 단계가 끝나게 되면 commitRoot 함수를 호출하게 됩니다.
commitRoot
- 이 함수가 실행되면 FiberRootNode와 Current Tree 포인터를 제거하고 workInProgress Tree에 포인터를 연결한 후 새로운 Current Tree로 만듭니다.
정리 해보자면
React의 Fiber Architecture는 두개의 VDOM을 사용합니다.
- 현재의 상태를 항상 유지할 수 있는 Current Tree
- 업데이트를 계속 진행 할 수 있는 workInProgress Tree
그리고 이 두가지 트리를 참조하는 최상위 노드인 FiberRootNode가 존재합니다.
이 두가지 트리를 이용하여 가상 렌더링을 하고 적절한 우선순위와 적절한 타이밍에 Real DOM에 적용하게 됩니다. 중간에 우선순위가 높은 유저 인터랙션이 생길 경우 workInProgress Tree를 제거하고 다시 생성하여 우선순위가 높은 업데이트에 대응합니다.
재조정 과정이 끝난다면 FiberRootNode의 포인터를 workInProgress Tree로 변경하고 이것을 새로운 Current Tree로 사용합니다.
결론
- 기존의 VDOM과 비교하자면 UI상으로의 성능은 확실히 보장이 됩니다.
- 우선순위에 맞는 결정을 하기 때문에 여러번의 인터랙션을 전부 대응하지 않고 가장 우선순위가 높은 인터랙션에 대응함으로서 유저입장에서는 부드러운 UX를 표현할 수 있습니다.
- useMemo, useCallback 등 메모이제이션을 사용할 경우에는 렌더링은 진행되지만 함수(혹은 변수 등 메모리 값)를 메모리에서 꺼내오기 때문에 렌더링이 되지 않는다. ?
그래서 React vs Vue는?
- 리액트는 라이브러리이기 때문에 어렵다. 모든 결정의 선택권은 개발자에게 있기 때문이다. 하지만 그만큼 자유롭다.
vue가 다른점
- 아무리 자식 컴포넌트라도 props가 전달되지 않으면 렌더하지 않는다(변하지 않는 props를 전달해보자)
- script내부의 js코드가 랜더링에 반응하여 작동하지 않는다. (리액트는 강제로 같이감)
- 랜더링을 감지하고 싶다면 onUpdated 훅을 사용하면된다.
결론 : 랜더관리가 훨씬 편하다, 리액트는 프레임워크로서 기능이 없고 UI 컴포넌트만 가져다 쓰기 때문에 라이브러리다.
출처 :
Tejas Kumar, Fluent React, O’Reilly Media, [The Virtual DOM,Inside Reconciliation]
Maya Shavin, Learning Vue, O’Reilly Media,
https://vuejs.org/guide/extras/rendering-mechanism.html
https://pooney.tistory.com/87https://velog.io/@1nthek/React-Virtual-DOM과-렌더링
https://velog.io/@bada308/Virtual-DOM-with-React

