본문 바로가기

Computer Programming/React

리액트의 렌더링 순서와 렌더링 최적화하는 법 (memoization을 통해 불필요한 리렌더링 해결하기!)

1. 리액트 렌더링의 순서

아래 코드에서 4가지 console.log() 문이 어떻게 출력될지 예상해보자.

 

import React, { useState } from 'react';

import Login from './components/Login/Login';
import Home from './components/Home/Home';
import MainHeader from './components/MainHeader/MainHeader';
import { useEffect } from 'react';

function App() {
  const [isLoggedIn, setIsLoggedIn] = useState(false);
  console.log("in function")

  useEffect(() => {
    const storedUserLoggedIn = localStorage.getItem('isLoggedIn')
    setTimeout(() => {
      if (storedUserLoggedIn === '1') {
        setIsLoggedIn(true);
      }
      console.log("in effect")
    }, 3000)
  },[])

  const loginHandler = (email, password) => {
    localStorage.setItem('isLoggedIn','1'); //1 means logged in.
    setIsLoggedIn(true);
  };

  const logoutHandler = () => {
    setIsLoggedIn(false);
  };

  return (
    <React.Fragment>
      <MainHeader isAuthenticated={isLoggedIn} onLogout={logoutHandler} />
      <main>
        {console.log("in return")}
        {!isLoggedIn && <Login onLogin={loginHandler} />}
        {isLoggedIn && <Home onLogout={logoutHandler} />}
      </main>
    </React.Fragment>
  );
}

export default App;

console.log 실행 순서는 다음과 같다.

  1. in function
  2. in return
  3. setTimeout (3초 후)
  4. in effect
  5. in function
  6. in return

앱 실행 시작 시점에 App 컴포넌트가 평가되면서 in function, in return 실행 ( +child component rerendring)

→ return 문까지 평가되고 effect 실행 (setTimeout 함수 실행)

→ useEffect 함수 내의 state 가 변경되었으므로 App component 리렌더링

→ App component 리렌더링으로 child component 인 <Login /> 또는 <Home /> 리렌더링

 


2. re-rendering의 조건 두 가지

  • state가 변경되었을 때
  • props가 변경되었을 때

mokkapps.de

 

만약 parents component의 state 변경의 영향을 받지 않는 grand child component 까지 리렌더링된다면 불필요한 리렌더링이 일어난다. grand child component가 100개, 1000개 이상이라면 매우 비효율적인 코드가 된다.

 

grand child component 가 props 를 전달받을 때 props 가 변경되지 않은 것 처럼 보일지라도, 메모리에 대한 참조값이 달라졌기 때문에 리렌더링이 발생한다. (리렌더링 될 때 마다 parents 에서 넘겨준 props가 재생성되고, 이전의 props 와 다른 참조값을 가지기 때문)

 

 

3.불필요한 re-rendering 해결 방법

parents component가 리렌더링될때 넘겨준 props 의 참조값이 바뀌지 않게 한다면 grand child component는 props 변경의 영향을 받지 않기 때문에 렌더링 최적화에 도움을 줌

  1. useCallback 사용
  2. React.memo 사용

  1) useCallback (props가 함수일 때)

useCallback은 함수를 memoization 해주는 리액트 훅이다.

useCallback – React

즉, 기존에 수행한 연산의 결과 값을 메모리에 저장해두고, 필요할 때 재사용하는 기법이다. 두번째 인수로 전달한 dependency가 변경되지 않는 이상, 컴포넌트가 리렌더링 될 때 마다 변수에 같은 참조값이 할당됨


useCallback을 사용하면 두가지 단계(Render Phase 와 Commit Phase)로 구성된 렌더링에서 한가지 단계만을 수행하면 되기 때문에 최적화에 도움이 된다.

아래는 렌더링의 두가지 단계이다.

렌더링 두 가지 단계

  1. Render Phase : 컴포넌트(함수) 호출 → 2) React Element 반환 → 3)새로운 Virtual DOM 생성하거나 또는 이미 한번 이상 렌더링이 된 상태라면 재조정(Reconciliation) 과정을 거친 후 Real DOM의 업데이트 필요 여부 체크
    • React Element란? 리액트 컴포넌트가 호출되면(렌더링 되면), 리액트는 React.createElement() 메소드를 내부적으로 호출한다. 이 메소드는 아래의 오브젝트를 반환한다. 
    • const App = () => { return <p className="danger">Hello React</p>; }; console.log(App()); // { // $$typeof: Symbol(react.element) // "type": "p", // "key": null, // "ref": null, // "props": { // "children": "Hello React", // "className": "danger" // }, // "_owner": null, // "_store": {} // }
    • https://www.robinwieruch.de/react-element-component/
    • 재조정(Reconciliation) 과정이란? 이전 Virtual DOM과 현재 VIrtual DOM을 비교하여 업데이트가 필요한지 확인하는 과정이다.

2. Commit Phase : Render Phase 에서 체크해뒀던 DOM에서의 변경이 필요한 부분들을 Real DOM에 반영해주는 단계이다. 만약 변경할 부분이 없다면 Commit Phase 는 skip 된다.

 

즉, 리액트에서 렌더링이 발생할 때 마다 재조정 과정을 포함한 render phase를 거치고, 필요하다면 그 후 commit phase 를 수행한다.

 

 

위의 useCallback을 사용하면, Parents component 가 리렌더링 되어도 props 함수가 같은 값을 참조하기 때문에 DOM update를 하지 않아도 되고, 결국 Render phase 만 실행되고 commit phase는 생략된다. (이외 모든 전제 조건은 같다라는 가정 하에) 따라서 useCallback은 렌더링 최적화의 방법 중 하나가 된다.

 

 

2) React.memo

memo – React

 

memo – React

The library for web and native user interfaces

react.dev

React.memo : 전달받은 props 가, 이전에 가지고 있던 props 와 비교했을때 동일하면 re-rendering을 막아준다. 또한 렌더링 되어있던 결과를 재사용하는 고차 컴포넌트(HOC)이다.

  • HOC (Higher Order Component) : HOC는 컴포넌트 로직을 재사용하기 위해 사용되고, 컴포넌트ㅡㄹ 가져와 새 컴포넌트를 반환하는 함수이며 대표적으로 Redux 사용 시의 connect 함수(store 와 연결하기 위한 third-party library 중)가 있다.그러나 함수형 리액트를 주로 사용하게 되면서 react life cycle과 함께 효율적으로 사용할 수 있는 HOC에 대한 관심은 줄어들고 있다. Cross-Cutting Concerns 에 HOC를 주로 사용하는데, 이는 각 계층을 넘어 공통으로 필요한 관심사에 필요한 정보를 주기 위해 만들어진 개념이다.
  • 고차 컴포넌트 – React
 

고차 컴포넌트 – React

A JavaScript library for building user interfaces

ko.legacy.reactjs.org

 

또한 비교 방법을 이용해 리렌더링 과정을 생략할 수 있다.

React.memo는 기본적으로 얕은 비교를 사용하여 이전의 props 와 현재 전달받은 props 를 비교한다.

 

1.얕은 비교란?

  • 원시타입의 데이터인 경우 값이 같은지 비교
  • 참조타입의 데이터인 경우 참조 값이 같은지 비교 (객체) → 객체를 state로 사용하게 될 경우, 값이 같더라도 참조값이 다르기 때문에 리렌더링이 발생함

2. 깊은 비교란?

  • 객체(참조 타입)의 경우에도 얕은 비교와는 다르게 값 또한 비교한다.

 

즉, useCallback으로 컴포넌트가 re-rendering 되어도 props의 참조값이 변하지 않게 하고 (Commit phase 생략) , React.memo를 통해 props를 얕은비교한 뒤 re-rendering을 막는 방법을 통해 Render phase까지 생략해줌으로서 리액트 렌더링을 최적화할 수 있다.

 

 

3) useMemo (prop가 함수가 아닌 객체일때)

앞서 언급했던 useCallback은 함수를 memoization 해주는 hook 이다.

만약 props 가 함수가 아닌 객체라면 , parents 컴포넌트가 리렌더링 될 때 마다 객체가 새로 생성되기 때문에 새로운 참조값이 할당되고, React.memo를 제대로 수행할 수 없게 된다.

이때는 값에 대한 memoization을 담당하는 훅인 useMemo를 사용하면 된다.

const memoizedItem = useMemo(() => item, [])

 

 

 

 

child component 가 넘겨받는 props가 함수인지, 객체인지에 따라 다르게 memoization을 활용하면 불필요한 렌더링을 줄일 수 있다.

 

 

 

참고) memoization이란?

What is Memoization? How and When to Memoize in JavaScript and React

What is Memoization? How and When to Memoize in JavaScript and React

 

What is Memoization? How and When to Memoize in JavaScript and React

Hi everyone! In this article we will talk about memoization, an optimization technique that can help make heavy computation processes more efficient. We will start by talking about what memoization is and when it's best to implement it. Later on we will gi

www.freecodecamp.org

 

 

 

Re-rendering | useCallback | 2 Rendering Phases | React.memo 와 HOC | 얕은 비교와 깊은 비교 | useMemo

 

참고 : https://www.youtube.com/watch?v=1YAWshEGU6g