늘 겸손하게

React - React.memo, useCallback, useMemo 본문

Programming/React

React - React.memo, useCallback, useMemo

besforyou999 2022. 7. 28. 18:36

useCallback, useMemo

 

useCallback, useMemo 모두 React 최적화를 위해 사용되는 hook입니다.

 

useCallback은 메모이제이션된 콜백(함수)을 반환하고 

 

useMemo는 메모이제이션된 값을 반환합니다. 

 

메모이제이션?

 

Memoization이란 프로그램이 같은 계산을 반복할 때, 이전에 계산한 값을 메모리에 저장해두고 재활용하며 불필요한

 

반복계산을 제거하여 프로그램 속도를 빠르게 하는 기술이다. 

 

동적 계획법(Dynamic Programming)의 핵심이되는 기술

 

계산값을 재활용하기 위해 메모리에 저장해두는것을 메모이징(Memoizing) 이라고 합니다.

 

React.memo

 

컴포넌트가 부모로부터 같은 props를 전달받고, 같은 컴포넌트를 렌더링한다면 불필요한 리렌더링을 피하기 위해 렌더링 결과를 메모이징하도록 컴포넌트를 래핑할 수 있습니다.

 

사용 예

 

 

메모이제이션을 이용하는것이 React 최적화에 어떻게 도움이 될까요?

 

리액트에서 컴포넌트가 리렌더링 되는 조건은

 

1. 자신의 state가 변경될 때

 

2. 부모 컴포넌트로부터 전달받은 props가 변경될 때

 

3. 부모 컴포넌트가 리렌더링 될 때

 

 

세 가지입니다.

 

어느 컴포넌트가 리렌더링되면 해당 컴포넌트의 자식 컴포넌트들 모두 조건 3에 따라서 리렌더링되는데 변경사항이 없는 자식 컴포넌트 까지도 모두 리렌더링하는 일은 낭비입니다. 그러므로 컴포넌트를 메모리에 저장해둔 뒤, 변경점이 없는 컴포넌트는 다시 렌더링하지 않고 메모리에서 읽어 재활용하는 메모이제이션이 React 최적화에 도움이 될 수 있습니다.

 

사용 예

 

간단한 카운터 앱입니다

 

 

React component 플러그인을 통해 리렌더링되는 컴포넌트는 하이라이트가 생깁니다

 

 

import React, {useCallback, useState} from 'react'
import Button from './Button';
import Title from './Title';
import './App.css';

function App() {
  const [number, setNumber] = useState(0);

  const plus = () => {
    setNumber(number + 1)
  }

  const minus = () => {
    setNumber(number - 1)
  }

  const reset = () => {
    setNumber(0)
  }

  return (
    <div className="App">
      <div className='number'>{number}</div>
      <div><Title/></div>
      <div>
        <Button onClick={plus} text="Plus"/>
        <Button onClick={minus} text="Minus"/>
        <Button onClick={reset} text="Reset"/>
      </div>
    </div>
  );
}

export default App;

 

 

위 코드의 Title 컴포넌트는 props를 전달받지 않는 순수 컴포넌트입니다

 

 

import React from "react"

function Title() {
  return ( 
    <div className='title'>This is simple counter</div>
  )
}

export default Title;

 

 

순수 컴포넌트라 리렌더링이 필요없지만 부모 컴포넌트인 app이 리렌더링되면서 같이 리렌더링되는것을 볼 수 있습니다

 

 

 

 

불필요한 리렌더링을 줄여야 성능을 향상 시킬 수 있습니다.

 

React.memo를 사용하여 Title 컴포넌트를 래핑하여 메모이징하면

 

 

import React from "react"

function Title() {
  return ( 
    <div className='title'>This is simple counter</div>
  )
}

export default React.memo(Title);

 

 

불필요한 렌더링이 막히는것을 볼 수 있습니다

 

 

 

 

마찬가지로 Button 컴포넌트도 React.memo로 래핑해보면

 

 

import React from "react"

function Title() {
  return ( 
    <div className='title'>This is simple counter</div>
  )
}

export default React.memo(Title);

 

 

 

 

 

 

기대와는 달리 Button 컴포넌트는 리렌더링 되는것을 볼 수 있습니다

 

 

 

왜 안되지?

 

컴포넌트가 리렌더링되면 해당 컴포넌트의 모든 객체들은 다시 생성됩니다.

 

그러므로 Button 컴포넌트가 전달받은 onClick 함수는 이전의 onClick 함수와 다른 레퍼런스를 가지고 있어 자바스크립트가 다른 props를 전달받은 것으로 판단하여 리렌더링이 계속되는것입니다.

 

 

해결책

 

이러한 경우를 위해 useCallback이 있습니다.

 

 

useCallback

 

자식 컴포넌트에게 전달하는 함수를 useCallback으로 감싸면 deps(dependency의 약자)가 바뀔 경우를 제외하고 동일한 레퍼런스를 넘겨 불필요한 렌더링을 막을 수 있습니다.

 

 

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

 

 

자식 컴포넌트에게 전달할 함수가 의존하는 값을 배열에 선언하여 의존하는 값이 변경될 경우 다른 함수 객체를 전달할 수 있습니다

 

 

전달할 함수를 useCallback으로 감싸면 같은 함수 객체를 전달하여 불필요한 렌더링을 막을 수 있습니다.

단, number state값에 의존하는 plus와 minus 함수는 number를 배열에 선언해두어야합니다.

 

import React, {useCallback, useState} from 'react'
import Button from './Button';
import Title from './Title';
import './App.css';

function App() {
  const [number, setNumber] = useState(0);

  const plus = useCallback(() => {
    setNumber(number + 1)
  }, [number])

  const minus = useCallback(() => {
    setNumber(number - 1)
  }, [number])

  const reset = useCallback(() => {
    setNumber(0)
  }, [])

  return (
    <div className="App">
      <div className='number'>{number}</div>
      <div><Title/></div>
      <div>
        <Button onClick={plus} text="Plus"/>
        <Button onClick={minus} text="Minus"/>
        <Button onClick={reset} text="Reset"/>
      </div>
    </div>
  );
}

export default App;

 

 

 

 

number state에 의존성이 있는 plus와 minus 함수를 props로 전달받은 button은 리렌더링되지만, 의존성이 없는 Reset button은

리렌더링이 막히는것을 볼 수 있습니다

 

 

 

 

useMemo

 

Reset Button 컴포넌트에 샘플 객체를 전달한다고 가정해보자

 

<Button onClick={reset} text="Reset" sample={{sample: 'test'}}/>

 

 

 

props로 객체를 주었더니 Reset 버튼이 리렌더링되는것을 볼 수 있다.

 

리렌더링할때마다 새로운 sample 객체가 생성되고 이전과 다른 레퍼런스를 가진 sample 객체가 props로 Button 컴포넌트에게 전달되어 리렌더링되는것.

 

리렌더링을 막기 위해서는 같은 내용이 아닌, 같은 레퍼런스값을 가지는 객체를 속성으로 전달해야한다.

 

 

 

객체를 useMemo로 감싸주자

 

 const sampleObj = useMemo(() => ({sample: 'test'}), [])

  return (
    <div className="App">
      <div className='number'>{number}</div>
      <div><Title/></div>
      <div>
        <Button onClick={plus} text="Plus"/>
        <Button onClick={minus} text="Minus"/>
        <Button onClick={reset} text="Reset" sample={sampleObj}/>
      </div>
    </div>
  );
}

 

 

 

 

객체를 useMemo로 감싸 같은 레퍼런스를 가진 객체를 Button 컴포넌트에 전달하여 리렌더링이 막히는것을 볼 수 있다

 

 

출처 

https://ko.reactjs.org/docs/hooks-reference.html#usememo

https://ko.reactjs.org/docs/hooks-reference.html#usecallback

https://leego.tistory.com/entry/React-useCallback%EA%B3%BC-useMemo-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0