ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [유데미x스나이퍼팩토리] 프로젝트 캠프 : Next.js 2기 - 사전직무교육 2주차 - Memoization
    프로젝트 캠프: Next.js 과정 2기 2024. 7. 28. 22:07
    728x90

    학습 목표

    memoization을 이해하고 렌더링 최적화 방법을 익힙니다.

    memoization이란?

    💡 컴퓨터 프로그램이 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장하여 사용함으로써 동일한 계산의 반복 수행을 제거하여 성능 향상을 도모하는 기술

     

    useCallback

    함수형 컴포넌트에서 함수를 메모이제이션하는 데 사용한다.

    사용 방법

    useCallback(fn, dependencies)

    예시

    카운트를 증가시키는 함수를 생성하고 이를 집합에 넣어보자.

    const functionSet = new Set();
    
    const App = () => {
      const [count, setCount] = useState(0);
    
      const increament = setCount((prev) => prev + 1);
    
      functionSet.add(increament);
      console.log(functionSet);
    
      return (
        <>
          <h1>Count: {count}</h1>
          <button onClick={increament}>increase</button>
        </>
      );
    };

    집합을 출력해보면 카운트를 증가시킬 때마다 increment 함수가 추가되는 것을 확인할 수 있다.

    왜 추가될까?

    카운트가 증가될 때마다 App컴포넌트가 rerendering 되는데, 이때 함수의 참조값인 increment가 갱싱되기 때문에 js는 이를 다른 객체로 인식해서 집합에 계속해서 쌓이는 것이다.

    어떻게 해결할 수 있을까?

    useCallback 사용!

    const functionSet = new Set();
    
    const App = () => {
      const [count, setCount] = useState(0);
    
      const increament = useCallback(() => {
        setCount((prev) => prev + 1);
      }, []);
    
      functionSet.add(increament);
      console.log(functionSet);
    
      return (
        <>
          <h1>Count: {count}</h1>
          <button onClick={increament}>increase</button>
        </>
      );
    };

    위와 같이 작성하면 useCallback이 함수를 저장하고 있기 때문에 카운트를 증가시키더라도 집합의 개체가 증가하지 않는 것을 확인할 수 있다.

     

    주의사항

    setState를 할 때 콜백함수를 사용하자

    만약 setState를 사용해 상태를 업데이트할 때

    const increament = useCallback(() => {
        setCount(count + 1);
      }, []);

    이런 방식으로 작성한다면 카운트는 1에서 더이상 증가하지 않을 것이다. 왜냐하면 useCallback은 스냅샷을 찍듯 함수를 저장한다. 따라서 위의 코드에서는 메모이제이션됐을 때 count가 0이었기 때문에 1에서 더이상 증가하지 않게 된다.

    dependencies를 확인하자

    useCallback에서는 dependencies에 등록된 값의 변화를 인지하고 변화가 일어났을 때 useCallback에 정의된 함수를 실행한다. 따라서 다음과 같이 작성했을 때

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

    카운트를 증가시킬 때마다 카운트를 증가시키는 함수가 실행되기 때문에 카운트가 무한으로 증가되는 문제가 발생하게 된다.

     

    useMemo

    값을 메모이제이션하는 훅이다.

    사용 방법

    const cachedValue = useMemo(calculateValue, dependencies)

    예시

    길이가 매우 긴 배열에서 특정 값을 찾아 렌더링하는 코드가 있다고 하자.

    import { useState } from "react";
    
    const initialItems = new Array(29_999_999).fill(0).map((_, i) => {
      return { id: i, selected: i === 29_999_998 };
    });
    
    const App = () => {
      const [count, setCount] = useState(0);
      const [items, _] = useState(initialItems);
      const selectedItem = items.find((item) => item.selected);
    
      return (
        <>
          <h1>Count: {count}</h1>
          <button onClick={() => setCount((prev) => prev + 1)}>increase</button>
          <h2>{selectedItem?.id}</h2>
        </>
      );
    };
    export default App;

    위와 같이 작성하면 카운트가 변경될 때마다 selectedItem은 값이 변하지 않음에도 find함수가 새롭게 실행되어 성능적인 문제가 발생하게 된다.

    어떻게 해결할까?

    import { useMemo, useState } from "react";
    
    const initialItems = new Array(29_999_999).fill(0).map((_, i) => {
      return { id: i, selected: i === 29_999_998 };
    });
    
    const App = () => {
      const [count, setCount] = useState(0);
      const [items, _] = useState(initialItems);
    
      const selectedItem = useMemo(() => items.find((item) => item.selected), []);
    
      return (
        <>
          <h1>Count: {count}</h1>
          <button onClick={() => setCount((prev) => prev + 1)}>increase</button>
          <h2>{selectedItem?.id}</h2>
        </>
      );
    };
    export default App;
    

    useMemo를 사용해 값을 지정해주면 그 값이 메모이제이션되어 값을 찾는 코드를 다시 실행하지 않는다.

     

    주의사항

    dependencies를 확인하자

    useCallback과 동일하게 useMemo 역시 dependencies에 등록된 값의 변화를 인지하고 변화가 일어났을 때 useCallback에 정의된 함수를 실행한다. 따라서 다음과 같이 작성했을 때

    const selectedItem = useMemo(
      () => items.find((item) => item.selected),
      [count]
    );

    카운트를 증가시킬 때마다 selectedItem을 찾는 함수가 실행되기 때문에 useMemo를 사용하는 의미가 없어진다.

     

    React.memo

    컴포넌트를 메모이제이션할 때 사용하는 함수이다.

    사용 방법

    const MemoizedComponent = React.memo(SomeComponent, arePropsEqual?)

    예시

    상위 컴포넌트에서 상태변화가 일어난다면 하위 컴포넌트는 모두 재렌더링된다. 이때 React.memo를 사용하면 재렌더링을 막을 수 있다.

    import { useState } from "react";
    import A from "./components/learn/A";
    import B from "./components/learn/B";
    
    const App = () => {
      const [count, setCount] = useState(0);
      return (
        <>
          <h1>App Component: {count}</h1>
          <button onClick={() => setCount((prev) => prev + 1)}>increase</button>
          <A />
          <B />
        </>
      );
    };
    export default App;
    export default React.memo(A);
    export default React.memo(B);

     

    주의사항

    만약 B 컴포넌트에 count를 props로 넘겨주면 React.memo를 지정해줬어도 App에서 count를 증가시킬 때마다 컴포넌트가 재렌더링된다.
    그 값을 사용하지 않더라도 props로 전달되는 순간부터 영향을 끼친다.
    만약 함수를 넘겨준다면 useCallback으로 함수를 메모이제이션해주면 해결할 수 있다.

     

    useCallback vs useMemo vs React.memo

      useCallback useMemo React.memo
    참조 대상 함수 컴포넌트
    훅 / 함수 함수 (고차 컴포넌트)

    *고차 컴포넌트: 컴포넌트를 반환하는 함수

    728x90

    댓글

Designed by Tistory.