JS/React

React - memo & useMemo(재랜더링 막기)

shin96bc 2024. 3. 8. 23:24

memo 를 사용한 자식 컴포넌트 재랜더링 막기

먼저 실험을 위해 자식 컴포넌트와 재랜더링 시키기 위한 state 를 만들어주고, 버튼도 하나 만들어줍니다.

  • Cart.js

 

 

이렇게 만들면 실험용버튼 이라는 버튼을 누르면 Cart 라는 컴포넌트가 재랜더링이 될겁니다. 근데 이렇게 부모 컴포넌트가 재랜더링되면 모든 자식 컴포넌트도 같이 재랜더링 됩니다. 그런데 이렇게 모든 자식 컴포넌트가 같이 재랜더링되면 성능저하를 일으킬 수 있습니다.

 

이런 성능 저하를 막기위해서는 아래와 같이 코드를 작성해줘서 자식 컴포넌트가 재랜더링 되지 않도록 해주면 됩니다. ( 정확히는 자식 컴포넌트를 꼭 필요할 때만 재랜더링 시켜줘라 라는 의미입니다. )

 

아래의 코드와 같이 컴포넌트를 생성할 때 memo 함수안에 생성해주시면 됩니다.

  • Cart.js

 

  • 이렇게 재랜더링이 오래걸리는 컴포넌트들은 memo 함수를 이용해서 생성해주는 것이 좋습니다.

 

memo 의 원리

 

memo 는 그냥 재랜더링을 막아주는 함수가 아니라 특정 상황에서만 재랜더링을 시켜주는 함수입니다.

정확히는 memo 안에 들어있는 컴포넌트로 전송되는 props 가 변할 때만 재랜더링 해줍니다.

  • Cart.js

 

 

그렇다고 모든 컴포넌트를 memo 로 감싸는 것은 좋지않습니다. memo 로 감싸놓은 컴포넌트는 항상 재랜더링 하기전에 기존 props 랑 신규 props 랑 같은지 다른지를 계속 비교해서 재랜더링 여부를 결정합니다.

 

만약에 자식 컴포넌트로 전송되는 props 가 길고 복잡하다면 비교하는 작업 자체가 시간이 오래걸려서 오히려 손해일 수도 있습니다.

대부분은 memo 를 쓸 일이 없고, 진짜 무거운 컴포넌트가 있을 때만 적절히 사용합시다.

 

useMemo 를 사용해서 재랜더링시 특정 함수의 호출 막기

만약에 아래처럼 Cart 컴포넌트에서 10억번 반복하는 loop 라는 함수를 사용해야한다고 가정해봅시다.

  • Cart.js

 

 

이럴 때 문제점은 Cart 컴포넌트가 재랜더링 될 때 마다 10억번 반복하는 loop 라는 함수가 실행되서 버튼한번 누를 때 마다 loop 함수가 실행되서 반복문을 10억번씩 계속 돌리면서 매우 비효율적으로 동작하는 상황이 됩니다.

 

그럴 때 useMemo 를 사용해서 첫번째 랜더링 때 딱 한번만 실행하도록 해주면 됩니다.

 

사용법은 아래의 코드처럼 무거운 함수를 useMemo 로 감싸주면 됩니다.

  • Cart.js

 

추가로 useMemo 에 디펜던시를 추가해서 원하는 state 가 변화할 때만 useMemo 안에 함수를 실행시키는 방법도 있습니다.

  • Cart.js

 

 

근데 생각해보면 useEffect 랑 비슷한데 왜 굳이 두개로 나눠서 사용하는가하면 useEffect 는 컴포넌트에 있는 html 의 랜더링이 끝나면 실행되고, useMemo 는 html 의 랜더링과 동시에 실행이되는 차이 정도만 있지 거의 비슷합니다.

 

다만 ‘useMemo’는 react-query로 받아온 데이터를 ‘props’로 자식 component에 넘겨줄 때 불필요한 재랜더링을 막아줄 수 있습니다.

  • 만약 아래와 같이 react-query로 받아온 데이터를 새로운 object로 가공해서 ‘props’로 넘겨주는 코드가 있다고 가정합니다.
  • 이 때 ‘state’의 변경이 발생해서 재렌더링 되는 경우 실제로 react-query의 ‘data’안에 있는 값은 변하지 않았지만 새로운 object를 생성해서 ‘props’로 넘겨주고 있기 때문에 ‘jsx’ 부분이 재렌더링 되면서 새로운 object가 생성됩니다.
  • 그래서 자식 component에서는 ‘props’의 값이 변경되었다고 인식되고 자식 component까지 재랜더링이 발생하게 됩니다.
function App () {
    const {
        data,
        isFetching,
        isSuccess,
        isLoading,
        refetch, 
      	remove,
        isError,
        error,
    } = useQuery('key', ({queryKey, pageParam, signal, meta}) => {
        return axios.get('url', {params: {}, header: {}});
    }, {
        cacheTime: 0,
        retry: 0,
        refetchOnWindowFocus: false,
        refetchOnReconnect: false,
        enabled: true,
        onError: () => {},
    });

    const [state, setState] = useState(0);

    return (
        <div>
            <div>
                <button type="button" onClick={() => {
                    setState(state + 1)
                }}>버튼</button>
            </div>
            {/* 
                위의 버튼을 눌러서 state가 변경되면 jsx 부분이 재렌더링되기 때문에 
                props로 넘겨주는 object({name: data.name, age: data.age})도 새로 생성됩니다.
            */}
            <ChildComponent props={{name: data.name, age: data.age}} />
        </div>
    );
}

 

  • 이런 상황에서 부모 component의 ‘state’ 변경에 의해서 자식 component가 재랜더링 되지 않아야만 하는 상황이라면 ‘useMemo’를 감싸서 재랜더링을 막아줄 수 있습니다.
  • 물론 자식 component도 ‘memo’로 감싸놓아야 합니다.
function App () {
    const {
        data,
        isFetching,
        isSuccess,
        isLoading,
        refetch, 
      	remove,
        isError,
        error,
    } = useQuery('key', ({queryKey, pageParam, signal, meta}) => {
        return axios.get('url', {params: {}, header: {}});
    }, {
    	cacheTime: 0,
  		retry: 0,
  		refetchOnWindowFocus: false,
  		refetchOnReconnect: false,
  		enabled: true,
  		onError: () => {},
    });

    // 디펜던시에 data를 추가해서 실제로 react-query가 fetching을 진행해서 
    // data의 값이 변경되었을 때만 memoizedState의 값을 바꿔줍니다.
    const memoizedState = useMemo(() => {
        return {name: data.name, age: data.age};
    }, [data]);

    const [state, setState] = useState(0);

    return (
        <div>
            <div>
                <button type="button" onClick={() => {
                    setState(state + 1)
                }}>버튼</button>
            </div>
            {/* 
                이렇게 useMemo를 사용하면 state가 변경되어도 넘겨주는 props의 값인
                memoizedState는 변하지 않았으므로 자식 component는 재렌더링되지 않습니다.
            */}
            <ChildComponent props={memoizedState} />
        </div>
    );
}