ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [React]When to use UseMemo and useCallback (feat:memo) / useCallback 편
    front-end/React 2023. 8. 9. 17:57

    리액트로 프론트 개발을 하다면 useMemo , useCallback 과 같은 컴포넌트 렌더링 최적화 훅을 한번쯤은 들어봤을텐데요.

    저는 솔직히 지금까지 제일 헷갈리는것 같습니다. 왜냐하면 렌더링 최적화 훅이라고 해도 그냥 막 사용하면 오히려 최적화가 아니라 

    성능에 안좋을 수 있어서, 상황에 맞게 잘 써야합니다.

     

    메모이제이션

    우선 useCallback 과 useMemo를 사용하기 위해선 "메모제이션" 이란 단어를 필수로 짚고 넘어가야합니다.

    메모제이션(memoziaiton)은 expensive 한 함수 호출 결과를 캐싱하고 같은 입력이 재발생 될때 캐싱된 결과를 그대로 반환하는

    프로그래밍 기술 입니다. 

     

    리액트 함수형 컴포넌트에서 동일한 입력으로 여러번 호출 되는 상황이 꽤 많습니다.

    이때 리액트에서 메모이제이션을 사용할때 useMemo , useCallback 훅을 사용해서 렌더링 최적화 및 성능 상향을 시킬 수 있습니다.

     

    useMemo 와 useCallback을 배우기전에 알아야 하는것

    1. 우선 리액트 함수형 컴포넌트는 단지 jsx 를 반환하는 함수 입니다.

    2. 컴포넌트가 렌더링 된다는것은 컴포넌트(함수)를 호출이 되서 실행이 되고 , 자바스크립트의 함수는 실행 될때마다 내부에 선언되어있던 변수가 매번 다시 선언 되어 사용됩니다.

    3. 함수형 컴포넌트는 자신의 state가 변경되고, 자식컴포넌트 경우 부모컴포넌트의 state가 변경 되어도 리렌더링(재호출)이 된다.

     

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

    useCallback :메모이제이션 된 함수를 반환합니다.

     

    또한 자바스크립트의 기초가 있다면 참조타입의 함수나 객체 배열은 아래와 같이 작동합니다.

    let dog1 = func(){console.log('14/10')}; // has a unique object reference
    let dog2 = func(){console.log('14/10')}; // has a unique object reference
    
    dog1 === dog2; // false
    
    let arr = [1,2,3]
    let arr2 = [1,2,3]
    
    arr === arr2 // false;
    
    const obj = {name:'kwangmin'}
    const obj2 = {name:'kwangmin'}
    
    obj === obj2 // false;



    헷갈리지 말아야할게 변수에 할당된 객체가 직접 다른 변수에 할당된 경우 참조가 일치할 것입니다.

    let dog1 = func(){console.log('14/10')}; // has a unique object reference
    let dog2 = dog1;  // assign the unique object reference of dog1 to a variable named dog2
    
    // dog1 and dog2 point to same object reference
    dog1 === dog2; // true
    
    let arr = [1,2,3,4];
    let arr2 = arr;
    
    arr === arr2 // true
    
    const obj = {name:'kwangmin'};
    const obj2 = obj;
    
    obj === obj2 // true

     

     

     

    UseCallback

    그럼 useCallback 언제 어떻게 사용하는지 알아보겠습니다.

    ParentComponent라는 컴포넌트를 생성해 보겠습니다.

    이 컴포넌트에는 age와 salary라는 상태(state) 그리고 다음과 같은 함수들이 있습니다
    1. incrementAge(): "나이 증가" 버튼을 클릭하면 나이를 1 증가시킵니다.
    2. incrementSalary(): "급여 증가" 버튼을 클릭하면 급여를 1000 Rs 증가시킵니다.

    ParentComponent는 다음과 같이 3개의 자식 컴포넌트를 가지고 있습니다.
    1. <Title>: 페이지 제목을 렌더링합니다.
    2. <Count>: 페이지에 나이나 급여를 표시합니다.
    3. <Button>: 나이와 급여를 증가시키기 위한 버튼입니다. 이 세가지 컴포넌트 모두 컴포넌트가 재렌더링될 때마다 로그 문장을 추가하여 결과를 확인할 수 있도록 합니다.

     

    Code Snippet

    // App.tsx
    import ParentComponent from "./components/ParentComponet";
    
    export default function App() {
      return (
        <ParentComponent/>
      )
    }
    // ParentComponet.tsx
    
    import React, { useState } from 'react';
    import Button from './Button';
    import Title from './title';
    import Count from './Count';
    function ParentComponent() {
      const [age, setAge] = useState(25);
      const [salary, setSalary] = useState(25000)
      const incrementAge = () => {
        setAge((prev => prev +1));
      }
      const incrementSalary = () => {
        setSalary(prev => prev + 10000);
      }
      return (
        <div>
          <Title />
          <Count text="age" count={age} />
          <Button handleClick={incrementAge}>Increment my age</Button>
          <Count text="salary" count={salary} />
          <Button handleClick={incrementSalary}>Increment my salary</Button>
        </div>
      );
    }
    export default ParentComponent;
    // Button.tsx
    import React from 'react';
    
    function Button(props:any) {
      console.log(`Button clicked ${props.children}`);
      return (
        <div>
          <button onClick={props.handleClick}> {props.children} </button>
        </div>
      );
    }
    export default Button;
    // Count.tsx
    import React from 'react';
    
    function Count(props:any) {
      console.log("Count rendering");
      return (
        <div>
          {props.text} is {props.count}
        </div>
      );
    }
    export default Count;
    // Title.tsx
    
    export default function Title() {
    console.log("Title is rendering.")
      return (
        <div>
          <h2>useCallBack hook</h2>
        </div>
      )
    }

     

    incrementAge  버튼을 클릭하면 나이가 25에서 26으로 증가합니다. 그러나 콘솔에서 확인해보면 모든 컴포넌트가 다시 호출되었습니다나이가 성공적으로 증가되었음을 볼 수 있지만, 다른 모든 컴포넌트도 매번 재렌더링되었고, 그렇게 되면 안되겠죠?
    App에서 변경된 것은 나이 상태 뿐입니다. 그럼에도 불구하고 모든 리액트 컴포넌트가 변경 여부와 관계없이 재렌더링됩니다.

     

     

    예제에서 나이를 증가시킬 때는 두 가지만 다시 렌더링되어야 합니다. 나이와 관련된 Count 컴포넌트. 나이를 증가시키는 Button 컴포넌트. 다른 세 가지 로그 문장은 다시 렌더링될 필요가 없습니다. 급여 역시 마찬가지입니다.

    급여를 증가시키면 제목과 나이 컴포넌트가 다시 렌더링되지 않아야 합니다. 그렇다면 어떻게 이를 최적화할 수 있을까요?

     

    그 답은 React.memo() 입니다.

     

     

    React.memo란?

    React.memo는 함수형 컴포넌트의 부모 컴포넌트로부터 전달받은 props나 상태state 변경되지 않는 한 해당 컴포넌트의 재렌더링을 방지하는 고차 컴포넌트(HOC)입니다. React.memo()는 훅과 아무 관련이 없다는 것을 염두에 두시기 바랍니다. 이 기능은 리액트 버전 16.6부터 제공되었으며, 클래스 컴포넌트에서는 이미 PureComponent 또는 shouldComponentUpdate를 사용하여 재렌더링을 제어할 수 있었습니다. 

     


    아래와 같이 예제에서 React.memo()를 활용해 보겠습니다.

    memo로 래핑을 해주세요.

    export default React.memo(Button); 
    export default React.memo(Count); //Count.js
    export default React.memo(Title); //Title.js

     

    이제 해당 컴포넌트는 상태(state)나 프롭스(props)에 변경이 있을 때만 재렌더링됩니다. 

    이게 잘 작동하는지 테스트해보죠. 그러나 여전히 안됩니다.

    브라우저에서 페이지를 로드하면 처음에는 5개의 로그가 모두 나타납니다. 


    이제 incrementAge() 를 클릭한 후, 로그가 적어진 것을 볼 수 있습니다. 그럼에도 불구하고 아직 완벽하지 않습니다. 나이를 증가시키면 급여 증가 버튼이 여전히 재렌더링되고, 급여를 증가시키면 나이 증가 버튼이 재렌더링됩니다.
    이런 일이 일어나는 이유를 알아보겠습니다.

     

    Title 컴포넌트는 자체 state나 props가 없으므로 리렌더링되지 않습니다. Count는 나이를 props로 받고, 버튼은 incrementAge()를 props로 받으며 이것은 나이에 의존적입니다. 그래서 둘 다 재렌더링됩니다. 그런데 우리가 볼 수 있는 것은 incrementSalary 버튼도 재렌더링됩니다. 왜일까요?

     

    급여에 대한 count 컴포넌트는 재렌더링되지 않습니다. 이는 부모 컴포넌트가 재렌더링될 때마다 새로운 incrementSalary 함수가 생성되기 때문입니다. 함수를 다룰 때는 항상 참조 동등성을 고려해야 합니다. 두 함수가 완전히 동일한 동작을 하더라도 서로 같다는 것을 의미하지 않습니다. 그래서 리렌더링 전의 함수와 리렌더링 후의 함수는 다릅니다. 그리고 함수인 handleClick이 props이기 때문에, React.memo는 프롭스가 변경되었다고 판단하고 재렌더링을 막지 않습니다. 그래서 3개의 로그가 남게 됩니다.

     

    따라서 이제 React에게 매번 incrementSalary 함수를 생성할 필요가 없다는 것을 알려야 합니다. 그 답은 useCallback 훅입니다. useCallback 훅은 incrementSalary 함수를 캐시하고, 급여가 증가되지 않는 경우 해당 함수를 반환합니다. 급여가 변경될 경우에만 새로운 함수가 반환됩니다.

      const incrementAge = useCallback(() => {
        setAge(prev => prev + 1);
      }, [age]);
    
      const incrementSalary = useCallback(() => {
        setSalary(prev => prev + 10000);
      }, [salary]);

    만약 자식 컴포넌트가 React.memo() 같은 것으로 최적화 되어 있고 그 자식 컴포넌트에게 callback 함수를 props로 넘길 때, 상위 컴포넌트에서 useCallback 으로 함수를 선언하는 것이 유용하다라는 의미이다. 함수가 매번 재선언되면 자식 컴포넌트는 넘겨 받은 함수가 달라졌다고 인식하기 때문이다. React.memo()로 함수형 컴포넌트 자체를 감싸면 넘겨 받는 props가 변경되지 않았을 때는 상위 컴포넌트가 메모리제이션된 함수형 컴포넌트(이전에 렌더링된 결과)를 사용하게 된다. 함수는 오로지 자기 자신만이 동일하기 때문에 상위 컴포넌트에서 callback 함수를 (같은 함수이더라도) 재선언한다면 props로 callback 함수를 넘겨 받는 하위 컴포넌트 입장에서는 props가 변경 되었다고 인식한다.

     

    When should you not use the useCallback hook

    useCallback 훅을 모든 참조 동등성 문제를 해결하기 위해 사용해서는 안 됩니다. 매번 재선언을 피하는 좋은 해결책처럼 보일 수 있지만, 여기서 주의할 점은 메모이제이션이 일부 데이터를 메모리에 저장하는 기술임을 염두에 두어야 합니다. 애플리케이션의 모든 부분을 메모이제이션하는 것은 더 많은 메모리 리소스를 사용하며, 이로 인해 애플리케이션의 성능에 부정적인 영향을 미칠 수 있습니다.

     

     

    Reference

    https://www.memberstack.com/blog/using-usecallback

     

    Using useCallback Hook in React — A Developer's Guide | Memberstack Blog

     

    www.memberstack.com

    https://kentcdodds.com/blog/usememo-and-usecallback

     

    When to useMemo and useCallback

    Stay up to date Subscribe to the newsletter to stay up to date with articles, courses and much more! Learn more Stay up to date Subscribe to the newsletter to stay up to date with articles, courses and much more! Learn more All rights reserved © Kent C. D

    kentcdodds.com

     

Designed by Tistory.