리액트는 항상 옳을까?

Ethan Yu·2023년 6월 10일
1
post-thumbnail

주니어 프런트엔드 개발자, 리액트의 중심에서 반리액트를 외치다?

바닐라 자바스크립트를 사용하던 과거의 제가 리액트를 선택한 이유에는 다음과 같은 이유에서였습니다.

  • 비즈니스 로직과 렌더링 로직(돔 요소를 직접 수정하기)이 혼합되지 않는다.
  • 상태 변화에 따른 화면 업데이트가 일방향적이다.
  • 가상돔을 이용하여 빠르게 화면을 업데이트한다.
  • 뷰나 앵귤러와 달리 자유도가 높다.

실제로 리액트의 다운로드 수는 나날이 증가하고 있으며, 증가세에는 가속도가 붙어 앵귤러와 뷰와의 격차를 점차 더 벌리고 있습니다.

앵귤러, 리액트, 뷰의 다운로드 수 비교
다운로드수 비교

리액트의 인기가 높아진 지금, 한번 멈춰서 생각해봅시다. 리액트, 과연 항상 옳을까?
오늘은 리액트의 렌더링 방식을 살펴보고, 리액트가 모든 경우에 효율적이라고 할 수 있을지, 리액트의 약점은 없을지 되돌아보도록 하겠습니다.

리액트는 이렇게 렌더링을 합니다.


리액트는 성능을 위해 가상돔을 활용합니다. 메모리에 가상돔을 올려두고, 화면이 갱신되어야 하는 시점에 이전과 이후의 가상돔을 비교하여 변경된 부분을 실제 돔에 반영하는 것입니다.
리액트의 렌더링 과정을 세 꼭지로 나누어서 살펴보도록 하겠습니다.

리액트 요소

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 등의 정보를 가지고 있습니다. 즉, 파이버는 컴포넌트의 정보와 컴포넌트를 호출하는 작업에 필요한 모든 정보를 포함한 구조체라고 할 수 있겠습니다.

리액트는 항상 옳을까?


이처럼 리액트는 리액트 요소 트리를 통해 가상돔을 만들고, 이를 실제 돔과 비교함으로써 효율적으로 화면을 렌더링합니다. 하지만 리액트 도입은 항상 효율적일까요? 세 가지 꼭지를 놓고 생각해보겠습니다.

(1) 비교를 위한 추가적인 메모리 사용

컴포넌트들을 차례로 호출하여 리액트 요소 트리를 만들었고, 이를 이용하여 vdom을 잘 만들었습니다. 실제 dom과 비교하여 변경된 영역만 빠르게 교체한다는 로직은 잘 이해했습니다. 그렇다면 비교할 대상들은 어디서 관리되어야 할까요?

리액트는 성능의 향상을 위해 vdom을 js 객체 형태로 메모리에 저장합니다. 렌더링 단계에서 이를 꺼내와 실제 dom과 비교를 수행하는 것이죠. 객체를 별도의 메모리에서 관리하기 때문에 자연스럽게 메모리 사용량은 증가하게 됩니다. 렌더링을 위해 메모리를 양보한 꼴이죠.

(2) 트리 비교 알고리즘의 최적화 문제

컴포넌트의 상태 혹은 속성이 변경되었습니다. 리액트 요소 트리가 다시 생성됩니다. 브라우저의 메모리에 저장된 이전의 vdom 스냅샷과 비교를 시작합니다. 이 diffing 알고리즘은 휴리스틱으로 최적화되어있다고는 하나, 휴리스틱을 벗어난 조건에 대해서는 성능 문제가 있을 수 있습니다. 휴리스틱은 해당 맥락에서 가장 최선의 해답을 찾을 뿐, 내놓은 답이 100% 정확하다는 것은 보장할 수 없기 때문입니다.

페이스북은 실제로 휴리스틱한 diffing 알고리즘의 성능 이슈를 인정하고, 리액트 16버전부터 fiber라는 구조체를 도입하여 diffing 알고리즘을 보완하였습니다.

(3) 상태 변경이 적은 경우

Dom의 노드를 변경하면 리플로우/리렌더링이 발생하게 됩니다. 즉, 브라우저가 화면을 렌더링하는 수많은 스텝들을 다시 밟아가야 한다는 의미이죠. 변경하는 노드가 많으면 많을 수록 브라우저가 연산해야할 것들은 많아지고, 결국 렌더링 속도에도 영향을 줄 수 밖에 없습니다. 리액트는 vdom을 사용하여 변경점을 빠르게 찾아내기 때문에 효율적인 렌더링을 합니다. 한가지 의뭉스러운 구석이 있습니다.

노드 변경이 잦지 않은 경우라면? 🤔

vdom을 만들고 비교하는 것도 모두 리소스를 사용하는 일입니다. vdom 스냅샷을 저장하는 것도 메모리 측면에서 추가 비용입니다. 노드 변경이 잦지 않은 경우라면 구태여 추가 비용을 소모하는 일이 효율적이지 않을 수 있습니다. 예를 들어, 정적인 사이트의 경우에는 단순히 dom에 접근하여 ui를 다시 그리는 것이 리액트의 렌더링-커밋 비용보다 적을 수 있습니다.


결론

👍 주어진 상황에 맞게 리액트를 잘 활용하자.

💬 도구는 도구일 뿐 목적과 내용을 먼저 생각하자.

profile
🧐 사용자와 개발자를 모두 배려하고 싶은 개발자. 백엔드부터 임베디드까지 다양하게 개발하다가 지금은 🎨 프런트엔드에 자리잡았어요.

0개의 댓글