[번역] 리액트에서 가상 DOM과 차이 비교(diffing)가 어떻게 작동하는지

January·2023년 9월 14일
0

번역

목록 보기
1/3

가상 DOM이 어떻게 동작하는지 이해하려고 노력하고 있었어요. 높은 수준에서는 이해가 됐지만, 더 자세한 설명이 필요했어요.

여러 검색을 해봤지만 원하는 정보를 찾지 못해, 결국 reactreact-dom의 코드를 직접 살펴보기로 했어요. 이렇게 하면 어떻게 동작하는지 더 잘 이해할 수 있을 것 같아요.

그러나 계속하기 전에 직접 DOM에 변경 사항을 렌더링하지 않는 이유에 대해 생각해 보았나요?

다음 섹션에서는 DOM이 어떻게 생성되는지를 요약하고, 왜 React가 처음부터 가상 DOM을 만들었는지에 대한 아이디어를 제공할 것입니다.

DOM이 어떻게 구성되는지 이해하기

DOM이 생성되고 화면에 그려지는 방식에 대해 너무 자세히 다루지는 않겠습니다.

DOM이 변경될 때마다 DOM은 트리 구조로 표현되므로 DOM의 변경은 빠릅니다. 하지만 변경된 요소와 해당 자식 요소는 Reflow/Layout 단계를 거쳐야 하고, 그런 다음 변경 사항을 다시 그려야 하므로 이러한 과정은 느립니다. 따라서 리플로우/리페인트를 해야 하는 항목이 더 많으면 앱이 더 느려집니다.

가상 DOM이 하는 일은 이 두 단계를 최소화하려고 시도하여 크고 복잡한 앱에서 더 나은 성능을 얻는 것입니다.

다음 섹션에서는 가상 DOM이 어떻게 작동하는지 자세히 설명하겠습니다.

가상 DOM 이해하기

이제 DOM이 어떻게 구축되는지 알았으니 이제 가상 DOM에 대해 더 자세히 알아보겠습니다.

여기서 작은 앱을 사용하여 가상 DOM이 어떻게 작동하는지 설명하겠습니다. 이렇게 하면 시각화하기가 더 쉬워질 것입니다.

처음 렌더링 중에 어떻게 작동하는지에 대한 세부 정보에 대해서는 들어가지 않겠습니다. 대신 다시 렌더링될 때 어떤 일이 발생하는지에 중점을 둘 것입니다. 이것을 이해하면 초기 렌더링을 이해하는 것은 정말 쉬워집니다 :)

저가 사용하는 앱의 코드는 이 깃 레포지토리에서 찾을 수 있습니다. 그리고 이것이 우리의 기본 계산기 화면입니다.

Main.jsCalculator.js를 제외하고 레포지토리의 다른 내용은 사소합니다.

// Calculator.js
import React from "react"
import ReactDOM from "react-dom"

export default class Calculator extends React.Component{
	constructor(props) {
		super(props);
		this.state = {output: ""};
	}

	render(){
		let IntegerA,IntegerB,IntegerC;
		

		return(
			<div className="container">						
				<h2>using React</h2>
				<div>Input 1: 
					<input type="text" placeholder="Input 1" ref="input1"></input>
				</div>
				<div>Input 2 :
					<input type="text" placeholder="Input 2" ref="input2"></input>
				</div>
				<div>
					<button id="add" onClick={ () => {
						IntegerA = parseInt(ReactDOM.findDOMNode(this.refs.input1).value)
						IntegerB = parseInt(ReactDOM.findDOMNode(this.refs.input2).value)
						IntegerC = IntegerA+IntegerB
						this.setState({output:IntegerC})
					  }
					}>Add</button>
					
					<button id="subtract" onClick={ () => {
						IntegerA = parseInt(ReactDOM.findDOMNode(this.refs.input1).value)
						IntegerB = parseInt(ReactDOM.findDOMNode(this.refs.input2).value)
						IntegerC = IntegerA-IntegerB
						this.setState({output:IntegerC})

					  }
					}>Subtract</button>
				</div>
				<div>
					<hr/>
					<h2>Output: {this.state.output}</h2>
				</div>
				
			</div>
		);
	}
}
// Main.js
import React from "react";
import Calculator from "./Calculator"

export default class Layout extends React.Component{
	render(){	

		return(
			<div>
			        <h1>Basic Calculator</h1>
				 <Calculator/>
			</div>
		);
	}
}

그리고 초기 로드 후 DOM은 다음과 같이 나타납니다.

그리고 이것이 React가 위의 DOM을 내부적으로 어떻게 컴포넌트 트리 구조로 구축하는지 나타냅니다.

이제 두 숫자를 추가하고 "더하기" 버튼을 클릭하여 더 자세히 이해해보겠습니다

가상 DOM에서 실제 DOM으로 diffing이 어떻게 작동하고 조정(reconciliation)이 이루어지는지 이해하기 위해 계산기에서 100과 50을 입력하고 "더하기" 버튼을 클릭합니다. 기대되는 출력은 150입니다.

Input 1: 100
Input 2: 50
Output : 150

그럼 "더하기" 버튼을 클릭했을 때 무슨 일이 벌어질까요?

우리의 예시에서 "더하기" 버튼을 클릭하면, 새로운 출력 값인 150으로 상태(State)를 설정합니다.

//Calculator.js
<button id="add" onClick={ () => {
      IntegerA = parseInt(ReactDOM.findDOMNode(this.refs.input1).value)
      IntegerB = parseInt(ReactDOM.findDOMNode(this.refs.input2).value)
      IntegerC = IntegerA+IntegerB
      this.setState({output:IntegerC})
       }
     }>Add</button>

컴포넌트를 더티(dirty) 상태로 표시합니다

먼저 컴포넌트가 더티 상태로 표시되는 첫 번째 단계를 이해해 봅시다.

  1. 모든 DOM 이벤트 리스너는 커스텀 React 이벤트 리스너 내에 포장됩니다. 따라서 "더하기"를 클릭하면 이벤트가 리액트 이벤트 리스너로 전달되어 위에서 보는 익명 함수가 실행됩니다.
  2. 익명 함수에서는 this.setState() 함수를 호출하여 새로운 상태 값을 설정합니다.
  3. setState() 함수는 다시 아래 코드에서 보는 것처럼 컴포넌트를 더티 상태로 표시합니다.
//ReactUpdates.js  - enqueueUpdate(component) function
dirtyComponents.push(component);

혹시 이해가 안 가신다면, 왜 React가 버튼을 더티 상태로 표시하지 않고 대신 전체 컴포넌트를 더티 상태로 표시했는지 궁금하실 수 있습니다. 이는 this.setState()로 setState를 호출했기 때문입니다. 여기서 thisCalculator 컴포넌트를 가리킵니다.

  1. 이제 우리 컴포넌트인 Calculator가 더티 상태로 표시되었습니다. 다음에 무엇이 일어나는지 살펴보겠습니다.

컴포넌트 라이프사이클을 따라가보겠습니다

좋아요! 이제 컴포넌트가 더티 상태로 표시되었습니다. 다음은 무엇인가요? 이제 가상 DOM을 업데이트하고 차이 비교 알고리즘을 사용하여 조정을 수행하고 실제 DOM을 업데이트하는 것이 다음 단계입니다.

다음 단계로 넘어가기 전에 컴포넌트의 다양한 라이프사이클에 익숙해지는 것이 매우 중요합니다.

여기가 React에서 우리의 계산기 컴포넌트가 보이는 모습입니다.

다음 단계는 컴포넌트를 업데이트하는 것입니다.

  1. 이 작업은 React가 일괄 업데이트를 실행함으로써 이루어집니다.
  2. 일괄 업데이트에서는 더티 상태로 표시된 컴포넌트가 있는지 확인하고 업데이트를 시작합니다.
//ReactUpdates.js
var flushBatchedUpdates = function () {
  while (dirtyComponents.length || asapEnqueued) {
    if (dirtyComponents.length) {
      var transaction = ReactUpdatesFlushTransaction.getPooled();
      transaction.perform(runBatchedUpdates, null, transaction);
  1. 그 다음으로, 앞으로 업데이트할 보류 중인 상태가 있는지 또는 강제 업데이트가 있는지 확인합니다.
if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
      this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
우리 경우에는 계산기 래퍼 안에 있는 **this._pendingStateQueue**에 새로운 출력 값을 가진 상태 객체가 있습니다.
  1. 먼저 componentWillReceiveProps()를 사용했는지 확인하고 사용했다면 우리는 받은 새로운 propsstate를 업데이트할 수 있습니다.

  2. 다음으로 shouldComponentUpdate()를 사용했는지 확인하고 사용했다면 state 또는 props의 변경으로 컴포넌트를 다시 렌더링해야 하는지 확인할 수 있습니다.

이를 사용하면 컴포넌트를 다시 렌더링할 필요가 없는 시나리오를 알고 있을 때 성능을 향상시킬 수 있습니다.

  1. 다음 단계는 componentWillUpdate(), render(), 그리고 마지막으로 componentDidUpdate()입니다.

    4, 5, 6 단계 중에서 우리는 render()만 사용합니다.

  2. 이제 render() 중에 무슨 일이 일어나는지 더 자세히 살펴보겠습니다.

렌더링은 가상 DOM이 다시 구성되고 차이 비교가 발생하는 곳입니다.

컴포넌트 렌더링 - 가상 DOM 업데이트, 차이 비교 알고리즘 실행 및 실제 DOM 업데이트

예시에서는 컴포넌트 아래의 모든 요소가 다시 가상 DOM에 구축됩니다.

이는 이전과 다음 렌더링된 요소가 동일한 유형과 키를 가지는지 확인한 후, 유형과 키가 일치하는 경우 component를 조정합니다.

var prevRenderedElement = this._renderedComponent._currentElement;
    var nextRenderedElement = this._instance.render(); //Calculator.render() method is called and the element is build.

중요한 점은 여기서 우리의 컴포넌트의 렌더 메서드가 호출된다는 것입니다. 즉, Calculator.render()가 호출됩니다.

일반적으로 조정 프로세스는 다음 단계를 거칩니다.

빨간 점선은 다음 자식 또는 해당 자식 내의 모든 조정 단계가 반복됨을 의미합니다.

내가 준비한 위의 플로우차트는 가상 DOM이 실제 DOM을 업데이트하는 방법에 대한 개요입니다.

의도적이든 아니든 빠뜨린 몇 가지 단계가 있을 수 있지만, 이 다이어그램은 주요 단계 대부분을 다룹니다.

따라서 우리의 예에서 조정이 다음과 같이 수행됨을 볼 수 있습니다:

Output: 150으로 DOM을 업데이트하고 이전 <div>의 조정 단계를 건너 뛰면서 당신을 안내해 드리겠습니다.

  • 조정 작업은 컴포넌트의 메인 <div>부터 시작됩니다. 이 <div>에는 class="container"가 있습니다.

  • <div>의 자식 요소는 Output를 포함하는 <div>입니다. 따라서 리액트는이 자식 요소의 조정을 시작합니다.

  • 이제 이 자식 요소에는 자체의 자식 요소 <hr><h2>가 있습니다.

  • 그래서 리액트는 <hr>의 조정을 시작합니다.

  • 다음으로 <h2>의 조정을 시작하게 됩니다. <h2>는 자체적으로 Output:상태에서 나온 결과라는 두 가지 텍스트를 가지고 있습니다. 그러므로 이 두 가지를 위한 조정이 시작됩니다.

  • 먼저 Output: 텍스트가 조정되고 변경 사항이 없으므로 DOM에 아무 일도 일어나지 않습니다.

  • 다음으로 상태에서 나온 결과는 조정되고 이제 새로운 값이 있으므로 150으로 업데이트됩니다.

실제 DOM 렌더링

우리 예제에서 조정 중에는 아래와 같이 Output 필드만 변경되며 개발자 콘솔에서 paint flashing이 켜집니다.

그리고 실제 DOM에서 업데이트된 구성 요소 트리입니다.

결론

이 예제가 매우 단순하긴 하지만 React 내부에서 어떤 일이 벌어지는지 기본적인 통찰력을 제공할 것입니다.

더 복잡한 앱을 다루지 않은 이유는 컴포넌트 트리 전체를 그리는 것이 정말 지루했기 때문입니다. :-|

재조정 프로세스는 React에서 다음과 같은 일들을 수행합니다.

  1. 이전 내부 인스턴스와 다음 내부 인스턴스를 비교합니다.
  2. JavaScript 객체(Virtual DOM) 구조의 내부 인스턴스를 업데이트합니다.
  3. 실제 변경 사항이 있는 노드와 해당 자식 노드만 실제 DOM에서 업데이트합니다.

원글

기술 설명 문맥이 자연스럽기 위해 chat-GPT를 사용해 번역했습니다.

0개의 댓글