주니어 프런트엔드 개발자, 리액트의 중심에서 반리액트를 외치다?
바닐라 자바스크립트를 사용하던 과거의 제가 리액트를 선택한 이유에는 다음과 같은 이유에서였습니다.
실제로 리액트의 다운로드 수는 나날이 증가하고 있으며, 증가세에는 가속도가 붙어 앵귤러와 뷰와의 격차를 점차 더 벌리고 있습니다.
앵귤러, 리액트, 뷰의 다운로드 수 비교 |
---|
![]() |
리액트의 인기가 높아진 지금, 한번 멈춰서 생각해봅시다. 리액트, 과연 항상 옳을까?
오늘은 리액트의 렌더링 방식을 살펴보고, 리액트가 모든 경우에 효율적이라고 할 수 있을지, 리액트의 약점은 없을지 되돌아보도록 하겠습니다.
리액트는 성능을 위해 가상돔을 활용합니다. 메모리에 가상돔을 올려두고, 화면이 갱신되어야 하는 시점에 이전과 이후의 가상돔을 비교하여 변경된 부분을 실제 돔에 반영하는 것입니다.
리액트의 렌더링 과정을 세 꼭지로 나누어서 살펴보도록 하겠습니다.
리액트 요소
jsx 문법으로 작성된 컴포넌트는 createElement 함수로 변경됩니다. 리액트의 createElement 함수가 리턴하는 객체를 리액트 요소 라고 합니다.
리액트 요소는 다음과 같은 형태를 가지고 있습니다.
{
type: 'button',
key: 'key1',
ref: null,
props: {
onclick: handleClick,
style: {
color: red,
},
children: 'start',
}
}
상태나 속성값이 변경되면 createElement 함수의 결과물인 리액트 요소의 구조도 변하게 되는데요, 이때 이전 결과물과 변경된 결과물을 비교하여, 변경된 영역만 실제 돔에 새로이 커밋하는 것이 리액트의 핵심이라고 할 수 있습니다.
리액트 요소 트리
리액트 요소는 트리 구조의 형태로 화면을 구성합니다. 리액트 요소가 모여 트리를 형성한 것을 리액트 요소 트리 라고 하며, 이는 결국 리액트의 가상돔 과 맞닿아 있습니다. 즉, 리액트 요소가 모인 리액트 요소 트리 중, 재귀적인 컴포넌트 함수 호출이 종료되어 실제 돔의 형태로 그릴 수 있는 형태가 되었을 때 가상돔 이라고 말할 수 있는 것입니다.
🤔 "재귀적인 컴포넌트 함수 호출이 종료되어 실제 돔의 형태로 그릴 수 있는 형태"란 무엇인가요?"
리액트의 렌더 단계는 리액트 요소 트리 간의 비교를 통해 종료됩니다. 리액트 요소 트리는 트리 형태로 구성된 컴포넌트들(함수들)이 차례로 호출되며 만들어집니다. 트리 형태로 구성되어 있기 때문에 컴포넌트들은 재귀적인 규칙에 따라서 호출되는 것입니다.
트리 형태로 구성된 모든 컴포넌트가 호출되면 최종 형태의 리액트 요소 트리가 만들어지고, 이때의 트리는 실제 돔의 형태로 변경될 수 있습니다. 위에서 살펴보았듯이, 리액트 요소에는 type
이라는 속성이 존재하는데요, 모든 리액트 요소의 type
속성값이 string일 때 실제 돔의 형태로 변경될 수 있다고도 할 수 있겠습니다.
파이버
파이버는 리액트 16버전부터 도입된 객체입니다. 리액트의 렌더링 단계에서 모든 리액트 요소는 파이버 로 변환됩니다. 파이버는 리액트 요소에 담긴 type, key, children 등
의 정보 외에도 이전 상태값인 memoizedProps
, 파이버의 작업 우선순위인 pendingWorkPriority
등의 정보를 가지고 있습니다. 즉, 파이버는 컴포넌트의 정보와 컴포넌트를 호출하는 작업에 필요한 모든 정보를 포함한 구조체라고 할 수 있겠습니다.
이처럼 리액트는 리액트 요소 트리를 통해 가상돔을 만들고, 이를 실제 돔과 비교함으로써 효율적으로 화면을 렌더링합니다. 하지만 리액트 도입은 항상 효율적일까요? 세 가지 꼭지를 놓고 생각해보겠습니다.
컴포넌트들을 차례로 호출하여 리액트 요소 트리를 만들었고, 이를 이용하여 vdom을 잘 만들었습니다. 실제 dom과 비교하여 변경된 영역만 빠르게 교체한다는 로직은 잘 이해했습니다. 그렇다면 비교할 대상들은 어디서 관리되어야 할까요?
리액트는 성능의 향상을 위해 vdom을 js 객체 형태로 메모리에 저장합니다. 렌더링 단계에서 이를 꺼내와 실제 dom과 비교를 수행하는 것이죠. 객체를 별도의 메모리에서 관리하기 때문에 자연스럽게 메모리 사용량은 증가하게 됩니다. 렌더링을 위해 메모리를 양보한 꼴이죠.
컴포넌트의 상태 혹은 속성이 변경되었습니다. 리액트 요소 트리가 다시 생성됩니다. 브라우저의 메모리에 저장된 이전의 vdom 스냅샷과 비교를 시작합니다. 이 diffing 알고리즘은 휴리스틱으로 최적화되어있다고는 하나, 휴리스틱을 벗어난 조건에 대해서는 성능 문제가 있을 수 있습니다. 휴리스틱은 해당 맥락에서 가장 최선의 해답을 찾을 뿐, 내놓은 답이 100% 정확하다는 것은 보장할 수 없기 때문입니다.
페이스북은 실제로 휴리스틱한 diffing 알고리즘의 성능 이슈를 인정하고, 리액트 16버전부터 fiber라는 구조체를 도입하여 diffing 알고리즘을 보완하였습니다.
Dom의 노드를 변경하면 리플로우/리렌더링이 발생하게 됩니다. 즉, 브라우저가 화면을 렌더링하는 수많은 스텝들을 다시 밟아가야 한다는 의미이죠. 변경하는 노드가 많으면 많을 수록 브라우저가 연산해야할 것들은 많아지고, 결국 렌더링 속도에도 영향을 줄 수 밖에 없습니다. 리액트는 vdom을 사용하여 변경점을 빠르게 찾아내기 때문에 효율적인 렌더링을 합니다. 한가지 의뭉스러운 구석이 있습니다.
노드 변경이 잦지 않은 경우라면? 🤔
vdom을 만들고 비교하는 것도 모두 리소스를 사용하는 일입니다. vdom 스냅샷을 저장하는 것도 메모리 측면에서 추가 비용입니다. 노드 변경이 잦지 않은 경우라면 구태여 추가 비용을 소모하는 일이 효율적이지 않을 수 있습니다. 예를 들어, 정적인 사이트의 경우에는 단순히 dom에 접근하여 ui를 다시 그리는 것이 리액트의 렌더링-커밋 비용보다 적을 수 있습니다.