React Custom Hooks (1)

HanSungUk·2022년 7월 27일
0

React

목록 보기
11/13
post-thumbnail

React Custom Hooks

현재 코드스테이츠 강의를 통해 프론트엔드를 학습하고 있습니다.
본 포스트는 해당 강의에 대한 내용 정리를 목적으로 합니다.

학습목표

  • Virtual DOM이 나오게 된 배경을 학습합니다.
  • React가 어떻게 Virtual DOM을 사용하는지 학습합니다.
  • Virtual DOM이 어떻게 생겼는지 학습합니다.
  • React가 DOM 트리를 탐색하는 방법에 대해 학습합니다.
  • DOM 엘리먼트의 타입이 같을 때와 다를 때의 React 동작 방식에 대해 학습합니다.

1. Virtual DOM

1-1. Virtual DOM이 나오게 된 배경

  • Real DOM(DOM)
    DOM은 Document Object Model의 약자로 문서 객체 모델을 의미합니다.
    문서 객체브라우저가 JavaScript와 같은 스크립팅 언어가 <html>, <head>와 같은 태그들에 접근하고 조작할 수 있도록 태그들을 트리 구조객체화시킨 것을 의미합니다.
    즉, DOM은 브라우저가 트리 구조로 만든 객체 모델입니다.

이렇게 프로그래밍 언어로 조작하는 DOM은 애플리케이션의 UI 상태가 변경될 때마다 해당 변경 사항을 나타내기 위해 업데이트 됩니다. 만약 이런 DOM을 조작하는 정도가 잦다면 성능에 영향을 미치게 될 것이고, DOM의 렌더링은 브라우저의 구동 능력에 의존하기 때문에 DOM의 조작 속도가 느려질 수 있습니다.

1-1-1. DOM의 조작 속도가 느려지는 정확한 이유는 무엇일까요?

DOM의 구조는 계층적 구조로 되어 있는 트리입니다. 자료 구조 중에서 트리는 "데이터 저장"의 의미보다는 "저장된 데이터를 더 효과적으로 탐색"하기 위해 사용되므로, 빠른 자료 탐색 성능이 장점인 자료라고 할 수 있습니다.
이러한 이유로 트리 구조로 된 DOM은 JavaScript와 같은 스크립팅 언어가 접근하고 탐색하는 속도가 빠르기 때문에 변경 및 업데이트 속도 또한 빠릅니다.

그러나 DOM이 변경되고 업데이트 된다는 것은 결국 브라우저의 렌더링 엔진 또한 리플로우(Reflow)한다는 것을 의미합니다. 즉, 업데이트 된 요소와 그에 해당하는 자식 요소들에 의해 DOM을 재구축함으로써 재랜더링 과정을 거쳐 UI를 업데이트 해야 합니다.

브라우저의 리플로우와 리페인트 과정은 다시금 레이아웃 및 페인트에 해당하는 재연산을 해야하기 때문에 속도가 그만큼 느려지게 됩니다.

결론적으로 JavaScript로 조작하는 DOM의 요소가 많을수록 모든 DOM 업데이트에 대하여 리플로우를 해야하므로 비효율적인 업데이트를 반복할 수 있습니다.

React의 Virtual DOM은 "바뀐 부분만 비교해서 그 부분만 렌더링을 할 수는 없을까?" 아이디어에서 탄생했습니다.

1-2. Virtual DOM 이란

React에는 모든 DOM 객체에 대응하는 가상의 DOM 객체가 있습니다.
가상 DOM 객체는 화면에 표시되는 내용을 실제 DOM 객체처럼 직접 변경하는 것이 아닙니다.

가상 DOM은 가상의 UI 요소를 메모리에 유지시키고, 그 유지시킨 가상의 UI요소를 ReactDOM과 같은 라이브러리를 통해 실제 DOM과 동기화시킵니다.

실제 DOM을 조작하는 것은 실제로 브라우저 화면에 그리기 때문에 느리지만, 가상 DOM을 조작하는 것은 실제 DOM처럼 브라우저 화면에 그리는 것이 아니기 때문에 훨씬 속도가 빠릅니다.

1-2-1. 어떻게 가상 DOM이 더 빠를까요?

React는 새로운 요소가 UI에 추가되면 트리 구조로 표현되는 가상의 DOM이 만들어집니다.
이러한 요소의 상태가 변경이 되면 다시 새로운 가상의 DOM 트리가 만들어집니다.
그리고 이전의 가상 DOM과 변경된 새로운 가상 DOM의 차이를 비교합니다.
그런 다음 트리가 업데이트된 UI를 제공하기 위해 부분적으로 리렌더링됩니다.

이렇게 업데이트된 트리는 실제 DOM으로 한꺼번에 업데이트가 됩니다.
이렇게 하면 실제 DOM은 최소한의 작업만 수행해도 렌더링을 할 수 있습니다.
즉, 실제 DOM의 업데이트 비용을 줄일 수 있기 때문에 더 빠른 렌더링이 가능해집니다.

노란색 원들은 업데이트된 노드, 즉 UI 요소를 의미합니다. React에서는 "상태가 변경된" UI 요소로 볼 수 있습니다.

1-2-2. Virtual DOM의 형태

가상 DOM은 추상화된 자바스크립트 객체의 형태를 가지고 있습니다. 추상화만 되었을 뿐 평범한 자바스크립트 객체이므로 실제 DOM을 건드리지 않고도 필요한 만큼 자유롭게 조작할 수 있습니다.

const vDom = {
	tagName: "html",
	children: [
		{ tagName: "head" },
		{ tagName: "body",
			children: [
				tagName: "ul",
				attributes: { "class": "list"},
				children: [
					{
						tagName: "li",
						attributes: { "class": "list_item" },
						textContent: "List item"
					}
				]
			]
		}
	]
}

2. React Diffing Algorithm

React가 이전의 가상 DOM과 변경된 새로운 가상 DOM을 비교할 때, 두 가지의 가정을 가지고 시간 복잡도 O(n)의 새로운 휴리스틱 알고리즘(Heuristic Algorithm)을 구현해냅니다.

2-1. React가 DOM 트리를 탐색하는 방법

두 가지 가정은 다음과 같습니다.

  1. 각기 서로 다른 두 요소는 다른 트리를 구축할 것이다.
  2. 개발자가 제공하는 key프로퍼티를 가지고, 여러 번 렌더링을 거쳐도 변경되지 말아야 하는 자식 요소가 무엇인지 알아낼 수 있을 것이다.

여기서 React는 비교 알고리즘(Diffing Algorithm)을 사용합니다.

React는 기존의 가상 DOM 트리와 새롭게 변경된 가상 DOM 트리를 비교할 때, 트리의 레벨 순서대로 순회하는 방식으로 탐색합니다. 즉, 같은 레벨(위치)끼리 비교한다는 의미입니다. 이는 너비 우선 탐색(BFS)의 일종이라고 볼 수 있습니다.
React는 이런 식으로 동일 선상에 있는 노드를 파악한 뒤 다음 자식 세대의 노드를 순차적으로 파악해 나갑니다.

2-1-1. 다른 타입의 DOM 엘리먼트인 경우

DOM 트리는 각 HTML 태그마다 각각의 규칙이 있어 그 아래 들어가는 자식 태그가 한정적이라는 특징이 있습니다.(예를 들어 <ul> 태그 밑에 <li>태그만 와야 한다던가, <P>태그 안에 <p>태그를 또 쓰지 못하는 경우) 자식 태그의 부모 태그 또한 정해져 있다는 특징이 있기 때문에, 부모 태그가 달라진다면 React는 이전 트리를 버리고 새로운 트리를 구축해버립니다.

<div>
  <Home />
</div>

// 부모 태그가 div에서 span으로 바뀝니다.
<span>
  <Home />
</span>

부모 노드였던 <div><span>으로 바뀌면 자식 노드인 <Counter />는 완전히 해제(Unmount)됩니다. 즉, 이전 <div>태그 속 <Counter />는 파괴되고 <span> 태그 속 새로운 <Counter />가 다시 실행됩니다.
해제되면 <Count />가 갖고 있던 기존의 state 또한 파괴됩니다.

2-1-2. 같은 타입의 DOM 엘리먼트인 경우

타입이 바뀌지 않는다면 React는 최대한 렌더링을 하지 않는 방향으로 최소한의 변경 사항만 업데이트합니다. 이것이 가능한 이유는 React가 실제 DOM이 아닌 가상 DOM을 사용해 조작하기 때문입니다.
업데이트 할 내용이 생기면 virtual DOM 내부의 프로퍼티만 수정한 뒤, 모든 노드에 걸친 업데이트가 끝나면 그때 단 한번 실제 DOM으로의 렌더링을 시도합니다.

<div className="before" title="stuff"/>

// 기존의 엘리먼트가 태그는 바뀌지 않은채 className만 바뀌었습니다.
<div className="after" title="stuff"/>

여기서 React는 두 요소를 비교했을 때 className만 수정되고 있다는 것을 알게 됩니다.

//className이 before인 컴포넌트
<div style={{color: 'red', fontWeight: 'bold"}} title="stuff" />

//className이 after인 컴포넌트
<div style={{color: 'green', fontWeight: 'bold"}} title="stuff" />

React는 하나의 DOM 노드를 처리한 뒤 해당 노드들 밑의 자식들을 순차적으로 동시에 순회하면서 차이가 발견될 때마다 변경합니다. 이를 재귀적으로 처리한다고 표현합니다.

2-1-3. 키(key)

React는 key라는 속성을 지원합니다. 이는 효율적으로 가상 DOM을 조작할 수 있도록 합니다. 개발할 당시 key라는 속성을 사용하지 않으면 React에서 key값을 달라고 경고문을 띄우는 것도 이 때문입니다. key값이 없는 노드는 비효율적으로 동작할 수 있기 때문입니다.

<ul>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

//key가 2014인 자식 엘리먼트를 처음에 추가합니다.
<ul>
  <li key="2014">Connecticut</li>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

React는 key속성을 통해 2014라는 자식 엘리먼트가 새롭게 생겼고, 2015, 2016키를 가진 엘리먼트는 그저 위치만 이동했다는 걸 알게 됩니다.
따라서 React는 기존의 동작 방식대로 다른 자식 엘리먼트를 변경하지 않고 추가된 엘리먼트만 변경합니다.

key 속성에는 보통 데이터 베이스 상의 유니크한 값(ex.id)을 부여해주면 됩니다. 키는 전역적으로 유일할 필요는 없고, 형제 엘리먼트 사이에서만 유일하면 됩니다.

만약 유니크한 값이 없다면 최후의 수단으로 배열의 인덱스를 key로 사용할 수 있습니다. 하지만 배열이 다르게 정렬될 경우에도 인덱스는 그대로 유지되기 때문에 그 요소가 바뀌어버린다면 React는 배열의 전체가 바뀌었다고 받아드리고 기존의 DOM 트리를 버리고 새로운 DOM 트리를 구출하여 비효율적인 동작을 할 수 있습니다.

0개의 댓글