[React] useCallback, useMemo, React.memo

Main·2023년 6월 30일
0

React

목록 보기
1/31
post-thumbnail

React hook를 이용한 렌더링 최적화 방법

1. useCallback

인자로 전달받은 콜백함수 자체를 memoization하여 캐싱
의존성 배열에 넣은 값들이 변화할때만 함수가 재생성

memoization: 기존에 수행한 연산의 결과값을 어딘가에 저장해두고 동일한 입력이 들어오면 재활용하는 프로그래밍 기법을 말합니다. memoization을 절적히 적용하면 중복 연산을 피할 수 있기 때문에 메모리를 조금 더 쓰더라도 애플리케이션의 성능을 최적화할 수 있다.

useCallback 구조

const printNumber = useCallback(() => {
	console.log(number)
}. [number])

useCallBack은 두 개의 인자를 받으면, 첫 번째 인자는 콜백함수, 두 번째 함수는 의존성 배열을 받는다.
첫 번째 인자 콜백함수는 memoization할 함수이며, 두 번째 인자는 배열 안의 값이 업데이트될 때 콜백함수를 다시 재성하여 memoization한다.

useCallback 사용이유 ?

리액트 컴포넌트에서는 렌더링이 될 때마다 함수형 컴포넌트는 다시 호출된다.
자바스크립트에서 함수가 다시 호출되면 함수 내부에 정의된 모든 변수들은 초기화 되어 다시 생성된다.
따라서 렌더링이 될 때마다 불필요하게 함수가 재생성된며, 이는 메모리 낭비가 된다.
따라서 한 번 만든 함수는 재사용하고, 필요할때만 재생성하여 사용하는것이 바람직하다.
이것을 위해 useCallback 함수를 이용한다.

useCallback 사용예시

import { useEffect, useState } from "react";
export default function App() {
  const [number, setNumber] = useState(0);
  const [isToggle, setIsToggle] = useState(false);
  const addNumber = (e) => {
    setNumber(e.target.value);
  };
  
  // useCallback 미적용
  const onClickToggle = () => {
    setIsToggle(!isToggle)
  };
  
  // useCallback 적용
  // const onClickToggle = useCallback(() => {
  // setIsToggle(!isToggle)
  // },[]);
  
  useEffect(() => {
    console.log("onClickToggle 함수가 재생성 되었습니다.");
  }, [onClickToggle]);
  
  return (
    <>
      <input type="number" value={number} onChange={addNumber} />
      <button type="button" onClick={onClickToggle}>
        클릭
      </button>
    </>
  );
}

useCallback를 적용하지 않을 시 input의 number를 변경시켜주어도 onClickToggle 함수가 재생성된다.
input의 number 값을 변경될 때마다 상태가 변경되기 때문에 App 컴포넌트가 다시 호출되고, APP 컴포넌트에 모든 변수가 새롭게 생성되기 때문에 onClickToggle 함수가 재생성된다.
useCallback를 적용 시 onClickToggle 함수가 memolization 되어 주소 참조값이 저장되고, APP 컴포넌트가 다시 호출되어 모든 변수가 새롭게 생성되어도 onClickToggle 함수는 memolization된 주소 참조값을 불러오기 때문에 재생성되지 않는다. 대신 의존성 배열의 값이 변화할때만 재생성이 발생된다.

useCallback 주의사항

모든 함수에 useCallback를 사용하는 것은 좋은 것이 아니다.
useCallback를 사용한다는 것은 메모리를 소비해서 함수를 저장하며, 그 만큼의 비용이 발생하는 것이기 때문이다.
따라서 useCallback은 아래와 같은 상황에서 사용하는것이 좋다.

useCallback를 사용하기 적합한 상황

  • 연산이 복잡한 함수인 경우
  • 자식 컴포넌트에 함수를 props로 넘길 때 불필요한 렌더링이 발생될 경우
  • 참조 동일성을 유지해야 하는 경우

2. useMemo

필요한 값을 memoization 하여 캐싱
의존성 배열에 넣은 값들이 변화할때만 값이 변경

useMemo 구조

const value = useMemo(() => {
	return func();
}. [])

useMemo는 두 개의 인자를 받으며, 첫 번째 인자로는 콜백 함수, 두 번째 인자로는 의존성 배열을 받는다.
첫 번째 콜백함수는 memoization을 할 값을 리턴하는 함수
두 번째 의존성 배열은 배열 안의 값이 업데이트될 때 함수를 재생성해서 memoization된 값을 업데이트한다.

useMemo 사용이유 ?

위에서 설명 했듯이 리액트 컴포넌트에서는 렌더링이 될 때마다 함수형 컴포넌트는 다시 호출된다. 자바스크립트에서 함수가 다시 호출되면 함수 내부에 정의된 모든 변수들은 초기화 되어 다시 생성된다. 따라서 무거운 계산을 하는 함수가 있고 그 값을 할당하는 함수가 계속 재생성 된다면 그 값을 다시 할당하는것은 매우 비효율적인 작업이 된다.
이를 해결하기 위해 useMemo가 사용된다.

import { useState } from "react";
 
function App() {
  const [hardNumber, setHardNumber] = useState(1);
  const [easyNumber, setEasyNumber] = useState(1);
  
  const hardCalculate = (number) => {
    console.log("어려운 계산");
    for (let i = 0; i < 99999999; i++) {} // 생각하는 시간
    return number + 10000;
  };

  const easyCalculate = (number) => {
    console.log("쉬운 계산");
    return number + 1;
  };
  
  const hardSum = hardCalculate(hardNumber);
  // useMoemo 적용
  // const hardSum = useMemo(()=>{
  //  	return hardCalculate(hardNumber);
  // },[hardNumber]);
  
  const easySum = easyCalculate(easyNumber);

  return (
    <div>
      <h2>어려운계산기</h2>
      <input
        type="number"
        value={hardNumber}
        onChange={(e) => setHardNumber(parseInt(e.target.value))}
      />
      <span> + 10000 = {hardSum}</span>

      <h2>쉬운 계산기</h2>
      <input
        type="number"
        value={easyNumber}
        onChange={(e) => setEasyNumber(parseInt(e.target.value))}
      />
      <span> + 1 = {easySum}</span>
    </div>
  );
}

export default App;

useMemo를 적용하지 않을 시, 어려운 계산기의 input의 number 값을 변경 시켜도 값이 즉시 증가되지 않는다. 값을 변경 시킬 때 마다 무거운 계산이 필요한 함수가 재생성되고, 값이 새로 할당되기 때문에 값이 증가할때 까지 딜레이가 걸리기 때문이다.
그럼 쉬운 계산기의 input의 number 값을 변경시키도 딜레이가 걸릴까? 쉬운 계산기 역시 easyNumber라는 값을 변경시키게 되고 이 때문에 함수가 새로 재생성되기 때문에 hardSum의 값이 초기화되어 hardCalculate 함수가 다시 호출되기 때문이다.
useMemo를 적용 시 hardSum의 값이 memoizaion 되어 캐싱되고, 함수가 재생성 되더라도 hardCalculate 함수를 다시 호출하지 않고 hardSum의 memoizaion된 값을 가져오게된다.

useMemo 사용 시 주의사항

useMemo 역시 메모리를 소비해서 값을 저장한다. 만약 불필요한 값까지 전부 메모이제이션을 하면 성능에 좋지 않은 영향을 준다. 따라서 필요할 때만 적절하게 사용하는것이 바람직하다.

useMemo를 사용하기 적합한 상황

  • 복잡한 연산이 포함된 경우
  • 렌더링 이후 동일한 참조를 사용할 가능성이 높은 경우
  • 부모가 리렌더링 될 때 자식 컴포넌트까지 렌더링 되는 것은 막고 싶은 경우

3. React.memo

React.memoHOC(High Order Compoent, 고차 컴포넌트) 어떤 컴포넌트를 인자로 받아 새로운 컴포넌트를 반환해주는 함수
Props Check를 통해 변화가 있는지 없는지를 감지해서 변화가 있을때만 새로운 컴포넌트를 반환함 변화가 없다면 기존의 컴포넌트를 재사용
불필요한 컴포넌트 렌더링을 방지하기 위해 사용
부모 컴포넌트로 전달받은 props가 변경될때만 자식 컴포넌트를 업데이트
React.memo는 오직 Props 변화에만 의존하는 최적화 방법!
useState, useReducer, useContext와 같은 상태와 관련된 hook을 사용한다면 Props의 변화가 없더라도 다시 렌더링이 일어난다는 것을 기억해야한다.

React.memo 구조

import {memo} from 'react'
  const Child = ({name, age}) => {
    return(
      <p>name: {name}</p>
      <p>age: {age}</p>
    )
  }
    
    export default memo(Child);

React.memo를 적용할 컴포넌트를 memo로 감싸준다.

React.memo 사용이유 ?

react에서 부모 컴포넌트가 업데이트 되면, 자식 컴포넌트도 업데이트가 발생된다.
이로 인해 자식 컴포넌트에 불필요한 렌더링이 발생하게 된다.
이를 해결하기위해 부모 컴포넌트로 전달받은 props가 변경될때 자식 컴포넌트 업데이트 되도록하는 React.memo가 사용된다.

React.memo 사용 예시

Parent 컴포넌트

import React, { useState } from "react";
import Child from "./Child";

const App = () => {
  const [parentAge, setParentAge] = useState(0);
  const [childAge, setChildAge] = useState(0);

  const incrementParnetAge = () => {
    setParentAge((prev) => prev + 1);
  };

  const incrementChildAge = () => {
    setChildAge((prev) => prev + 1);
  };

  console.log("부모 컴포넌트 렌더링")

  return (
    <div style={{border: "2px solid blck", padding:"10px"}}>
      <h1>부모</h1>
      <p>age: {parentAge}</p>
      <button onClick={incrementParnetAge}>부모 나이 증가</button>
      <button onClick={incrementChildAge}>자식 나이 증가</button>
      <Child name={"홍길동"} age={childAge}/>
    </div>
  );
};

export default App;

Child 컴포넌트

import React, { memo } from "react";

const Child = ({ name, age }) => {
  console.log("자식 컴포넌트 렌더링");
  return (
    <div style={{border: "2px solid powerblue", padding:"10px"}}>
      <h2>자녀</h2>
      <p>name: {name}</p>
      <p>age: {age}</p>
    </div>
  );
};
export default memo(Child);

React.memo를 적용하지 않을 시 Parent컴포넌트의 ParentAge State가 업데이트 될때마다 Child 컴포넌트도 같이 렌더링이 발생되어 Child 컴포넌트에는 변경사항이 없지만 불필요한 렌더링이 발생하게 된다.
React.memo를 적용 시 렌더링이 발생할 상황이 될 때마다 Props Check통해 props가 변화되었을때만 렌더링 되도록 하고, Props가 변화되지 않았다면 이전 컴포넌트 값을 그대로 재사용하게 된다. 따라서 불필요한 렌더링을 방지할 수 있다.

React.memo useCallback, useMemo 사용 조합 예시

Parent 컴포넌트

import React, { useCallback, useMemo, useState } from "react";
import Child from "./Child";

const App = () => {
  const [parentAge, setParentAge] = useState(0);
  const [childAge, setChildAge] = useState(0);

  const incrementParnetAge = () => {
    setParentAge((prev) => prev + 1);
  };

  const incrementChildAge = () => {
    setChildAge((prev) => prev + 1);
  };

  console.log("부모 컴포넌트 렌더링");

  const name = useMemo(() => {
    return {
      myname: "홍길동",
    };
  });

  const talk = useCallback(() => {
    console.log("그래 밥 먹자");
  },[]);

  return (
    <div style={{ border: "2px solid blck", padding: "10px" }}>
      <h1>부모</h1>
      <p>age: {parentAge}</p>
      <button onClick={incrementParnetAge}>부모 나이 증가</button>
      <button onClick={incrementChildAge}>자식 나이 증가</button>
      <Child name={name.myname} age={childAge} talk={talk} />
    </div>
  );
};

export default App;

Child 컴포넌트

import React, { memo } from "react";

const Child = ({ name, age, talk }) => {
    console.log("자식 컴포넌트 렌더링");
  return (
    <div style={{border: "2px solid powerblue", padding:"10px"}}>
      <h2>자녀</h2>
      <p>name: {name}</p>
      <p>age: {age}</p>
      <button onClick={talk}>배고파요</button>
    </div>
  );
};
export default memo(Child);

위의 React.memo 예시에서 몇 가지를 추가
name 변수를 객체형태로 만들어서 추가하였고, talk라는 함수를 만들어서 Child 컴포넌트의 Props에 추가하였다.
useMemo와 useCallback를 적용하지 않고 사용하면 React.memo 적용했더라도 부모 컴포넌트가 렌더링 될때 자식 컴포넌트에 다시 렌더링이 발생하게 된다.
그 이유는 name이 객체로 선언될 시 객체는 매번 새로운 메모리 주소값을 참조하여 할당되기 때문에 값의 변화가 발생되기 때문에 React.memo를 적용했더라도 다시 렌더링이 발생하게 된다. talk 함수에서는 부모 컴포넌트가 렌더링 될때마다 함수가 재생성되므로 talk 함수 또한 재생성되어 React.memo를 적용했더라도 다시 렌더링이 발생하게된다.
useMemo와 useCallback를 name값과 talk 함수에 적용하게되면 memoization되기 때문에 다시 렌더링 되는 문제를 막을 수 있다.

React.memo 사용 시 주의사항

React.memo 역시 useMemo와 동일하게 메모리를 소비해서 값을 저장한다. 만약 불필요한 값까지 전부 메모이제이션을 하면 성능에 좋지 않은 영향을 준다. 따라서 필요할 때만 적절하게 사용하는것이 바람직하다.

React.memo를 사용하기 적합한 상황

  • 컴포넌트가 같은 Props로 자주 렌더링될때
  • 컴포넌트가 렌더링이 될때마다 복잡한 로직이 처리될때

참고 사이트

React Hooks에 취한다 - useCallback 제대로 사용하기 | 리액트 훅스 시리즈
React Hooks에 취한다 - useMemo 제대로 사용하기 | 리액트 훅스 시리즈
React.memo로 컴포넌트 최적화하기 (ft. useMemo, useCallback)

profile
함께 개선하는 개발자

0개의 댓글