글 작성자: bbangson
반응형

useMemo

useMemo Hook성능 최적화를 위하여 사용됩니다.

 

사실 useMemo 함수를 알기 전에 메모이제이션(memoization) 개념을 알고 있으면 좋습니다. 간단하게 설명하고 넘어가겠습니다. 

 

메모이제이션이란 기존에 수행한 연산의 결과값을 변수나 어딘가에 저장하고 동일한 입력이나 행동이 들어올 경우 그 값을 재활용하는 프로그래밍 기법을 말합니다.

이를 잘 활용하면, 중복 연산을 피할 수 있고 애플리케이션의 성능을 최적화시킬 수 있습니다. 

 

이 메모이제이션 개념이 들어간 로직을 간편하게 사용할 수 있게끔 해주는 것이 useMemo hook입니다. 

 

useMemo 함수는 2개의 인자를 받습니다. 

1. 결과값을 생성해주는 함수

2. 기존의 결과값을 재활용 여부의 기준이 되는 기존 배열

 

즉, 어떤 값을 렌더링 할 때 기존의 렌더링 했을 때와 동일한 결과를 렌더링 할 경우, 이 전 렌더링 때 저장해두었던 결과값을 재활용합니다. 하지만 기존의 값과 다를 경우에는 함수를 호출하여 새로운 결과값을 생성합니다. 

 

간단한 실습을 통해 알아보겠습니다. 

UserList.js와 CreateUser.js 컴포넌트는 이전 React 포스팅을 통해서 확인하여 주시면 감사하겠습니다. 

 

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 => {
    setUsers(users.filter(user => user.id !== id));
  };
  
  const onToggle = id => {
    setUsers(
      users.map(user =>
        user.id === id ? { ...user, active: !user.active } : user
      )
    );
  };
  // 활성 사용자 수의 갯수를 count에 저장한다.
  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 함수에서 콘솔에 메시지를 출력하도록 하여 함수가 호출될 때마다 우리가 확인할 수 있게끔 했습니다. 

 

계정명을 눌러서 초록색 혹은 검은색으로 만들면 활성 사용자 수 또한 업데이트됩니다. 

 

하지만 위의 코드는 성능적 문제가 있습니다. 바로 input 값을 바꿀 때에도 countActiveUsers 함수가 호출된다는 것입니다. 

 

활설 사용자 수를 세는 건, users에 변화가 있을 때만 계산을 해야 하지만, input 값이 바뀔 때에도 컴포넌트가 리렌더링 되므로 불필요하게 호출하여서 자원이 낭비되고 있습니다. 

 

 

App.js 결과 화면 

결과 화면 

이러한 문제는 useMemo Hook함수를 통해서 성능 최적화를 할 수 있습니다. 

참고로 Memo는 "memoized"를 뜻하고, 이전에 계산한 값을 재사용한다는 의미를 갖고 있습니다.

 

App.js를 아래와 같이 바꿔보겠습니다. 

 

App.js

import React, { useRef, useState, useMemo } 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 => {
    setUsers(users.filter(user => user.id !== id));
  };
  const onToggle = id => {
    setUsers(
      users.map(user =>
        user.id === id ? { ...user, active: !user.active } : user
      )
    );
  };
  // users가 바뀔 때에만 호출이되고 그렇지 않으면 기존의 값을 재사용한다.
  const count = useMemo(() => countActiveUsers(users), [users]);
  
  return (
    <>
      <CreateUser
        username={username}
        email={email}
        onChange={onChange}
        onCreate={onCreate}
      />
      <UserList users={users} onRemove={onRemove} onToggle={onToggle} />
      <div>활성사용자 수 : {count}</div>
    </>
  );
}

export default App;

 

결과 화면 

결과 화면

다시 설명하면, useMemo의 첫 번째 파라미터에는 연산 결과 값에 대한 함수를 넣어주고, 두 번째 파라미터에는 deps 배열(즉, 결과 값이 기존에 있는지 없는지 판단하는 기존 배열)을 넣어주면 됩니다. 

 

이 배열 안에 넣은 내용이 바뀐다면, 첫 번째 파라미터에 등록한 함수를 호출해서 값을 연산하여 새로운 값을 도출해주고, 만약에 바뀌지 않는다면 이전에 연산한 값을 재사용하게 됩니다. 

 

위의 결과 사진은 계정 명을 클릭했을 경우, 새로운 값을 도출해야 하기 때문에 useMemo의 첫 번째 파라미터인 함수를 호출하게 되지만 input 값을 수정했을 경우에는 활성 사용자 수가 수정되지 않기 때문에 기존에 있는 배열에서 결과 값을 가져오게 됩니다. 

 

 

 

 

useCallback

useCallback Hook 함수는 위에 있는 useMemo와 유사한 성격의 Hook입니다. 

 

useMemo는 특정 결과 값을 재사용할 때 사용하는 반면, useCallback은 특정 함수를 새로 만들지 않고 재사용하고자 할 때 사용합니다. 

 

useCallback 함수 또한 2가지를 인자로 받습니다. 

 

1. 선언된 함수 

2. 기존 값을 변경되기 전까지 저장할 수 있는 배열 

 

기존 App.js 코드를 한번 살펴보겠습니다. 

 

App.js

const onCreate = () => {
  const user = {
    id: nextId.current,
    username,
    email
  };
  setUsers(users.concat(user));

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

const onRemove = id => {
  setUsers(users.filter(user => user.id !== id));
};
const onToggle = id => {
  setUsers(
    users.map(user =>
      user.id === id ? { ...user, active: !user.active } : user
    )
  );
};

onCreate, onRemove, onToggle 위와 같은 함수들은 컴포넌트가 리 렌더링 될 때마다 새로 만들어집니다.

 

함수를 선언하는 것 자체는 메모리, CPU, 리소스에 부담이 가는 작업은 아닙니다. 하지만 한번 만든 함수를 필요할 때만 새로 만들고 재사용하는 것은 중요합니다. 

 

컴포넌트에서 props가 바뀌지 않았으면 Virtual DOM에 새로 렌더링 하지 않는 것조차 결국 컴포넌트의 결과물을 재사용하는 최적화 작업입니다. 

 

이 작업을 하기 위해서 함수를 재사용하는 것이 필수입니다. 

 

useCallback 함수를 사용하여 코드를 작성해보겠습니다. 

 

App.js

import React, { useRef, useState, useMemo, useCallback } 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;
  
  // onChange 함수는 inputs가 바뀔 때만 새로 만들어 호출하게 되고
  // 그렇지 않으면, 기존의 함수를 재사용한다.
  const onChange = useCallback(
    e => {
      const { name, value } = e.target;
      setInputs({
        ...inputs,
        [name]: value
      });
    },
    [inputs]
  );
  
  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 = useCallback(() => {
    const user = {
      id: nextId.current,
      username,
      email
    };
    setUsers(users.concat(user));

    setInputs({
      username: '',
      email: ''
    });
    nextId.current += 1;
  // username과 email도 inputs에서 바깥으로 꺼내온 값이긴 하지만 
  // 이러한 값들도 결국엔 상태이기 때문에 dep 배열에 넣어줘야 한다.
  }, [users, username, email]);

  const onRemove = useCallback(
    id => {
      setUsers(users.filter(user => user.id !== id));
    },
    [users]
  );
  
  const onToggle = useCallback(
    id => {
      setUsers(
        users.map(user =>
          user.id === id ? { ...user, active: !user.active } : user
        )
      );
    },
    [users]
  );
  
  const count = useMemo(() => countActiveUsers(users), [users]);
  
  return (
    <>
      <CreateUser
        username={username}
        email={email}
        onChange={onChange}
        onCreate={onCreate}
      />
      <UserList users={users} onRemove={onRemove} onToggle={onToggle} />
      <div>활성사용자 수 : {count}</div>
    </>
  );
}

export default App;

함수 안에서 사용하는 상태 혹은 props가 있다면 두 번째 파라미터인 deps 배열 안에 포함시켜야 합니다. 

 

만약 deps 배열 안에 useCallback 함수에서 사용하는 값을 넣지 않게 된다면, 함수 내에서 해당 값들을 참조할 때

가장 최신 값을 참조할 것이라고 보장할 수 없습니다. 또한, 만약 props로 받아온 함수가 있다면 이 또한 deps에 넣어주어야 합니다. 

 

useCallback을 사용한다고 해서 바로 이뤄낼 수 있는 눈에 띄는 최적화는 없습니다. 컴포넌트 렌더링 최적화 작업을 해주어야만 성능이 최적화됩니다. 이 부분은 아래 React.memo 부분에서 추가적으로 다루겠습니다. 

 

 

 

React.memo

 

컴포넌트의 props가 바뀌지 않았다면, 리렌더링을 방지하여 컴포넌트의 성능을 최적화해줄 수 있는 React.memo에대해서 알아보겠습니다. 

 

이 함수를 사용한다면 컴포넌트에서 변화가 없어 리렌더링이 불필요한 상황에서는 이전에 사용했던 결과를 다시 사용할 수 있게 해줍니다.  

 

사용법은 컴포넌트 선언부나 export 부분에서 감싸주기만 하면 됩니다.  

 

CreateUser.js

import React from 'react';

const CreateUser = ({ username, email, onChange, onCreate }) => {
  return (
    <div>
      <input
        name="username"
        placeholder="계정명"
        onChange={onChange}
        value={username}
      />
      <input
        name="email"
        placeholder="이메일"
        onChange={onChange}
        value={email}
      />
      <button onClick={onCreate}>등록</button>
    </div>
  );
};

export default React.memo(CreateUser);  // React.memo 사용 부분!

 

UserList.js

import React from 'react';

const User = React.memo(function User({ user, onRemove, onToggle }) { // React.memo 사용 부분!
  return (
    <div>
      <b
        style={{
          cursor: 'pointer',
          color: user.active ? 'green' : 'black'
        }}
        onClick={() => onToggle(user.id)}
      >
        {user.username}
      </b>
      &nbsp;
      <span>({user.email})</span>
      <button onClick={() => onRemove(user.id)}>삭제</button>
    </div>
  );
});

function UserList({ users, onRemove, onToggle }) {
  return (
    <div>
      {users.map(user => (
        <User
          user={user}
          key={user.id}
          onRemove={onRemove}
          onToggle={onToggle}
        />
      ))}
    </div>
  );
}

export default React.memo(UserList);   // React.memo 사용 부분!

이런 식으로 코드를 작성하여 적용을 하면 input에서 내용을 수정해도 UserList 컴포넌트가 리렌더링 되지 않습니다. 

 

하지만 User 중 하나라도 클릭하여 색깔을 변하게 해서 수정하면 모든 User들이 리렌더링 되고, CreateUser 컴포넌트도 리렌더링 됩니다. 

 

그 이유는. users 배열이 바뀔 때마다 onCreate() 함수도 새로 만들어지고 onToggle(), onRemove()도 새로 만들어지기 때문입니다. 잠시 코드를 보면

const onCreate = useCallback(() => {
  const user = {
    id: nextId.current,
    username,
    email
  };
  setUsers(users.concat(user));

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

const onRemove = useCallback(
  id => {
    setUsers(users.filter(user => user.id !== id));
  },
  [users]
);

const onToggle = useCallback(
  id => {
    setUsers(
      users.map(user =>
        user.id === id ? { ...user, active: !user.active } : user
      )
    );
  },
  [users]
);

각 함수 deps 배열에 users가 들어있기 때문에 배열이 수정될 때마다 함수가 새로 만들어지는 건 당연합니다. 

그렇다면 이걸 최적화하고 싶다면 어떻게 할 수 있을까요?

 

deps 배열에서 users를 지우고 각 함수들에서 현재 useState로 관리하는 users를 참조하지 않게 하면 됩니다. 

이 방법은 함수형 업데이트를 통해 할 수 있습니다.

 

함수형 업데이트를 하게 되면, setUsers에 등록하는 콜백 함수의 최신 users를 참조할 수 있기 때문에 deps에 users를 넣지 않아도 됩니다.

 

코드를 수정해보겠습니다. 

 

App.js

import React, { useRef, useState, useMemo, useCallback } 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 = useCallback(e => {
    const { name, value } = e.target;
    setInputs(inputs => ({
      ...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 = useCallback(() => {
    const user = {
      id: nextId.current,
      username,
      email
    };
    // 함수형 업데이트, 항상 최신 users를 조회한다.
    setUsers(users => users.concat(user));

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

  const onRemove = useCallback(id => {
  	// 함수형 업데이트 
    // onRemove는 컴포넌트가 딱 한 번 렌더링될 때만 만들어지고 그 이후는 재사용된다. 
    // dep 배열이 빈 배열이기 때문.
    setUsers(users => users.filter(user => user.id !== id));
  }, []);
  
  const onToggle = useCallback(id => {
  	// 함수형 업데이트
    // onRemove와 마찬가지.
    setUsers(users =>
      users.map(user =>
        user.id === id ? { ...user, active: !user.active } : user
      )
    );
  }, []);
  
  const count = useMemo(() => countActiveUsers(users), [users]);
  return (
    <>
      <CreateUser
        username={username}
        email={email}
        onChange={onChange}
        onCreate={onCreate}
      />
      <UserList users={users} onRemove={onRemove} onToggle={onToggle} />
      <div>활성사용자 수 : {count}</div>
    </>
  );
}

export default App;

이렇게 해주면 특정 항목을 수정하게 될 때, 그 항목만 리렌더링 하게 됩니다. 연관 있는 다른 컴포넌트들은 기존에 있는 값을 재사용 하기 때문에 불필요하게 다시 렌더링 하는 일이 없어집니다.

 

이걸로 최적화는 마무리됐습니다.

 

 


정리

연산된 값을 재사용할 때는 useMemo, 특정 함수를 재사용하기 위해서는 useCallback, 렌더링된 결과물을 재사용하기 위해서는 React.memo를 사용합니다.

 

리액트 개발을 할 때, useCallback, useMemo, React.memo는 컴포넌트의 성능을 실제로 개선할 수 있는 상황에서만 하는 것이 좋습니다. 모든 컴포넌트, 모든 함수, 모든 값에 다 적용한다고 성능이 좋아지는 것은 아닙니다. 

 

예를 들어서, User 컴포넌트에 <b>와 <button> 태그의 onClick으로 설정해준 함수들은, 해당 함수들을 useCallback으로 재사용한다고 해서 리 렌더링을 막을 수 있는 것은 아니므로, 굳이 할 필요가 없습니다. 

 

추가적으로 렌더링 최적화를 하지 않는 컴포넌트에 React.memo를 사용하는 것은 불필요한 props 비교만 하는 것이기 때문에 실제로 렌더링을 방지할 수 있는 상황이 있는 경우에만 사용하는 것이 좋습니다. 

 

참고로 React.memo에서 두 번째 파라미터에 propsAreEqual이라는 함수를 사용하여 특정 값들만 비교하는 것도 가능합니다. 

export default React.memo(
  UserList,
  (prevProps, nextProps) => prevProps.users === nextProps.users
);

하지만 이걸 잘못 사용한다면 오히려 의도치 않은 버그들이 발생하기 쉽습니다. 예를 들어, 함수형 업데이트로 전환을 안 했는데 이런 식으로 users만 비교를 하게 된다면, onToggle과 onRemove에서 최신 users 배열을 참조하지 않으므로 심각한 오류를 초래할 수 있습니다. 

 

 

 

 

강의 링크

 

프론트엔드 개발 올인원 패키지 with React Online. | 패스트캠퍼스 (fastcampus.co.kr)

 

프론트엔드 개발 올인원 패키지 with React Online. | 패스트캠퍼스

프론트엔드 개발 러닝패스, 이 강의 패키지 하나로 끝낼 수 있습니다. 총 90시간 분량의 평생 소장 온라인 강의로 프론트엔드 개발자가 되세요.

www.fastcampus.co.kr

 

반응형