useMemo, useCallback으로 Rendering 최적화

YUKI KIM·2022년 2월 17일
0

프로젝트가 커질수록 컴포넌트의 렌더링을 최적화 하는 것은 중요하다. (안커도 중요겠지만.ㅋ) 렌더링 최적화와 같은 성능 관련 최적화는 사용자 경험에 큰 영향을 주기 때문에 hooks 위주로 알아보려고 한다.


Re-Rendering 조건

컴포넌트의 렌더링을 최적화 하기 위해서는 우선 컴포넌트가 리렌더링 되는 조건을 알아야 한다. 그 조건은 다음과 같다.

  • 부모 컴포넌트에서 전달받은 props가 변경될 때
  • 부모 컴포넌트가 리렌더링 될 때
  • 자기 자신의 컴포넌트의 state가 변경될 때

useMemo 사용하기

React Hooks API Reference - useMemo

useMemo는 메모제이션된 값을 반환하기 때문에, 보통 React 프로젝트 안에서 연산이 많이 요구되는 함수들을 캐싱하기 위해 사용한다.

문제 상황

만약 컴포넌트 내의 특정 함수가 값을 return 하기 위해 많은 시간을 소요하면, 이 컴포넌트는 리렌더링 될 때마다 함수가 호출되면서 많은 시간과 메모리가 소요될 것이다. 또한 이 함수가 return 하는 값을 자식 컴포넌트가 사용한다면 그 자식 컴포넌트는 매번의 함수 호출 마다 새로운 값을 받고 리렌더링한다.

useMemo 사용 예시

import { useState } from "react";
import Average from "./Average";

function App () {
  const [nums, setNums] = useState([10, 20, 30, 40, 50]);

  const average = (() => {
    return nums.reduce((acc, cur) => {
      return acc + cur / nums.length;
    }, 0);
  })();

  return (
    <Average average={average} />
  );
}

export default App;

nums의 평균을 구하는 함수인 average의 반환값이 Average 컴포넌트에 props로 보내지고 있다. 만약 average 함수가 연산이 엄청 오래걸리는 함수라면 App 컴포넌트가 리렌더링 될 때마다 시간이 오래 걸릴 것이다.

그럼 이제 useMemo를 사용해서 average 함수에 종속성을 부여해보자!

const average = useMemo(() => {
  return nums.reduce((acc, cur) => {
    return acc + cur / nums.length;
  }, 0);
}, [nums]);

위와 같이 average 함수를 수정해주면 종속 변수인 nums가 변하지 않으면 리렌더링 할 때마다 average 함수를 호출하지 않고 이전의 return 값을 사용한다. 즉, 함수 호출 시간도 줄고, 자식 컴포넌트의 리렌더링도 방지할 수 있다.


useCallback 사용하기

React Hooks API Reference - useCallback

앞서 useMemo는 메모제이션된 값(return 값)을 반환한다고 했다. useCallback은 이름 그대로 함수의 콜백을 반환한다.

문제 상황

부모 컴포넌트에서 자식 컴포넌트에 함수를 props로 전달한다고 할 때, 부모 컴포넌트에서 정의한 state가 자주 변경되어 부모 컴포넌트가 리렌더링되는 경우가 잦다면 해당 함수는 그 부모 컴포넌트가 리렌더링 될 때마다 새로 생성되며 자식으로 전달된다.

useCallback 사용 예시

import { useState, useMemo } from "react";
import Average from "./Average";
import Button from "./Button";

function App () {
  const [text, setText] = useState("");
  const [nums, setNums] = useState([10, 20, 30, 40, 50]);

  const average = useMemo(() => {
    return nums.reduce((acc, cur) => {
      return acc + cur / nums.length;
    }, 0);
  }, [nums]);

  const addNums = () => setNums([...nums, 10]);

  return (
    <>
      <input
        value={text}
        onChange={(event) => setText(event.target.value)}
      />
      <Average average={average} />
      <Button onClick={addNums} />
    </>
  );
}

export default App;

text state가 타이핑될 때마다 App 컴포넌트는 리렌더링 되고, Button 컴포넌트는 App의 리렌더링마다 addNums 함수를 props로 전달 받는다.

이 경우 Button 컴포넌트가 onClick을 받을 때 이전에 받았던 onClick과 동일한지 체크한 후 동일하다면 리렌더링되지 않으면 Button 컴포넌트의 불필요한 리렌더링을 막을 수 있다.

하지만 함수는 객체이고, 부모 컴포넌트가 리렌더링 될 때마다 새로 생성되는 함수는 다른 참조 값을 가지기 때문에 Button 입장에서는 새로 생성된 함수를 받을 때 props가 변한 것으로 인지한다.

useCallback과 React.memo

useCallback과 React.memo의 조합으로 앞서 언급한 문제를 해결할 수 있다. React.memo는 컴포넌트의 props가 바뀌지 않으면 리렌더링하지 않도록 해준다.

const addNums = useCallback(() => {
  setNums([...nums, 10]);
}, [nums]);
import { memo } from "react";

function Button (props) {
  console.log("Button Render")
  return (
    <button onClick={props.onClick}>10 추가</button>
  );
}
  
export default memo(Button);

useCallback과 React.memo를 사용하여 위처럼 해주면 text가 타이핑될 때마다 Button 컴포넌트가 리렌더링 되는 것을 방지해서 성능을 최적화할 수 있다.


레퍼런스

profile
유키링と 욘데 쿠다사이

0개의 댓글