코딩 정보/React

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

꽁이꽁설꽁돌 2024. 7. 25. 16:06
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

     

    반응형