[React] 심화(1)

이성은·2023년 1월 20일
0
post-custom-banner

1. React 심화

학습목표

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

1-1. Virtual DOM

  • Real DOM (DOM)
    • DOM은 Document Object Model의 약자, 문서 객체 모델, 즉 DOM은 브라우저가 트리 구조로 만든 객체 모델이다.
    • 문서 객체란 브라우저가 JavaScript와 같은 스크립팅 언어가 <html>, <head>, <body>와 같은 태그들에 접근하고 조작할 수 있도록 태그들을 트리 구조로 객체화 시킨 것을 의미한다.
    • 트리 구조로 DOM 객체가 이뤄져 있기 때문에 JavaScript는 쉽게 DOM 객체에 접근할 수 있고, DOM 객체를 조작할 수 있다.
    • 프로그래밍 언어로 조작하는 DOM은 애플리케이션의 UI 상태가 변경될 때마다 해당 변경 사항을 나타내기 위해 업데이트가 된다. => DOM을 조작하는 정도가 잦다면 성능에 영향을 미치게 될 것이고, DOM의 렌더링은 브라우저의 파워, 즉 브라우저의 구동 능력에 의존하기 때문에 DOM의 조작 속도는 느려지게 된다.
    • DOM의 조작 속도가 느려지는 이유
      • 트리 구조로 된 DOM은 “데이터 저장"의 의미보다는 “저장된 데이터를 더 효과적으로 탐색”하기 위해 사용되므로, 빠른 자료 탐색 성능이 장점인 자료구조 => 변경 및 업데이트 속도 빠르다.
      • DOM의 변경, 업데이트 => 브라우저의 렌더링 엔진 또한 리플로우(Reflow)한다는 것을 의미 => 즉 업데이트 된 요소와 그에 해당하는 자식 요소들에 의해 DOM 트리를 재구축함으로써 재랜더링 과정을 거쳐 UI를 업데이트 해야한다 => 다시금 레이아웃 및 페인트에 해당하는 재연산을 해야 하기 때문에 속도가 그만큼 느려지게 된다.
      • 대부분의 JavaScript 프레임워크는 필요 이상으로 DOM을 업데이트 시킨다.
      • 계속해서 이런 비효율적인 업데이트를 반복한다면 극단적인 예로 프레임 드랍(frame drop)과 같은 치명적인 UX 문제가 발생할 수 있다.
  • Virtual DOM
    • React에는 모든 DOM 객체에 대응하는 가상의 DOM 객체가 있다.
    • Real DOM과 동일한 속성을 가지고 있음에도 가벼운 사본, 다만 가상 DOM 객체는 화면에 표시되는 내용을 실제 DOM 객체처럼 직접 변경하는 것은 아니다.
    • 가상 DOM은 가상의 UI 요소를 메모리에 유지시키고, 그 유지시킨 가상의 UI 요소를 ReactDOM과 같은 라이브러리를 통해 실제 DOM과 동기화시킨다.
    • 가상 DOM이 더 빠른 이유
      • 요소의 상태가 변경이 되면 다시 새로운 가상의 DOM 트리가 만들어진다. => 이전의 가상의 DOM과 이후의 가상의 DOM의 차이를 비교 => 가상의 DOM은 실제 DOM에 변경을 수행할 수 있는 최상의 방법을 계산 =>실제 DOM은 최소한의 작업만 수행해도 렌더링을 할 수 있게 된다.
      • 즉 더 빠른 렌더링이 가능해지고, 업데이트 비용을 줄일수 있다.
    • Virtual DOM의 형태
      추상화된 자바스크립트 객체의 형태, HTML 문서 객체를 기반
      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"
      					}
      				]
      			]
      		}
      	]
      }

1-2. React Diffing Algorithm

  • 하나의 트리를 다른 트리로 변형을 시키는 가장 작은 조작 방식
  • React는 두 가지의 가정을 가지고 시간 복잡도 O(n)의 새로운 휴리스틱 알고리즘(Heuristic Algorithm)을 구현
    1. 각기 서로 다른 두 요소는 다른 트리를 구축할 것이다.
    2. 개발자가 제공하는 key 프로퍼티를 가지고, 여러 번 렌더링을 거쳐도 변경되지 말아야 하는 자식 요소가 무엇인지 알아낼 수 있을 것이다.

< React가 DOM 트리를 탐색하는 방법 >

  • 트리의 레벨 순서대로 순회하는 방식 즉 같은 레벨(위치)끼리 비교한다. => 너비 우선 탐색(BFS)의 일종
  • 동일 선상에 있는 노드를 파악한 뒤 다음 자식 세대의 노드를 순차적으로 파악
  • 다른 타입의 DOM 엘리먼트인 경우
    • DOM 트리는 각 HTML 태그마다 각각의 규칙이 있어 그 아래 들어가는 자식 태그가 한정적이라는 특징이 있다.
      자식 태그의 부모 태그 또한 정해져 있다는 특징이 있기 때문에, 부모 태그가 달라진다면 React는 이전 트리를 버리고 새로운 트리를 구축해버린다. => 부모 태그가 바뀌어버리면, React는 기존의 트리를 버리고 새로운 트리를 구축하기 때문에 이전의 DOM 노드들은 전부 파괴
      => 새로운 컴포넌트가 실행되면서 기존의 컴포넌트는 완전히 해제(Unmount)되어버리기 때문에 <Counter />가 갖고 있던 기존의 state 또한 파괴
<div>
	<Counter />
</div>

//부모 태그가 div에서 span으로 바뀝니다.
<span>
	<Counter />
</span>
  • 같은 타입의 DOM 엘리먼트인 경우
    • React는 최대한 렌더링을 하지 않는 방향으로 최소한의 변경 사항만 업데이트
    • 업데이트 할 내용이 생기면 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" />

=> className before와 after는 각자 이런 스타일을 갖고 있다고 하면, React는 color 스타일만 수정하고 fontWeight 및 다른 요소는 수정하지 않는다. 이렇게 하나의 DOM 노드를 처리한 뒤 React는 뒤이어서 해당 노드들 밑의 자식들을 순차적으로 동시에 순회하면서 차이가 발견될 때마다 변경한다. 이를 재귀적으로 처리한다고 표현한다.

  • 자식 엘리먼트의 재귀적 처리
<ul>
  <li>first</li>
  <li>second</li>
</ul>

//자식 엘리먼트의 끝에 새로운 자식 엘리먼트를 추가했습니다.
<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>

=>React는 자식 노드를 순차적으로 위에서부터 아래로 비교하면서 바뀐 점을 찾는다.
그렇기 때문에 예상대로 React는 첫 번째 자식 노드들과 두 번째 자식 노드들이 일치하는 걸 확인한 뒤 세 번째 자식 노드를 추가.

<ul>
 <li>Duke</li>
 <li>Villanova</li>
</ul>

//자식 엘리먼트를 처음에 추가합니다.
<ul>
 <li>Connecticut</li>
 <li>Duke</li>
 <li>Villanova</li>
</ul>

=> React는 우리의 기대대로 최소한으로 동작하지 못하게 된다. React는 원래의 동작하던 방식대로 처음의 노드들을 비교하게 된다.
=> 처음의 자식 노드를 비교할 때,React는 리스트 전체가 바뀌었다고 받아들인다.
즉 전부 버리고 새롭게 렌더링 해버린다.이는 굉장히 비효율적인 동작 방식이다.

  • React는 이 문제를 해결하기 위해 key라는 속성을 지원
  • 키(key)
    • 만약 자식 노드들이 이 key를 갖고 있다면, React는 그 key를 이용해 기존 트리의 자식과 새로운 트리의 자식이 일치하는지 아닌지 확인할 수 있다.
      => 따라서 React는 기존의 동작 방식대로 다른 자식 엘리먼트는 변경하지 않고 추가된 엘리먼트만 변경
    • key 속성에는 보통 데이터 베이스 상의 유니크한 값(ex. Id)을 부여, 키는 전역적으로 유일할 필요는 없고, 형제 엘리먼트 사이에서만 유일하면 된다.
    • 만약 이런 유니크한 값이 없다면 최후의 수단으로 배열의 인덱스를 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>

2. React Hooks

학습목표

  • 함수 컴포넌트와 클래스 컴포넌트의 차이를 학습하고, 함수 컴포넌트에서 Hook을 사용하는 이유를 이해합니다.
  • Hook의 사용 규칙에 대해 학습하고 이해합니다.
  • useMemo의 쓰임새와 작성 방법에 대해 학습합니다.
  • useCallback의 쓰임새와 작성 방법에 대해 학습합니다.
  • custom hooks의 쓰임새와 작성 방법에 대해 학습합니다.

2-1. Component와 Hook

  • Class Component
    • 함수 컴포넌트 이전에는 클래스(class) 컴포넌트사용
    • 복잡해질수록 이해하기 어려워졌고, 컴포넌트 사이에서 상태 로직을 재사용하기 어렵다는 단점
    • React의 클래스 컴포넌트를 사용하기 위해서는 JavaScript의 this 키워드 사용
    • 함수 컴포넌트는 클래스 컴포넌트와는 다르게 상태 값을 사용하거나 최적화할 수 있는 기능들이 조금 미진했는데, 그 부분들을 보완하기 위해 Hook이라는 개념을 도입
  • Function Component
    • 함수형 컴포넌트는 클래스형 컴포넌트에 비해 훨씬 더 직관적이고, 보기 쉽다.
    • 상태값을 저장하고 사용할 수 있게 해주는 useState()가 Hook
    • state는 컴포넌트가 리렌더링 되어도 그대로 유지
function Counter () {
    const [counter, setCounter] = useState(0);
// Counter 컴포넌트에서 useState() Hook을 호출해 함수 컴포넌트(function component) 안에 state를 추가한 형태
    const handleIncrease = () => {
        setCounter(counter + 1)
    }

    return (
        <div>
            <p>You clicked {counter} times</p>
            <button onClick={handleIncrease}>
                Click me
            </button>
        </div>
    )
}
  • Hook
    • Hook은 다르게 말하면 함수형 컴포넌트에서 상태 값 및 다른 여러 기능을 사용하기 편리하게 해주는 메소드
  • Hook 사용 규칙
    1. 리액트 함수의 최상위에서만 호출해야 한다.
    반복문, 조건문, 중첩된 함수 내에서 Hook 실행 X
    2. 오직 리액트 함수 내에서만 사용되어야 한다.
    리액트 함수형 컴포넌트나 커스텀 Hook에서만 사용

2-2. useMemo

  • 컴포넌트는 기본적으로 상태가 변경되거나 부모 컴포넌트가 렌더링이 될 때마다 리렌더링을 하는 구조, 하지만 너무 잦은 리렌더링은 앱에 좋지않은 성능을 끼친다.
  • 렌더링 최적화를 위한 Hook
  • 특정 값(value)를 재사용하고자 할 때 사용하는 Hook
function Calculator({value}){

	const result = calculate(value);

	return <>
      <div>
					{result}
      </div>
  </>;
}

=>calculate가 내부적으로 복잡한 연산을 해야 하는 함수라 계산된 값을 반환하는 데에 시간이 몇 초 이상 걸린다고 가정하면, 해당 컴포넌트는 렌더링을 할 때마다 이 함수를 계속해서 호출 =>그 때마다 시간이 몇 초 이상 소요, 렌더링 지연

/* useMemo를 사용하기 전에는 꼭 import해서 불러와야 합니다. */
import { useMemo } from "react";

function Calculator({value}){

	const result = useMemo(() => calculate(value), [value]);

	return <>
      <div>
					{result}
      </div>
  </>;
}

=> 렌더링을 할 때마다 이 value값이 계속 바뀌는 게 아니라고 가정
=> 그럼 이 값을 어딘가에 저장을 해뒀다가 다시 꺼내서 쓸 수만 있다면 굳이 calculate 함수를 호출할 필요도 없을 것이다. 여기서 useMemo Hook을 사용
=> useMemo를 호출하여 calculate를 감싸주면, 이전에 구축된 렌더링과 새로이 구축되는 렌더링을 비교해 value값이 동일할 경우에는 이전 렌더링의 value값을 그대로 재활용

  • Memoization
    • 기존에 수행한 연산의 결과값을 메모리에 저장을 해두고, 동일한 입력이 들어오면 재활용하는 프로그래밍 기법
    • 적절히 사용한다면 굳이 중복 연산을 할 필요가 없기 때문에 앱의 성능을 최적화
    • useMemo는 바로 이 개념을 이용하여 복잡한 연산의 중복을 피하고 React 앱의 성능을 최적화
  • 실습
    • 컴포넌트는 기본적으로 상태가 변경되거나 부모 컴포넌트가 렌더링이 될 때마다 리렌더링을 한다. 그러나 너무 잦은 리렌더링은 앱에 좋지 않은 성능을 끼친다.
    • 실제로 연산 로직에 영향을 주는 값은 val1val2
    • 이름 상태가 변화하면 add 함수가 계속 같은 결괏값을 리턴함에도 불구하고 불필요하게 계속 호출되고 있기 때문에, useMemo를 이용하여 add 함수의 호출을 최소화해야만 한다. 즉 이름을 입력할 때는 add 함수가 호출되지 않아야 최적화가 된 컴포넌트라고 볼 수 있다.
      => 이름 상태가 변화하면, val1,val2는 일정한 값인데도 add가 계속 호출돼서 콘솔창에 숫자가 들어옵니다가 계속 찍힌다.
      =>즉 이름 상태가 변화할때, useMemo를 사용해서val1,val2를 저장해놨다가 이전 렌더링의 val1,val2를 사용하도록 한다.
//App.js
import React, { useState, useMemo } from "react";
import "./styles.css";
import { add } from "./add";


export default function App() {
  const [name, setName] = useState("");
  const [val1, setVal1] = useState(0);
  const [val2, setVal2] = useState(0);
  const answer = useMemo(()=> add (val1,val2), [val1,val2])
  // const answer = add(val1, val2);
  return (
    <div>
      <input
        className="name-input"
        placeholder="이름을 입력해주세요"
        value={name}
        type="text"
        onChange={(e) => setName(e.target.value)}
      />
      <input
        className="value-input"
        placeholder="숫자를 입력해주세요"
        value={val1}
        type="number"
        onChange={(e) => setVal1(Number(e.target.value))}
      />
      <input
        className="value-input"
        placeholder="숫자를 입력해주세요"
        value={val2}
        type="number"
        onChange={(e) => setVal2(Number(e.target.value))}
      />
      <div>{answer}</div>
    </div>
  );
}

//add.js
export const add = (num1, num2) => {
  console.log("숫자가 들어옵니다.");
  return Number(num1) + Number(num2);
};

2-3. useCallback

  • 렌더링 최적화를 위한 Hook
  • 메모이제이션 기법을 이용한 Hook, 함수의 재사용을 위해 사용
  • useMemo에 비해 괄목할 만한 최적화 X
  • 단순히 컴포넌트 내에서 함수를 반복해서 생성하지 않기 위해서 사용 X
  • 자식 컴포넌트의 props로 함수를 전달해줄 때 useCallback을 사용
  • useCallback과 참조 동등성
    • React는 JavaScript 언어로 만들어진 오픈소스 라이브러리이기 때문에 기본적으로 JavaScript의 문법을 따라간다.
    • JavaScript에서 함수는 객체, 객체는 메모리에 저장할 때 값을 저장하는 게 아니라 값의 주소를 저장하기 때문에, 반환하는 값이 같을 지라도 일치연산자로 비교했을 때 false가 출력
    • React는 리렌더링 시 함수를 새로이 만들어서 호출
      => 새로이 만들어 호출된 함수는 기존의 함수와 같은 함수가 아니다.
    • 하지만 useCallback을 이용해 함수 자체를 저장해서 다시 사용하면 함수의 메모리 주소 값을 저장했다가 다시 사용한다는 것과 같다고 볼 수 있다.
      따라서 React 컴포넌트 함수 내에서 다른 함수의 인자로 넘기거나 자식 컴포넌트의 prop으로 넘길 때 예상치 못한 성능 문제를 막을 수 있다.
function doubleFactory(){
    return (a) => 2 * a;
}
  
const double1 = doubleFactory();
const double2 = doubleFactory();
  
double1(8); // 16
double2(8); // 16
  
double1 === double2;  // false
double1 === double1;  // true
/*double1과 double2는 동일한 코드를 공유하더라도 메모리 주소가 다르기 때문에,
메모리 주소에 의한 참조 비교 시 다른 함수로 본다 */
  • useCallback를 이용하여 앱 최적화하기
    • 옆의 button dark mode를 누르면 “아이템을 가져옵니다.”가 콘솔에 출력되는 걸 볼 수 있다.
    • 이 동작의 이유는 버튼을 누를 때도 앱이 리렌더링 되므로, App 내부의 getItems() 함수가 다시 만들어진다.
    • 새로이 만들어진 함수는 이전의 함수와 참조 비교시 다른 함수이기 때문에 List 구성 요소 내에서 useEffectsetItems를 호출하고 종속성이 변경됨에 따라 “아이템을 가져옵니다.”를 출력하는 것이다.
//App.js
import { useState, useCallback} from "react";
import "./styles.css";
import List from "./List";

export default function App() {
  const [input, setInput] = useState(1);
  const [light, setLight] = useState(true);

  const theme = {
    backgroundColor: light ? "White" : "grey",
    color: light ? "grey" : "white"
  };

  /* 이전 코드 
	const getItems = () => {
		return [input + 10, input + 100];
	}; 
	*/	
  const getItems = useCallback(() => [input + 10, input + 100],[input]);

  const handleChange = (event) => {
    if (Number(event.target.value)) {
      setInput(Number(event.target.value));
    }
  };

  return (
    <>
      <div style={theme} className="wall-paper">
        <input
          type="number"
          className="input"
          value={input}
          onChange={handleChange}
        />
        <button
          className={(light ? "light" : "dark") + " button"}
          onClick={() => setLight((prevLight) => !prevLight)}
        >
          {light ? "dark mode" : "light mode"}
        </button>
        <List getItems={getItems} />
      </div>
    </>
  );
}

// List.js
import { useState, useEffect } from "react";

function List({ getItems }) {
  /* Initial state of the items */
  const [items, setItems] = useState([]);

  /* This hook sets the value of items if 
     getItems object changes */
  useEffect(() => {
    console.log("아이템을 가져옵니다.");
    setItems(getItems());
  }, [getItems]);

  /* Maps the items to a list */
  return (
    <div>
      {items.map((item) => (
        <div key={item}>{item}</div>
      ))}
    </div>
  );
}

export default List;

=> 버튼을 눌러 리렌더링이 되어도, 자식 컴포넌트의 props로 전해줄 getItems()함수가 useCallback을 이용해 함수 자체를 저장
=> 함수의 메모리 주소 값이 저장돼서 같은함수이기 때문에 React가 List 구성 요소 내에서 useEffect은 setItems를 호출하지 않는다.

profile
함께 일하는 프론트엔드 개발자 이성은입니다🐥
post-custom-banner

0개의 댓글