[TIL] React 심화

lmimoh·2022년 11월 25일
0

TIL

목록 보기
22/26
post-thumbnail

React의 Virtual-DOM ?

React는 DOM의 사본인 Virtual-DOM 을 가지고 있다. 이는 Real-DOM이 동적 UI에 최적화된 Interface가 아니라는 약점을 극복하기 위함이다.

예를 들어서, 모던웹은 사용자에게 동적인 UI를 제공하고 개발자는 이를 구현하기 위해 DOM을 통해 XML, HTML등의 문서에 접근하게 된다.

이때 브라우저는 Real-DOM의 변경사항을 나타내기 위해 렌더링 엔진을 통해 리플로우(Reflow) 하게 된다.

즉, 업데이트된 요소를 반영하기 위해 DOM 트리를 재구축하고 브라우저는 이를 토대로 재렌더링 과정을 거치게 되는 것을 의미한다.

(이때, DOM의 접근마다 변경되는 요소가 아닌 DOM 트리 전체를 재구축한다.)

따라서 동적 UI를 구현하기 위해 프로그래밍 언어로 DOM 조작이 잦아질수록 성능에 영향을 끼치게 된다.


Virtual-DOM의 해결법 ?

이에 React는 DOM 객체와 동일한 속성을 가지고 있지만 실제로 화면에 렌더링시키지는 않는 가상 돔(Virtual-DOM)을 대안으로 내세운다.

React는 DOM 트리의 변경사항이 발생했을 때 이를 반영한 Virtual-DOM을 생성한다.

즉, 동적으로 데이터가 변화했을 때 Real-DOM을 조작하는 것이 아닌 각각의 새로운 Virtual-DOM을 생성한다.

이후 이전의 Virtual-DOM과 새로 생성된 Virtual-DOM을 비교하여 변경된 Node만을 변경하며 이를 조화 과정(reconciliation) 이라고 한다.

최종적으로 반영된 Virtual-DOM을 토대로 Real-DOM을 변경한다.

이는 Real-DOM에 직접 접근했을 때 접근 시 매번 Reflow 과정을 거쳤던 것과 다르게 APP의 모든 변경이 완료된 Virtual-DOM을 토대로 Real-DOM에 한 번 접근한다.

React Fundamental
React Reconciliation


React Diffing Algorithm ?

React가 기존의 가상 DOM과 새로운 가상 DOM을 비교하여 기존의 UI를 효율적으로 갱신하기 위한 알고리즘을 의미 한다. 이는 가장 적은 조작 횟수로 변경사항을 반영한다.

하지만 최초의 해당 알고리즘을 React에 적용할 경우 N개의 Element 반영을 위해서는 N^3번의 비교 연산 을 거쳐야 했다. => O(n^3)

이는 렌더링을 위해 너무 많은 비교 연산이 요구되었기 때문에 React는 두가지의 가정을 통해 O(n)의 복잡도를 가진 새로운 휴리스틱 알고리즘(Heuristic Algorithm)을 구현 한다.

이때, 두 가지 가정은 다음과 같다.

1) 기존의 가상 DOM과 새로운 가상 DOM은 다른 트리를 생성 할 것이다.

2) 개발자는 Key를 제공하고, 해당 프로퍼티를 통해 요소의 변경을 명시적으로 표현 할 수 있다.


React Diffing Algorithm의 동작 원리

React는 기존의 가상 DOM 트리에서 새롭게 변경된 가상 DOM 트리를 비교할 때 트리의 노드 레벨 순서대로 순회 하게 된다.

이는 너비 우선 탐색(BFS)의 일종이라고 볼 수 있고 구체적으로 다음과 같이 탐색하게 된다.

1) 변경된 DOM 엘리먼트의 타입이 다를 경우

DOM 트리는 각 HTML 태그 마다 각각의 규칙이 있어 자식 태그의 종류가 한정적이라는 특징이 있다. (ul, ol 태그 내부에 li 태그가 와야한다 등등..)

이에 React는 DOM 트리 내에서 변경된 엘리먼트의 타입이 변경된 경우 해당 노드와 자식 노드들을 모두 완전히 해제시킨다(파괴).

// 부모 태그 div 가 span으로 변경된 경우
<div>
	<Component />
</div>

// 자식 태그는 변경되지 않았으나
<span>
	<Component />
</span>

// 해당 경우 React는 <div>를 포함한 하위의 노드를 완전 해제(파괴)한 뒤 새로 만들어낸다.

2) 변경된 DOM 엘리먼트의 타입이 동일한 경우

위와 같이 태그의 타입이 변경되지 않은 경우, 태그의 property 혹은 내부 콘텐츠가 변경되었음을 의미 한다.

따라서 React는 최소한의 변경사항만을 업데이트 하기 위해 노드를 유지한 채 해당 property의 변경을 감지하여 수정한다.

또한 이러한 처리를 트리 전체를 순회할 동안 재귀적으로 처리 한다.

// 기존의 태그는 유지된 채
<div style="color : blue;">
  	<Component />
</div>

// 태그의 property만 변경됐을 때
<div style="color : black;">
  <Component />
</div>

// React는 해당 style의 변경만을 감지하여 반영한다.

3) 2)과정을 재귀적으로 처리할 때 효율성 확보

2)의 과정처럼 React가 가상 돔을 비교하여 반영할 때 자식 요소 중 첫번째 요소가 추가된 경우 Recat는 자식 요소가 모두 변경되었다고 인식하고 모든 자식 요소를 새롭게 생성 한다.

// 해당 ul이 변경될 때
<ul>
  <li>list 1</li>
  <li>list 2</li>
  <li>list 3</li>
</ul>

// 자식 요소 중 첫번째 요소가 추가되는 경우
<ul>
  <li>list 0</li>
  <li>list 1</li>
  <li>list 2</li>
  <li>list 3</li>
</ul>

// 이때 <li> 태그들은 같은 레벨에 존재하는 노드들이며, React는 해당 노드를 비교하게 된다.
// ul 태그의 자식 요소에 list 0이라는 새로운 노드가 추가됨으로써 모든 노드들은 한칸씩 밀리게 되고
// React는 ul 태그의 모든 자식 요소를 새롭게 생성하게 된다.

이렇게 동작할 시 React는 최소한의 동작이 아닌 li 태그 3개를 새롭게 생성하기 위해 비효율적인 연산 과정 을 거치게 된다.

이를 해결하기 위해 React는 Key property를 이용 한다.

만약 자식 노드들이 'Key'를 갖고 있다면 React는 해당 property를 이용하여 해당 노드가 새롭게 생겨난 것인지 확인한다.

// 자식 요소 중 첫번째 요소가 추가되는 경우
<ul>
  <li key='3'>list 0</li>
  <li key='0'>list 1</li>
  <li key='1'>list 2</li>
  <li key='2'>list 3</li>
</ul>

// 위와 동일한 예시에서 React는 기존에 존재하지 않은 key=3의 요소만 새로 생성됨을 인식하고
// 나머지 자식 노드를 유지한다.

React Hooks

React Hook은 React 16.8부터 추가된 기능으로 함수형 컴포넌트 내에서 상태 및 다른 여러 기능을 편리하게 사용가능한 메소드를 의미한다.

React 16.8 이전의 과거 React 개발자는 함수형 컴포넌트가 아닌 클래스 컴포넌트를 통해 React App을 구현 했다.

그러나 클래스 컴포넌트는 복잡해질수록 컴포넌트 내부를 이해하기 어려웠고 컴포넌트 사이에서 상태 로직을 재사용하기 어렵다는 단점 이 있었다.

또한, 클래스 내부에서 변수를 선언하고 해당 변수를 클래스 메소드 내부에서 사용하기 위해서는 this 키워드를 반드시 사용해야 했고 이는 클래스 컴포넌트를 이해하는 것을 더 어렵게 만들었다.

여러가지 이유로 React의 컴포넌트는 점진적으로 함수형 컴포넌트로 넘어가게 되었고 함수형 컴포넌트 내에서 상태 및 최적화 기능을 보완하기 위해 Hook 개념이 도입 되었다.


Hook의 기본 원칙

1) Hook은 리액트 함수의 최상위 스코프에서 호출해야 한다.

React는 컴포넌트 내에서 Hook이 여러차례 실행된 경우 Hook의 실행 순서에 따라 저장한다.

그런데 조건문, 반복문 안에서 Hook이 호출되는 경우 React가 Hook을 순서대로 저장하는 것에 어려움이 생길 수 있고 예상치 못한 버그를 초래 할 수 있다.

const Component = () => {
  //Hook이 호출되어야하는 최상위 스코프
  const [value, setValue] = useState(null);
  
  //만약, 최상위 스코프가 아닌
  if(...) {
     //...x
  }
  
  for(...) {
    //...x
  }
  
  // 위의 경우 React의 동작 방식을 거스르므로 오류 코드를 출력한다.
}

2) Hook은 React의 함수 컴포넌트 내에서만 사용되어야 한다.

이는 React Hook을 React의 함수형 컴포넌트가 아닌 클래스 컴포넌트나 일반 JavaScript 내에서 사용할 수 없음을 의미한다.

// 일반 javascript 내에서 Hook을 사용한 예시
window.onload = function() {
  useEffect(() => {
    //do something..
  }, []);
}
//이는 React의 동작 방식을 거스르므로, 오류 코드를 출력한다.

즉, React Hook은 함수형 컴포넌트를 보완하기 위해 생겨난 메소드이므로 근본적으로 React의 함수형 컴포넌트 내부가 아닌 경우 동작할 수 없다.


Custom Hooks ?

React APP을 개발하는 과정에서 반복되는 로직을 Hook 함수로 만들어 사용하는 것을 의미한다.

Custom-Hook을 사용하는 경우 다음과 같은 장점이 존재한다.

1) 상태관리 로직의 재활용이 가능하다.
2) 클래스 컴포넌트보다 적은 양의 코드로 동일한 로직 구현이 가능하다.
=> React-Hook은 function Component에서만 사용가능하다.

예를 들어, 여러 Component에서 사용되는 Fetch API를 Custom Hook으로 만드는 경우

// useFecth 내부
function useFetch(url, [option]) {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    fetch(url, [option])
    	.then(res => res.json())
    	.then(res => setData(res))
    	.catch(err => setData(err))
  }, [url])
  
  return data;
}

// useFetch를 사용하는 컴포넌트 내부
const data = useFetch

// data를 사용하는 로직

이때, useFetch 내부에서 생성되는 상태는 useFetch가 호출되는 컴포넌트 내에서 독립적으로 정의되고 있다.


profile
성장하는 개발자, 이민훈입니다.

0개의 댓글