[React] useMemo

hyeondoonge·2023년 7월 6일
2

📝 useMemo?

React Hook that lets you cache the result of a calculation between re-renders.

useMemo는 연산 결과를 캐시함으로서 리렌더링으로 인한 성능 저하를 개선할 수 있고 React에서 제공하는 hooks api 중 하나다. 성능 최적화를 위한 도구로 useMemo 외에도 컴포넌트 자체를 메모하는 memo API, 연산 함수를 캐시하는 useCallback 을 제공이 되지만, 본 글에는 useMemo에 대해서 알아보자.

'오...최적화할 수 있다고? 그럼 무조건 모든 연산 결과를 메모해서 쓰는게 좋겠네?' 라고 생각했다.

장점이 있으면 단점이 있는 법... 무조건 그런 것은 아니다!

useMemo의 용도와, 불필요하게 사용된 사례를 이해함으로서 적절한 상황에서 useMemo를 사용할 수 있도록 해보자 🤹‍♀️

useMemo의 용도

1. 참조 동등성 보장

Primitive타입의 값은 별다른 조치없이 컴포넌트가 여러번 리렌더링돼도 동등성이 보장된다.

2===2, 'hello'==='hello'

컴포넌트에서 Non-primitive (ex. 배열, 객체, 함수 등) 타입의 값을 사용한다고 하자. 만약 컴포넌트가 리렌더링되면 새로운 메모리 주소가 값들에 할당된다. 따라서 동등성이 보장되지 못한다.

{ a: 1 } !== { a: 1 }

function Component () {
	const bar = [1, 2, 3];
	const [foo, setFoo] = useState();

  return (
		<div>
			<ChildComponent bar={bar} foo={foo}/>
		</div>
	);
}

function ChildComponent ({ bar }) {
	useEffect(() => {
		api(bar, foo);
	}, [bar, foo]);

	return (
		<div>Child</div>
	);
}

자식 컴포넌트는 상위 컴포넌트에서 상태변경이 일어나면 리렌더링될 것이다. effec는 bar, foo에 의존하고있다. bar는 리렌더링 될 때 마다 새로 메모리를 할당받기 때문에, effect는 상위 컴포넌트에 리렌더링이 발생할 때 마다 실행된다.

bar는 상태가 아니고 변경되지 않을 값이므로, 값을 메모해서 관리하면 의도한대로 동작할 것이다.

이를 위해 bar 변수를 useMemo로 감싸주자. const bar = useMemo(() => [1, 2, 3], [])

이제 컴포넌트에 리렌더링이 발생해도 값을 직접 새로 할당하지 않는 한 참조 동일성을 보장할 수 있다.

2. 값비싼 연산

실제 프로젝트를 하면서 오랜 시간이 걸리는 기능을 만들었던 경험이 손에 꼽는다. File I/O가 필요할 때 또는 알고리즘 풀이를 할 때 많은 입력들을 처리해야하는 경우가 그러하다.

총 소요시간이 3초가 되는 result 연산을 컴포넌트에서 해야한다고 가정하자. 이때 부모 컴포넌트에 빈번한 상태 변화가 일어나서 리렌더링이 발생하게되면 연산은 매번 수행될 것이다.

import React, { useState, useEffect } from "react";

function Runner({ number }) {
  const result = () => {
    const arr = Array.from({ length: number }, (_, index) => index);
	};

  return <div>useMemo test with expensive calculating</div>
}

const nameList = ["코난", "찰리", "비버"];

export default function App() {
  const [nameIndex, setNameIndex] = useState(0);
  const number = 50000000;

  useEffect(() => {
    const interval = setInterval(() => {
      setNameIndex((nameIndex) =>
        nameIndex === nameList.length - 1 ? 0 : nameIndex + 1
      );
    }, 1000);

    return () => {
      clearInterval(interval);
    };
  }, []);

  return (
      <Runner number={number} />
  );
}

실제로 실행해본 결과, 배열의 크기가 키울수록 서비스 속도가 느려졌다.

이처럼 시간이 걸리는 연산이 불필요하게 반복되어 성능이 저하될 경우, useMemo를 고려할 수 있다. 개선하면 이렇게 된다.

function Runner({ number }) {
  const result = useMemo(() => {
    const arr = Array.from({ length: number }, (_, index) => index);
    return arr;
	}, []);

  return <div>useMemo test with expensive calculating</div>
}

불필요하게 사용된 사례

성능 최적화를 하면 오히려 비용이 발생하는 부분이 있고, 오히려 마이너스가 될 수 있다. useMemo는동등성 검사, 코드 가독성 저하, 유지 비용등의 문제를 가지고 있다.

아래는 내가 실제 useMemo를 통해 최적화를 시도한 흔적이다.

const [auth, setAuth] = useState({
    email: '',
    password: '',
  });

const isValidEmail = useMemo(() => emailRegex.test(auth.email), [auth.email]);
const isValidPassword = useMemo(() => pwRegex.test(auth.password), [auth.password]);

내가 useMemo를 사용 이유는 email 또는 password 둘 중 하나라도 변경되면 리렌더링이 일어나는데, 이때 발생하는 불필요한 유효성 검사 비용을 없애려고 했기 때문이다. 실제 실행해보며 메모를 통해 불필요하게 유효성 검사를 하지않는 것을 확인했다.

그럼, 불필요한 비용을 줄였으니 적절하게 사용된 것 아닌가? 🤔

훅을 사용하는데 발생한 비용도 살펴봐야한다. useMemo의 호출로 인한 코드 복잡성이 증가한다.

또한 이전 의존성 배열과 비교해서 달라진 점은 없는지 동등성 검사를 수행한다.

외에도 useMemo로 인해 저장된 값들이 gc에 의해 수거되지 않아, 메모리에 쌓이게 되는 단점이 있다.

해당 사례는 유효성 검사와 의존성 검사에 드는 비용만 비교해도 O(N)으로 아주 비슷할 것인데, 추가적으로 코드 가독성까지 떨어지는 상황이다. 따라서 성능 개선을 시도했지만, 불필요한 개선이었다고 할 수 있겠다.


결론

이런 memo와 관련된 도구들은 성능이 저하되어 서비스에 영향을 미칠 때 고려해야할 것 같다. 성능 저하가 당장에 발생하지도 않는데, 성능 향상을 위한 목적으로 memo를 했지만 오히려 마이너스가 될 수 있기 때문이다.

useMemo 뿐만 아니라 메모하는 다른 훅들도 마찬가지로, 얻는 게 있으면 잃는 게 있듯이 불필요한 연산 비용을 줄여도 의존성 비교나 코드 작성 비용 등이 발생할 수 있다.

따라서 요구사항을 파악하고, 최적화를 통해 프로덕트 성능이 개선된다는 객관적인 근거가 존재한다면 그때 사용하는 것이 좋겠다.

그러는 것이 다른 팀원들도 납득하기 쉬울 것이고, 프로덕트 관점에서 가치있는 행동을 했다고 볼 수 있기 때문이다.

성능 최적화에는 trade-off 가 따른다는 걸 파악하고, 필요한 시점에 적절한 도구를 선택할 수 있도록 해야겠다.

참고

useMemo and useCallback - kentcdodds
useMemo - react

0개의 댓글