<React> 리액트의 백그라운드 작업 / React.memo

·2023년 7월 1일
0

React

목록 보기
12/23

리액트의 백그라운드 작업

리액트가 실제로 작동하는 방식

컴포넌트의 재평가 !== DOM의 리렌더링

컴포넌트가 재평가된다고 해서 실제 DOM이 리렌더링 되는 것은 아니다.

리액트에 의해 컴포넌트 함수가 재실행된다고 해서 실제 DOM의 각 부분들이 다시 랜더링된다던가 재평가되는 것은 아니다.

컴포넌트 부분과 리액트 부분, 그리고 실제 DOM을 구분할 줄 알아야한다.

Component Real DOM의 비교

  • Component
    - State, props, Context가 변경될 때 재실행하고 재평가된다. 이 때 React는 컴포넌트 함수를 다시 실행한다.

  • Real DOM
    - 리액트가 구성한 컴포넌트의 이전 상태와 트리, 그리고 현재 상태간의 차이점을 기반으로 변경이 필요할 때만 업데이트 된다.

    즉, Real DOM은 필요한 경우에만 변경된다.
    이 부분은 성능 측면에서 매우 중요하다.

    이전과 현재의 상태를 가상으로 비교한다는 것은 간편하고, 자원도 적게 들기 때문.
    이 작업은 메모리 안에서만 작동한다.

    실제 DOM을 사용하게 된다면,(= 브라우저에 직접 렌더링 하게 된다면) 성능 측면에서 자원이 많이 필요하게 된다.

    • 여러 곳에서 소규모 변경이 발생한다면 그만큼 실제 DOM을 많이 사용하게 되므로 페이지가 느려지는 성능 부하가 많이 발생할 수 있다.

      따라서 React는 가상 DOM과의 비교를 통해 최종 스냅샷과 현재의 스냅샷을 실제 DOM에 전달하는 방식을 가진다.

Virtual DOM Diffing

이전 Component

<div>
  	<h1>저는 신입 개발자입니다.</h1>
</div>

----------------------------------

수정 후 Component

<div>
  	<h1>저는 신입 개발자입니다.</h1>
	<p>열심히 하되 잘하도록 하겠습니다.</p> // ✅ 이전 Component에 추가로 생성됨.
</div>

이전 컴포넌트 상태에서 수정 후에 컴포넌트 상태를 리액트가 재평가하고 이전 스냅샷과 현재 스냅샷의 차이를 Virtual DOM으로 보낸다.

이후, Virtual DOM은 실제 DOM을 업데이트하고 새로운 문장을 넣는다.
이때, Vitual DOM은 전체 DOM을 리렌더링 하지 않는다.
이전과 비교하여 수정된 부분만 추가 및 삭제하여 갈아끼운다.

불필요한 재평가를 방지할 수 있는 React.memo()

특정한 상황일 경우에만 컴포넌트를 재실행하도록 리액트에 지시할 수 있다.

  • 특정한 상황?
    - 컴포넌트가 받은 props가 변경되었을 때

props가 바뀌었는지 확인할 컴포넌트를 지정한 뒤에 React.memo()를 씌워주면 된다.

DemoOutput의 props이 바뀔때만 재평가하고 싶을 때

import React from "react";

const DemoOutput = (props) => {
  console.log("데모 아웃풋 실행중");
  return <p>{props.show ? "데모 아웃풋 컴포넌트입니다." : ""}</p>;
};

export default React.memo(DemoOutput);

React.memo()함수형 컴포넌트에만 가능하다.

React.memo()는 인자로 들어간 컴포넌트에 어떤 props가 입력되는지 확인하고 입력되는 모든 props의 신규 값을 확인한 뒤,
이를 기존의 props의 값과 비교하도록 리액트에게 전달한다.
또한 props의 값이 바뀐 경우에만 컴포넌트를 재실행 및 재평가하게 한다.

만약, 부모 컴포넌트가 변경되어도 해당 컴포넌트의 props 값이 바뀌지 않았다면
컴포넌트 실행을 건너뛴다.

React.memo()의 한계

React.memo()는 App에 변경이 발생할 때마다 해당 컴포넌트로 이동하여 기존 props 값과 새로운 값을 비교하게 한다.

이 때, React는 두 가지 작업을 할 수 밖에 없다.

  1. 기존의 props 값을 저장할 공간을 마련
  2. 기존의 props 값과 재평가된 컴포넌트의 props를 비교하여 변경되었는지 확인

따라서 어떤 컴포넌트를 평가하는지에 따라 성능 효율이 달라질 수 있다.

컴포넌트를 재평가하는 데 필요한 성능 비용과 props를 비교하는 성능 비용을 서로 맞바꾸게 되는 셈이다.
➡️ props의 개수와 컴포넌트의 복잡도, 자식 컴포넌트의 숫자에 따라 달라지므로 어느쪽의 비용이 더 높다고 말할 수 없다.

Ex )

  1. 자식 컴포넌트가 많아 컴포넌트 트리가 매우 크다?
    ➡️ React.memo()는 매우 유용하게 쓰일 수 있다.

  2. 컴포넌트 트리의 상위에 위치해있다면 전체 컴포넌트 트리에 대한 쓸데없는 리렌더링을 막을 수 있다.

반대로 매번 부모 컴포넌트를 매번 재평가할 때마다 컴포넌트의 변화가 있거나 props 값이 변화할 수 있는 경우에는React.memo는 크게 의미를 갖지 못한다.
이 때는 컴포넌트의 리렌더링이 어떻게든 필요한 상황이기 때문이다.

규모가 큰 React Project라면 컴포넌트 트리에서 잘라낼 수 있는 몇 가지 주요 컴포넌트 부분을 선택해서 사용하면 된다.

Javascript의 함정

React는 Javascript 환경에서 돌아간다.
React에서 props를 전과 비교할 때, 결국은 Javascript 환경에서 전과 동일한가?를 비교하게 된다.

React에서 원시타입 props를 비교할 때는 전과 동일하기 때문에 재실행 또는 재평가 하지 않는다.
false === false // true
'hi' === 'hi' // true

반면, 자바스크립트에서 참조 타입을 비교한다면?
[1,2,3] === [1,2,3] // false
이렇게 전과 다르다고 false를 반환한다.

자바스크립트에서 함수는 하나의 객체에 불과하다.
React에서 App 함수가 실행될 때 마다 새로운 함수 객체가 생성이 되고. 이 함수 객체가 onClick={}등의 props로 전달된다.
전달된 props는 이를 사용하는 컴포넌트의 이전 상태의 onClick={}을 비교하는 셈이 된다.

props.onClick === props.previous.onClick
// 현재 props === 이전 상태 props

✅ 자바스크립트에서 원시타입이 아닌 참조타입을 비교할 경우,
같은 내용을 가지고 있다 하더라도 평가시에 동일하지 않다라고 평가하고 false를 반환한다.

이 때문에 React.memo()로 컴포넌트를 감싸도, React.memo()는 값이 변경되었다고 인식하게 된다.
따라서 실제로 props가 변경되지 않아도 props 타입이 참조형 값을 가지고 있다면 React가 재평가 및 재실행되는 상황을 마주할 수 있다.

해결책 ( useCallback()으로 함수 재생성 방지 )

React.memo()는 객체 외의 prop 값에도 작동하게끔 할 수 있다.
객체를 생성하고 저장하는 방식만 조금 변경해주면 된다.

useCallback Hook은 기본적으로 컴포넌트 실행 전반에 걸쳐 함수를 저장할 수 있게 하는 Hook으로
리액트에 어떤 저장할 함수를 지정하고, useCallback을 이용하여 매번 실행때마다 이 함수를 재생성할 필요가 없다는 것을 알릴 수 있다.

이렇게 하면 동일한 함수 객체가 메모리의 동일한 위치에 저장되므로 이를 통해 리액트가 비교 작업을 할 수 있다.

아래 코드 예시는 자바스크립트가 객체를 비교할 때, 같은 메모리를 참조하는 지를 비교하여 비교하는 것을 보여주는 예시이다.

위 코드 처럼 동일한 역할을 하는 것이 useCallback이다.

우리가 선택한 함수를 리액트의 내부 저장 공간에 저장해서 함수 객체가 실행될 때마다 이를 재사용할 수 있게 한다.

useCallback 사용 방법은 저장하려는 함수를 useCallback으로 감싸주기만 하면 된다.

  1. useCallback의 첫 번째 인자로 저장할 함수를 전달한다. 그러면 useCallback은 저장된 함수를 반환해준다.
    그리고 해당 함수를 가지고 있는 컴포넌트가 다시 실행되면 useCallback이 리액트가 저장한 함수를 찾아서 같은 함수 객체를 재사용하게 된다.

어떤 함수가 절대 변경되어서는 안된다면 useCallback을 사용해 함수를 리액트에 저장시켜놓으면 된다.

  1. useEffect와 동일하게 useCallback도 두 번째 인자가 필요하며, 두 번째 인자는 배열이 와야한다. (useCallback 호출에 대한 의존성 배열. === useEffect와 동일함) 함수를 감싼 컴포넌트로부터 전달받는 모든 것을 사용할 수 있다.
    의존성 배열에 State나 props, Context를 지정할 수 있다.
    useCallback의 의존성 배열에는 업데이트 함수의 상태를 작성하면 된다.

useCallback 빈 의존성 배열
➡️ 해당 함수는 절대 변경되지 않을 것임을 리액트에 알려주는 배열.

만약, 의존성 배열에 특정 State, props, Context이 있다면 해당 값이 변경될 때만 함수가 변한다는 것을 리액트에 알려줌.

React.memo는 오직 Props 변화에만 의존하는 최적화 방법이다.

0개의 댓글