본문 바로가기
코딩 정보/React

[React] 랜더링 최적화 방법을 자세히 알아보자

by 꽁이꽁설꽁돌 2024. 7. 25.
728x90
반응형

목차

    React Component Tree

    리액트의 컴포넌트 트리는 다음과 같이 만들어 진다.

    App을 중심으로 순차적으로 만들어 진다.

     

    React Dev Tools

    이것을 이용하면 여러 분석으로 할 수 있다.

    아래와 같은 웹에서 increment 버튼을 눌렀을 때 렌더링을 분석할 수 있다.

     

    색깔이 칠해진 것은 재랜더링이 된 것이다.

     

    이런식으로 랜더링 시간 순위별로 볼 수 있다.

     

    컴포넌트 랜더링 이유 분석 방법

    설정 버튼을 누른다.
    첫번째 박스를 체크하면 왜 각 컴포넌트가 랜더링 되었는지 확인 가능하다.

     

    Hook1 changed라는 분석 결과를 볼 수 있다.

     

     

    memo를 통한 랜더링 최적화 하기

    아래와 같은 코드가 있을 때 첫번째 상태인 enteredNum만 바꾸었는데 모든 컴포넌트가 리랜더링이 일어난다.

    따라서 이것을 막기위해서 memo를 사용한다.

    import { useState } from 'react';
    
    import Counter from './components/Counter/Counter.jsx';
    import Header from './components/Header.jsx';
    import { log } from './log.js';
    
    function MainApp() {
      log('<App /> rendered');
    
      const [enteredNumber, setEnteredNumber] = useState(0);
      const [chosenCount, setChosenCount] = useState(0);
    
      function handleChange(event) {
        setEnteredNumber(+event.target.value);
      }
    
      function handleSetClick() {
        setChosenCount(enteredNumber);
        setEnteredNumber(0);
      }
    
      return (
        <>
          <Header />
          <main>
            <section id="configure-counter">
              <h2>Set Counter</h2>
              <input type="number" onChange={handleChange} value={enteredNumber} />
              <button onClick={handleSetClick}>Set</button>
            </section>
            <Counter initialCount={chosenCount} />
          </main>
        </>
      );
    }
    
    export default MainApp;

     

    이런식으로 Counter함수를 memo로 묶게되면 App Component에서 전달하는 속성인 initialCount의 변화여부만 확인하여

    변하지 않았으면 재랜더링 하지 않는다.

    import { useState, memo } from "react";
    
    
    const Counter = memo(function Counter({ initialCount }) {
     ...
      return (
        <section className="counter">
          <p className="counter-info">
            The initial counter value was <strong>{initialCount}</strong>. It{" "}
            <strong>is {initialCountIsPrime ? "a" : "not a"}</strong> prime number.
          </p>
          <p>
            <IconButton icon={MinusIcon} onClick={handleDecrement}>
              Decrement
            </IconButton>
            <CounterOutput value={counter} />
            <IconButton icon={PlusIcon} onClick={handleIncrement}>
              Increment
            </IconButton>
          </p>
        </section>
      );
    });
    
    export default Counter;

     

    memo로 재랜더링을 막은 모습

     

    React 구조를 바꾸어 랜더링 최적화하기

    import { useState } from 'react';
    
    import Counter from './components/Counter/Counter.jsx';
    import Header from './components/Header.jsx';
    import { log } from './log.js';
    
    function MainApp() {
      log('<App /> rendered');
    
      const [enteredNumber, setEnteredNumber] = useState(0);
      const [chosenCount, setChosenCount] = useState(0);
    
      function handleChange(event) {
        setEnteredNumber(+event.target.value);
      }
    
      function handleSetClick() {
        setChosenCount(enteredNumber);
        setEnteredNumber(0);
      }
    
      return (
        <>
          <Header />
          <main>
            <section id="configure-counter">
              <h2>Set Counter</h2>
              <input type="number" onChange={handleChange} value={enteredNumber} />
              <button onClick={handleSetClick}>Set</button>
            </section>
            <Counter initialCount={chosenCount} />
          </main>
        </>
      );
    }
    
    export default MainApp;

     

    아래와 같이 컴포너트를 분리해 상태를 내려주어 MainApp컴포넌트의 리랜더링을 막아준다.

    import { useState } from 'react';
    import ConfigureCounter from './components/Counter/ConfiguerCounter.jsx';
    import Counter from './components/Counter/Counter.jsx';
    import Header from './components/Header.jsx';
    import { log } from './log.js';
    
    function MainApp() {
      log('<App /> rendered');
    
     
      const [chosenCount, setChosenCount] = useState(0);
      function handleSetCount(newCount){
        setChosenCount(newCount);
      }
     
      return (
        <>
          <Header />
          <main>
            <ConfigureCounter setChosenCount={handleSetCount}></ConfigureCounter>
            <Counter initialCount={chosenCount} />
          </main>
        </>
      );
    }
    
    export default MainApp;

     

    import { useState } from "react";
    import { log } from "../../log";
    function ConfigureCounter({ setChosenCount }) {
      log("<ConfigureCouner/>", 1);
      const [enteredNumber, setEnteredNumber] = useState(0);
    
      function handleChange(event) {
        setEnteredNumber(+event.target.value);
      }
    
      function handleSetClick() {
        setChosenCount(enteredNumber);
        setEnteredNumber(0);
      }
      return (
        <section id="configure-counter">
          <h2>Set Counter</h2>
          <input type="number" onChange={handleChange} value={enteredNumber} />
          <button onClick={handleSetClick}>Set</button>
        </section>
      );
    }
    export default ConfigureCounter;

     

    MainApp이 회색으로 바뀌었다

     

    useMemo VS Memo Hook

     

    useMemo

    계산된 값을 메모이제이션하여 불필요한 계산을 피한다.

    import React, { useMemo } from 'react';
    
    function ExpensiveComponent({ a, b }) {
      // useMemo를 사용하여 계산된 값을 메모이제이션
      const expensiveValue = useMemo(() => {
        return a + b; // 가정: 이 계산은 매우 비용이 많이 드는 작업임
      }, [a, b]);
    
      return (
        <div>
          {expensiveValue}
        </div>
      );
    }

     

     

    Memo Hook

    컴포넌트 자체를 메모이제이션하여 props가 변경되지 않는 한 재렌더링을 방지한다.

    import React, { memo } from 'react';
    
    const MyComponent = ({ name }) => {
      console.log('MyComponent rendered');
      return (
        <div>
          Hello, {name}!
        </div>
      );
    };
    
    // memo를 사용하여 컴포넌트를 메모이제이션
    export default memo(MyComponent);

     

    key의 중요성

    상태는 컴포넌트 타입에만 속한 게 아니라 컴포넌트가 사용되는 위치에도 속해 있다.

    아래 예를 통해 알아보자

    import { useState } from 'react';
    
    import { log } from '../../log.js';
    
    function HistoryItem({ count }) {
      log('<HistoryItem /> rendered', 3);
    
      const [selected, setSelected] = useState(false);
    
      function handleClick() {
        setSelected((prevSelected) => !prevSelected);
      }
    
      return (
        <li onClick={handleClick} className={selected ? 'selected' : undefined}>
          {count}
        </li>
      );
    }
    
    //index는 엄격하게 매핑되는 값이 아니라 항상 정적으로 유지된다.
    export default function CounterHistory({ history }) {
      log('<CounterHistory /> rendered', 2);
    
      return (
        <ol>
          {history.map((count, index) => (
            <HistoryItem key={index} count={count} />
          ))}
        </ol>
      );
    }

     

    아래 코드는 일부지만 실행시켜보면 다음과 같다.

    0을 누른 상태로 값을 더 추가시켜주었다.
    0이 선택된 채로 추가가 된 것이 아닌 1이 선택된 채로 추가되었다.

     

    따라서 아래와 같이 정적인 위치의 컴포넌트를 만들때는 신경 쓸 필요가 없는 것과 달리 

    컴포넌트의 위치가 동적으로 변할 때는 key를 신경써주어야 한다.

    import { useState } from 'react';
    import ConfigureCounter from './components/Counter/ConfiguerCounter.jsx';
    import Counter from './components/Counter/Counter.jsx';
    import Header from './components/Header.jsx';
    import { log } from './log.js';
    
    function MainApp() {
    
      return (
        <>
          <Header />
          <main>
            <Counter initialCount={chosenCount} />
            <Counter initialCount={0} />
          </main>
        </>
      );
    }
    
    export default MainApp;

     

     

    unique한 속성으로 key 전달해 준 수정된 코드 

    import { useState } from 'react';
    
    import { log } from '../../log.js';
    
    function HistoryItem({ count }) {
      log('<HistoryItem /> rendered', 3);
    
      const [selected, setSelected] = useState(false);
    
      function handleClick() {
        setSelected((prevSelected) => !prevSelected);
      }
    
      return (
        <li onClick={handleClick} className={selected ? 'selected' : undefined}>
          {count}
        </li>
      );
    }
    
    export default function CounterHistory({ history }) {
      log('<CounterHistory /> rendered', 2);
    
      return (
        <ol>
          {history.map((count) => (
            <HistoryItem key={count.id} count={count.value} />
          ))}
        </ol>
      );
    }

    잘 동작하는 것을 볼 수 있다.

     

    key를 써야하는 또 다른 이유

    index를 통해 키를 설정한 경우 -> 모든 요소를 업데이트 한다...
    unique한 id로 키를 설정한 경우 -> 새로 추가한 것만 업데이트 한다.

     

    unique한 키를 통해 해야 업데이트를 추가한 요소만 하는 것을 볼 수 있다.

     

    MillionJs를 통한 랜더링 최적화

    npm install million

     

    https://pyjun01.github.io/v/million-js/

     

    Million.js는 어떻게 React보다 최대 70% 빠를까?

    React의 Virtual DOM replacement인 Million.js의 코어 로직을 탐구하며 소개합니다.

    pyjun01.github.io

     

     

     

    https://million.dev/

     

    Million

    Ready for a speed up? Start building with a free account. Speak to an expert for your Enterprise needs.

    million.dev

     

    반응형