React

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

하루 2022. 3. 3. 19:11

useMemo

useMemoMemoization

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>;
}

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

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


예시

App.js

import React, { useRef, useState } from 'react';
import UserList from './UserList';
import CreateUser from './CreateUser';

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

function App() {
  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]);

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

 

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

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

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

 

 

 

출처/참고