본문 바로가기

FrontEnd

리액트와 가상돔(virtual dom)

반응형

리액트의 렌더 / 커밋과 virtual dom 사이의 관계를 알아봅시다.

 

virtual Dom이라는 용어는 리액트 공식 문서의 단 한곳에서만 사용되며 설명도 길지 않습니다.

(https://reactjs.org/docs/faq-internals.html)

즉, 리액트 사용자는 전혀 몰라도 되는 implementation Detail일 뿐입니다.

리액트는 렌더링 요청을 pulling한 후 적절한 시점에 렌더링을 대신해주는 UI 라이브러리일 뿐입니다.

...
일부 인기 있는 라이브러리는 새 데이터를 사용할 수 있을 때 계산이 수행되는 "푸시" 접근 방식을 구현합니다. 
React는 필요할 때까지 계산이 지연될 수 있는 "풀(pull)" 접근 방식을 고수합니다.
...
React가 코드 제어를 가져오기 전에 실행되는 사용자 코드의 양을 최소화하는 것이 React의 핵심 목표입니다.
이를 통해, React는 UI에 대해 코드가 알고 있는 내용에 따라 작업을 스케줄링 하고, 분해할 수 있습니다.
...
React는 완전히 "reactive"한 것을 원하지 않기 때문에
React를 "scuedule"이라고 불렀어야 한다는 팀 내부 농담이 있습니다.
- 공식 문서

구현 세부 사항일 뿐이라면서, 왜 버추얼 돔을 알아야 하나요?

이미 알고계시겠지만, 이펙트는 실제 Dom 트리가 document에 존재하냐 존재하지 않느냐에 따라 실행 결과가 달라집니다.

이펙트의 실행 결과를 디버깅하기 위해선 컴포넌트, 리액트의 라이프사이클,  Virtual DOM의 조정(reconcilation),

정확하게는 코드의 실행 시점의 상태를 알아야 합니다


VueJs와 렌더 파이프라인

Vue.js의 렌더링에 대해 잠깐 알아보겠습니다.

Virtual Dom을 이용하는 부분에 있어 VueJs를 살펴보는 것은 꽤 도움이 됩니다.

먼저 변화가 발생하면, 뷰의 경우 변화가 필요한 곳만 추적해서 큐잉합니다.

리액트의 경우 컴포넌트를 렌더링하고 이펙트를 큐잉합니다.

리액트와 뷰는 버추얼 돔을 사용합니다

즉, 실제 돔에 변화된 부분만 반영하는 부분은 거의 동일하다고 봐도 무방하며,

버츄얼 돔이 실제 돔에 반영되기 전후에 사이드 이펙트를 실행할 수 있다는 것도 동일합니다.

 

VueJs에 관한 내용은 해당 문단에서만 다루고 넘어가겠습니다

VueJs의 공식 문서에서는 마운트, 언마운트를 각각

  • 컴포넌트가 처음 Actual Dom에 반영 되었을때,
  • Actual Dom에서 제거되었을 때로 설명합니다.

또한 해당 컴포넌트의 라이프 사이클에 콜백을 등록해 원하는 시점에 이펙트를 실행할 수 있습니다.

마지막으로 중요한 문단이 있어 짚고 넘어갑니다

가상 DOM 트리의 복사본이 두 개 있는 경우
렌더러는 두 트리를 살펴보고 비교하여 차이점을 파악하고 이러한 변경 사항을 실제 DOM에 적용할 수 있습니다.
이 프로세스를 "patch", "diffing" 또는 "reconciliation"이라고도 합니다.

즉 reconcilation, diffing이라는 개념은 Actual dom에 가상 돔의 표현을 반영하는 과정을 포함합니다.

 

참고 : 


렌더와 커밋

리액트의 렌더링은 변화를 추적하며 사용자의 코드를 동기적으로 실행하는 시점이며,

커밋은 변화를 Actual Dom에 반영하고, 리액트가 적절한 때에 사용자의 코드를 실행해 주는 시점입니다.

아래에 매우 적절한 이미지가 있네요.

(보통 렌더 / 커밋 두 단계(phase)로 나누고

위 한단계를 커밋,

아래 세 단계를 커밋으로 퉁쳐서 설명하는데,

아래 그림은 좀 더 세부적으로 나누었네요)

해당 이미지는 https://medium.com/trabe/react-useref-hook-b6c9d39e2022 에서 가져왔습니다.


useEffect와 useLayoutEffect

이게 바로 React 같은 라이브러리들이 유용한 이유입니다.
이런 라이브러리들은 여러분이 항상 UI를 처음부터 새로 만든다는 사고방식을 가지게 해줍니다.
...
이것이 우리가 실수가 누적되지 않도록 함으로써 피할 수 없는 엔트로피와 싸울 수 있는 방법입니다.
이것은 "재부팅"와 동일한 코딩이며 놀라울 정도로 잘 동작합니다.

- 댄 아브라모프 : the-bug-o-notation

생산성을 높이려면 "이펙트 관점"에서 생각해야 하며,
이 멘탈 모델은 라이프사이클 이벤트에 응답하는 것보다
렌더랑과 이펙트의 동기화를 구현하는 데 더 가깝습니다.
...
이펙트는 React 데이터 흐름의 일부가 됩니다.
count state 변수는 number일 뿐입니다.
그것은 마법의 "data binding", "watcher", "proxy" 또는 다른 것이 아닙니다.
단지 js number입니다
훅의 멘탈 모델에서 effect 함수는 이벤트 핸들러와 마찬가지로 특정 렌더링에 속합니다.
개념적으로, effect는 렌더링 결과의 일부라고 생각할 수 있습니다.
React: ...UI 업데이트.중입니다. 브라우저 씨, DOM을 변경했어요.
Browser: 좋습니다. 변경 사항을 화면에 paint 했어요
React: 좋아요, 방금 렌더링에 포함된 이펙트를 실행할게요
React는 브라우저가 페인트 한 후에만 효과를 실행합니다.
이렇게 하면 대부분의 효과가 화면 업데이트를 차단할 필요가 없으므로 앱이 더 빨라집니다.
이전 효과 정리도 지연됩니다.
이전 효과는 새 prop을 이용해 렌더링을 수행한 후 정리(cleanded Up)됩니다
React에서 내가 가장 좋아하는 것 중 하나는 초기 렌더링 결과와 업데이트 결과를 동일하게 설명할 수 있다는 것입니다.
이것은 프로그램의 엔트로피를 줄입니다.
...
리액트는 여행 여정이 아니라 목적지에만 관심이 있습니다.
여정이 다르다고 정해진 목적지가 달라진다면 버그입니다.
$.removeClass 는 여정입니다.
JSX는 목적지 입니다.

- 댄 아브라모프 : a complete guide to useeffect/

좀 더 자세한 설명은 so-what-about-cleanup 을 참고하세요!


지금까지 내용은 사실 해당 문단을 작성하기 위한 빌드업이었습니다.

리액트는 VueJs와 달리 렌더링(UI)과 X의 동기화라는 멘탈 모델을 사용합니다.

즉 모든 효과는 UI를 기반으로 실행됩니다.

이게 무슨 말일까요?

리액트는 항상 모든 효과를 실제 돔과 가상 돔의 조정(reconcilation)이 완료된 후에 실행합니다.

useEffect 함수의 본문 내부에서 호출되는 로직은, 해당 컴포넌트 렌더링 결과를 Dom과 동기화 한 상태에서 실행됩니다.

  • VueJs의 onMount를 에뮬레이션 하려면 useEffect(fn, [])을 사용합니다.
  • mount라는 것은 컴포넌트의 표현이 실제 돔에 없었다 생긴 것을 의미합니다.

useEffect 함수의 클린업 함수 호출되는 로직도, 해당 컴포넌트 렌더링 결과를 Dom과 동기화 한 상태에서 실행됩니다.

  • 업데이트 혹은 언마운트 시에 호출됩니다.
  • 이전 렌더링의 이펙트와 다음 렌더링의 이펙트는 별개입니다. 이펙트는 렌더링 결과의 일부입니다. 클린업을 잊지마세요
  • 언마운트 시 언마운트 대상 컴포넌트는 렌더링 되지 않으며, 컴포넌트의 표현은 더이상 Actual Dom 위에 존재하지 않습니다.
    • 하지만 이전 렌더링 결과에 존재하는 클린업 이펙트는 리액트에 의해 호출됩니다.

위 그림의 useLayoutEffect와 useEffct와의 차이가 뭘까요?

useLayoutEffect는 React가 모든 DOM 변형을 수행한 직후 동기적으로 한번에 실행됩니다.
이것은 DOM 측정(예: 엘리먼트의 스크롤 위치 또는 엘리먼트의 스타일 가져오기)을 수행한 다음 DOM 조작을 수행하거나,
상태를 업데이트하여 동기식 리렌더링을 트리거해야 하는 경우에 유용할 수 있습니다.

리액트가 virtual dom을 이용해 dom 변경을 배치 처리하면, 화면에 렌더링 할 actual dom이 준비됩니다.

만약 더 실행할 코드가 없다면, 브라우저는 렌더 트리를 이용해 paint 하겠죠?

이 paint를 실행하기 이전,

렌더링에 필요한 모든 정보를 갖고 있는 dom을 이용해 이펙트를 동기적으로 먼저 실행하는 것이 LayoutEffect입니다.

즉 레이아웃 > 페인트 단계에서 레이아웃을 차단하고 효과를 실행한다 볼 수 있겠네요.

 

useEffect는 브라우저의 페인트 이후에 이펙트가 실행됩니다.

useLayoutEffect: DOM을 변경해야 하거나 Dom 관련 측정(measure)을 수행해야 하는 경우,
useEffect : 그 외 전부 (Dom과 상호작용(변경)하지 않거나, 변경사항을 관찰하지 않는 경우)

kent c dodds : useeffect vs uselayouteffect

결론

리액트는 상태 / 이펙트와 UI의 동기화 멘탈 모델을 사용합니다.

훅 아키텍처가 등장한 이후로 컴포넌트 라이프사이클이란 개념은 명시적으로 드러나지 않습니다. (또는 없어졌다 볼 수 있겠네요)

물론 리액트는 초기 마운트와 업데이트 시 다른 로직을 작성하는 것을 지양하는 멘탈 모델을 갖고 있습니다.

하지만, Actual Dom에 컴포넌트의 표현이 존재하느냐 존재하지 않느냐에 따라 생각했던 것과 다르게 동작할 수 있으며,

그 원인은 가상돔과 렌더 / 커밋 페이즈에서 기인합니다.

 

리액트 컴포넌트를 작성하면서

어떤 위치에서 어떤 함수를 실행할 때,

해당 함수의 실행 결과가 렌더링 결과에 어떻게 영향을 미칠지,

해당 함수가 해당 위치에 렌더링 결과 생성된 Actual Dom에 접근할 수 있을지를 판단하는데 도움이 되었길 바랍니다.

 

 

 

반응형