useMemo를 사용하여 연산값 재사용하기

송은·2023년 6월 13일
0

useMemo

useMemo → Memoization

Memoization 이란 기존에 수행한 연산의 결과값을 어딘가에 저장해두고 동일한 입력이 들어오면 재활용하는 프로그래밍 기법을 말한다. memoization을 적절히 적용하면 중복 연산을 피할 수 있기 때문에 메모리를 조금 더 쓰더라도 애플리케이션의 성능을 최적화 할 수 있다.

만약에, 함수가 내부적으로 매우 복잡한 연산을 수행해서 결과값을 리턴하는데 시간이 몇초 이상 오래 걸린다면 컴포넌트의 리렌더링이 필요할 때 마다 함수가 호출되므로 사용자는 지속적으로 UI에서 지연이 발생하는 경험하게 될 것이다.

이를 memorization 기법을 적용하면 React의 useMemo Hook 함수를 이용하여 개선시킬 수 있다.


사용법

첫번째 파라미터에는 (어떻게 연산할지 정의해주는)결과값을 생성해주는 팩토리 함수이고, 두번째는 기존 결과값 재활용 여부의 기준이 되는 입력값(deps) 배열이다. useMemo는 성능을 최적화 할 때 사용된다.

const count = useMemo(() => countActiveUsers(users), [users]);

배열 안에 내용이 바뀌게 되면 등록한 함수를 호출해서 값을 연산해주고, 만약에 내용이 바뀌지 않았다면 이전에 연산한 값을 재사용한다.

function MyComponent({ x, y }) {
  const z = useMemo(() => compute(x, y), [x, y]);
  return <div>{z}</div>;
}

xy값이 이전에 렌더링했을 때와 동일할 경우, 이전 렌더링 때 저장해두었던 결과값을 재활용한다.

하지만, xy값이 달라졌을 경우, ( ) => compute(x, y) 함수를 호출하여 결과값을 새롭게 구해 z에 할당해준다.


예시

function countActiveUsers(users) {
  console.log('활성 사용자 수를 세는중...');
  return users.filter(user => user.active).length;
}

function App() {
  // input 상태 관리
  const [inputs, setInputs] = useState({
    username: '',
    email: ''
  });
  const { username, email } = inputs;
  
  // 인풋 입력 이벤트 핸들러
  const onChange = e => {
    const { name, value } = e.target;
    setInputs({
      ...inputs,
      [name]: value
    });
  };
  
  // 유저 정보
  const [users, setUsers] = useState([
    {
      id: 1,
      username: 'velopert',
      email: 'public.velopert@gmail.com',
      active: true
    },
    {
      id: 2,
      username: 'tester',
      email: 'tester@example.com',
      active: false
    },
    {
      id: 3,
      username: 'liz',
      email: 'liz@example.com',
      active: false
    }
  ]);

  const nextId = useRef(4);
  
  // 새로운 유저 생성 함수
  const onCreate = () => {
    const user = {
      id: nextId.current,
      username,
      email
    };
    setUsers(users.concat(user));

    setInputs({
      username: '',
      email: ''
    });
    nextId.current += 1;
  };

 // 유저 삭제 함수
  const onRemove = id => {
    // user.id 가 파라미터로 일치하지 않는 원소만 추출해서 새로운 배열을 만듬
    // = user.id 가 id 인 것을 제거함
    setUsers(users.filter(user => user.id !== id));
  };
  const onToggle = id => {
    setUsers(
      users.map(user =>
        user.id === id ? { ...user, active: !user.active } : user
      )
    );
  };
  const count = countActiveUsers(users);
  
  return (
    <>
      <CreateUser
        username={username}
        email={email}
        onChange={onChange}
        onCreate={onCreate}
      />
      <UserList users={users} onRemove={onRemove} onToggle={onToggle} />
      <div>활성사용자 수 : {count}</div>
    </>
  );
}

export default App;

countActiveUsers 함수에서 console.log를 통해 함수가 호출 될 때 마다 확인할 수 있도록 했다.

function countActiveUsers(users) {
  console.log('활성 사용자 수를 세는중...');
  return users.filter(user => user.active).length;
}

사용자명 활성 시 콘솔창

여기서 다른 사용자명을 눌러서 활성화 시키면 활성 사용자수가 늘어나고, '활성 사용자 수를 세는중...' 로그가 기록된다.

렌더링 시 활성 사용자 수도 카운트되는 문제

여기서 발생하는 문제는 input에 사용자를 추가하려고 작성할 때(바꿀때)에도 countActiveUsers가 호출되면서 '활성 사용자 수를 세는중' 로그가 찍힌다는 것이다.

활성 사용자 수 로그가 찍히는 모습

활성 사용자의 수를 세는 건 users에 변화가 있을 때만 세야되는 건데, input값이 바뀔 때에 컴포넌트가 리렌더링 되므로 이렇게 불필요할 때에도 호출하여 자원이 낭비된다.

이러한 상황에 useMemo Hook 함수를 이용해서 성능을 최적화 시켜줄 수 있다.

const count = useMemo(() => countActiveUsers(users), [users]);

useMemo를 최적화 시켜준 뒤, 렌더링 모습

이제 input값이 바뀌어도 리렌더링되지 않는다.


일반적으로 소프트웨어의 성능 최적화에는 그에 상응하는 대가가 따른다. 따라서 성능 최적화를 할 때는 얻을 수 있는 실제 성능 이점이 지불하는 대가에 비해서 미미하지 않은지에 대해서 반드시 따져보고 사용을 해야 한다.

예를 들어, useMemo hook 함수를 남용하면 컴포넌트의 복잡도가 올라가기 때문에 코드를 읽기도 어려워지고 유지보수성도 떨어지게 된다. 또한 useMemo가 적용된 레퍼런스는 재활용을 위해서 가비지 컬렉션(garbage collection)에서 제외되기 때문에 메모리를 더 쓰게 된다.

실제 웹 프로젝트에서 useMemo Hook 함수를 사용할 일은 생각보다 그렇게 많지 않다.
왜냐하면 수초 이상 걸리는 로직이 프론트엔드에 존재한다는 것 자체가 일반적인 앱에서는 흔치 않은 일이고, 설사 그렇게 오래걸리는 로직이 있다고 해도 useEffect hook 함수 등을 이용해서 비동기로 처리하는 방안을 먼저 고려되기 때문이다.
따라서 useMemo가 빛을 발휘할 수 있는 상황은 극히 제한적이며, 브라우저에서 React가 실행되는 속도도 워낙 빠르다보니 웬만한 컴포넌트가 여러번 렌더링이 일어난다고 해서 실제 심각한 성능 이슈로 이어지는 경우는 의외로 적다.




출처

profile
개발자

0개의 댓글