본문 바로가기

Front-End/React

React #19 (렌더 최적화 훅 - useMemo, useCallback)

반응형

 

 

useMemo - 계산된 "값"을 메모이제이션

  • 계산량이 많은 함수가 리렌더링마다 호출될 때
  • 의존성이 바뀌지 않았다면 이전 값을 재사용하고 싶을 때
더보기

src/components/ExpensiveComponent.tsx

아래의 코드에서는 수많은 반복을 통해 값이 계속 바뀌어 리렌더링되어 성능이 떨어진다.

import React, { useMemo, useState } from "react";

export default function ExpensiveComponent() {
    const [count, setCount] = useState(0);
    const [toggle, setToggle] = useState(false);

    const expensiveValue = useMemo(() => {

        console.log('Expensive calculation...');

        let total = 0;
        for (let i = 0; i < 100000000 ; i++) {
            total += 1;
        }
        
        return total + count;
    }, [count]);

    
    return (
        <div className="p-4 m-4 bg-gray-100">
            <p>Count: {count}</p>
            <button className="p-4 m-4 bg-blue-300" onClick={() => setCount((c) => c + 1)}> + 1 </button>
            <button className="p-4 m-4 bg-blue-300" onClick={() => setToggle((t) => !t)}> Toggle </button>
            <p>Expensive Value: {expensiveValue} </p>
        </div>
    )


}

 

src/components/UseMemoExample.tsx

useMemo라는 훅을 통해 미리 계산해서 값을 출력해줌

import React, { useMemo, useState } from "react";

function slowFunction(num: number) {
    console.log('무거운 계산 실행 중...');
    
    let result = 0;
    
    for (let i = 0; i < 1e8; i++) {
        result += num * Math.random();
    }
    
    return result;
}


export default function UseMemoExample() {
    const [count, setCount] = useState(0);
    const [other, setOther] = useState(false);

    const expensiveResult = useMemo(() => {
        return slowFunction(count);
    }, [count]);

    return (
        <div style={{ padding: 20 }}>
            <h2>useMemo 예제</h2>
            <p>Count: {count}</p>
            <button onClick={() => setCount(count + 1)}> + 1 증가 </button>
            <button onClick={() => setCount(count)}> + 0 증가 </button>
            <button onClick={() => setOther(!other)}> Toggle : {other.toString()} </button>
            <p> 계산 결과: {expensiveResult.toFixed(2)} </p>
        </div>
    )
}

 

src/pages/MemoTest.tsx

import ExpensiveComponent from "../components/ExpensiveComponent";
import UseMemoExample from '../components/UseMemoExample';


export default function MemoTest() {
  return (
    <div>
      <h2>useMemo 테스트</h2>
      <ExpensiveComponent />
      <UseMemoExample />

    </div>
  );
}

 

src/App.tsx

import React from 'react';
import logo from './logo.svg';
import './App.css';
import MemoTest from './pages/MemoTest'


function App() {
  return (
    <div className="App">
      <MemoTest />
    </div>
  );
}

export default App;

 

 

 

계산이 무거운데, 리렌더링 되면 또 계산해야 돼?에 대한 해결책.

 

 

 

 

useCallback - 함수를 메모이제이션

  • 자식 컴포넌트에 콜백 함수를 props로 넘길 때
  • 의존성이 바뀌지 않았다면 기존 함수를 재사용하고 싶을 때
더보기

src/components/Parent.tsx

import React, { useState, useCallback } from "react";

const Button = React.memo(
    ( { onClick, label }: {onClick: () => void, label: string}) => {
    console.log(`Rendering: ${label}`);
    return (<button onClick={onClick}> {label} </button>);
});


export default function Parent() {
    const [count, setCount] = useState(0);

    const handleClick = useCallback(() => {
        setCount((prev) => prev + 1);
    }, []);

    return (
        <div>
            <p>Count: {count}</p>
            <Button onClick={handleClick} label="Increment"/>
        </div>
    )

}

 

src/pages/MemoTest.tsx

import ExpensiveComponent from "../components/ExpensiveComponent";
import UseMemoExample from '../components/UseMemoExample';
import Parent from '../components/Parent'

export default function MemoTest() {
  return (
    <div>
      {/* <ExpensiveComponent /> */}
      {/* <UseMemoExample /> */}
      <Parent />
    </div>
  );
}

 

src/App.tsx

import React from 'react';
import logo from './logo.svg';
import './App.css';
import MemoTest from './pages/MemoTest'


function App() {
  return (
    <div className="App">
      <MemoTest />
    </div>
  );
}

export default App;

 

props가 바뀌었다고 함수가 새로 만들어져서 리렌더링되는 경우에 대한 해결책, React.memo와 함께 사용

 

 

 

 

+ useCallback 예제 하나 더

더보기

src/components/UseCallback.tsx

import React, { useState, useCallback } from "react";


const Child = React.memo(({ onClick }: { onClick: () => void }) => {
    console.log("자식 컴포넌트 렌더링");
    return (
        <div>
            <button onClick={onClick}> 자식 버튼 클릭 </button>
        </div>
        );
})


export default function UseCallbackExample() {

    const [count, setCount] = useState(0);

    const [other, setOther] = useState(false);

    const handleClick = useCallback(() => {
        setCount((prev) => prev + 1);
    }, []);

    return (
        <div style={{ padding: 20 }}>
            <h2>UseCallback 예제</h2>
            <p>Count: {count}</p>
            <button onClick={() => setOther(!other)}> Toggle: {other.toString()} </button>
            <Child onClick={handleClick} />
        </div>
    )
}

 

src/pages/MemoTest.tsx

import ExpensiveComponent from "../components/ExpensiveComponent";
import UseMemoExample from '../components/UseMemoExample';
import Parent from '../components/Parent'
import UseCallbackExample from "../components/UseCallbackExample";

export default function MemoTest() {
  return (
    <div>
      {/* <ExpensiveComponent /> */}
      {/* <UseMemoExample /> */}
      <Parent />
      <UseCallbackExample />
    </div>
  );
}

 

src/App.tsx

import React from 'react';
import logo from './logo.svg';
import './App.css';
import MemoTest from './pages/MemoTest'


function App() {
  return (
    <div className="App">
      <MemoTest />
    </div>
  );
}

export default App;

 

 

클릭할 때마다 리렌더링이 되는 것이 아닌 그대로 오름

 

 

 

 

 

 

 

 

 

 

※ 주의사항

  • 무조건 사용하는 것이 아니라, 성능 문제를 확인하고 도입하는 것이 중요
  • useMemo, useCallback 자체도 비용이 있기 때문에 과도한 사용은 오히려 성능 저하

 

 

 

 

 

반응형