본문 바로가기

React.js

React(리액트): 최적화 함수들 useMemo, useCallback, React.memo

반응형

useMemo

이번에는 'useMemo'라는 Hook을 사용해서 이전에 연산된 값을 재사용하는 방법에 대해서 알아보자.

이 Hook 함수는 주로 성능을 최적화해야 하는 상황에서 사용한다.

 

이전 글에서 만든 컴포넌트들에 이어서 만들어보도록 하겠다. App.js 컴포넌트의 users 배열에서 active 값이 true인 

객체의 수 ('계정명'의 글자가 초록색)를 표시하는 함수를 만드는 것을 예제로 하겠다.

 

App.js

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

App.js의 맨 윗부분에 작성해준 코드이다. 이 함수는 users배열을 파라미터로 가져오고 

filter 함수를 사용해서 users배열에서 active값이 true인 것의 length를 리턴하도록 했다. 

 

const count = countActiveUsers(users);

  return (
  <>
    <CreateUser 
      username={username} 
      email={email} 
      onChange={onChange} 
      onCreate={onCreate}
    />
    <UserList users={users} onRemove={onRemove} onToggle={onToggle}/>
    <div>활성 사용자 수: {count}</div>
  </>
  );

1번 줄과 같이 함수를 사용해주고 div를 만들어서 나타나도록 했다. 

활성 사용자 수가 잘 표시된다.

 

여기서 발생하는 문제는 input칸에 타이핑을 할 때마다 countActiveUsers 함수가 호출된다는 것이다.

input에 입력을 하면 onChange함수가 호출되어 계속 리 렌더링 되기 때문에 활성 사용자 수를 계속 세게 되는 것이다.

이 문제를 해결하기 위해 사용할 수 있는 것이 바로 useMemo라는 Hook이다. useMemo는 특정 값이 바뀌었을 때만 

특정 함수를 실행해서 연산을 하도록 처리하고 원하는 값이 바뀌지 않았다면 리 렌더링 할 때 이전에 만들었던 값을

재사용할 수 있게 해 준다. 

 

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

위에서 선언한 count를 useMemo를 사용해서 다음과 같이 바꿔주면 된다. 이렇게 해주면 최적화가 끝난 것이 된다.

첫 번째 파라미터에는 함수를 넣어주어야 해서 다음과 같이 해주었고, 두 번째 파라미터에는 useEffect와 같은

'deps  배열'이 들어간다. 첫 번째 파라미터에 넣어준 함수는 users가 바뀔 때에만 호출된다. 바뀌지 않았다면 이전에 

사용한 값을 재사용하게 된다. 

 

다음과 같이 input을 바꾼다 해도 활성 사용자 수를 세는 함수가 호출되지 않는 것을 확인할 수 있다.

이렇게 useMemo를 사용하면 우리가 필요한 연산을 정말 필요할 때만 할 수 있다는 것을 잘 알아두자.

나중에 컴포넌트 성능을 최적화해야 할 때 이 useMemo를 잘 사용하면 되겠다.

 

useCallback

이번에는 useCallback이라는 Hook함수를 사용해서 이전에 만들었던 함수를 새로 만들지 않고 재사용하는 방법에 대해서 알아보자.

useCallback은 useMemo랑 비슷한데 useCallback은 함수를 위한 Hook이다.

 

이전에 만들었던 onCreate, onRemove, onToggle 함수들은 컴포넌트가 리 렌더링 될 때마다 새로운 함수를 만들고있다.

이렇게 함수를 새로 만드는 것 자체는 메모리도 CPU에서도 리소스를 많이 차지하는 작업은 아니기 때문에 함수를 

새로 선언한다고 해서 그 자체만으로 부하가 걸리지는 않지만, 한 번 만든 함수를 재사용할 수 있다면 재사용하는 것이 좋다. useCallback을 사용하면 UserList, User와 같은 컴포넌트들의 props가 바뀌지 않으면 Virtual DOM이 하는 리 렌더링 조차 하지 않게끔 만들어 줄 수 있다. props가 바뀌지 않을 경우 이전에 사용했던 값들을 재사용할 수 있게 해주는 것이다. 참고로 useCallback도 useMemo와 마찬가지로 두 번째 파라미터에 deps를 넣어주어야 한다.

 

App.js

const onChange = useCallback(e => {
    const {name, value} = e.target;
    setInputs({
      ...inputs,
      [name]: value
    })
  },[inputs])
  
  const onCreate = useCallback(() =>{
    const user = {
      id: nextId.current,
      username,
      email,
    }
    setUsers([...users, user])
  setInputs({
    username: '',
    email: ''
  })
  nextId.current += 1;
},[username, email, users])

다음과 같이 이전에 선언했던 함수들을 useCallback으로 감싸주고 deps배열에는 참조하는 값들을 넣어주었다.

이렇게 하면 onChange함수는 deps에 넣어준 inputs가 바뀔 때에만 함수가 새로 만들어지고 그렇지 않다면 기존의 

함수를 재사용하게 된다.

 

이렇게 useCallback 함수를 사용한다고 해서 눈에 띄는 최적화 효과는 아직 없다.

나중에 컴포넌트 리 렌더링 성능 최적화 작업을 해줘야만 성능이 좋아진다.

 

React.memo

이번에는 React.memo라는 함수를 사용해서 컴포넌트에서 리 렌더링이 불필요할 때는 이전에 렌더링 했던 결과를 

재사용할 수 있다. React.memo 함수는 컴포넌트의 리 렌더링 성능을 최적화해준다.

 

export default React.memo(CreateUser);

사용법은 아주 간단하다. 컴포넌트를 내보내는 부분을 다음처럼 React.memo로 감싸주면 된다. 최적화가 끝난 것이다.

React.memo를 사용하면 props가 바뀌었을 때만 리 렌더링 해준다. 

 

const User = React.memo(function User({user, onRemove, onToggle}){

UserList 컴포넌트에 속해있는 User 컴포넌트의 경우는 다음과 같이 감싸주면 된다.

 

이게 끝은 아니고 더 해야 할게 남아있다.

 

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])

여기서도 문제가 하나 발생한다. App 컴포넌트에 onRemove와 onToggle 함수를 살펴보면 deps배열에 users를 넣어줘서 users배열이 바뀔 때마다 onRemove와 onToggle도 새로 바뀐다. 결과적으로 onRemove와 onToggle이 바뀌기 때문에 UserList와 User 컴포넌트 모두 리 렌더링을 해야 한다. 이 문제를 해결하려면 이 함수들에서 기존 users를 참조하면 

안된다. user를 참조하지 않고 useState의 함수형 업데이트를 해주면 해결된다. 함수형 업데이트를 해주면 deps 부분에

users를 넣지 않아도 된다. 

 

const onCreate = useCallback(() =>{
    const user = {
      id: nextId.current,
      username,
      email,
    }
    setUsers([...users, user])
  setInputs({
    username: '',
    email: ''
  })
  nextId.current += 1;
},[username, email, users])

원래는 이랬던 onCreate함수를

const onCreate = useCallback(() =>{
    const user = {
      id: nextId.current,
      username,
      email,
    }
    setUsers(users => [...users, user])
  setInputs({
    username: '',
    email: ''
  })
  nextId.current += 1;
},[username, email])

다음과 같이 바꿔주면 된다. 

이렇게 하면 onCreate함수는 username과 email이 바뀔 때에만 리 렌더링 된다.

 

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

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

onRemove와 onToggle 함수도 다음과 같이 바꾸어주면 최적화가 완료된다.

 

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

추가적으로 다음과 같이 React.memo 함수를 사용할 때 두 번째 파라미터에 props are equal 함수를 넣어줄 수 있다.

prevProps와 nextProps가 같은지 확인을 해서 같다면 리 렌더링을 막고 다르면 리 렌더링을 하는 식의 함수이다.

이 함수는 잘못 사용할 경우 치명적 오류가 될 수 있기 때문에 주의해서  사용하도록 하자.

 

정리 

우리는 지금까지 컴포넌트들을 최적화하는 방법들을 알아보았다. 

먼저, 연산된 값을 재사용하기 위해서는 useMemo를 사용하고 특정 함수를 재사용하기 위해서는 useCallback을 

사용한다. 그리고 컴포넌트가 렌더링 된  결과를 재사용하기 위해서는 React.memo를 사용한다는 것 잘 알아두자.

 

또, useCallback을 사용한다고 해서 반드시 성능이 좋아지는 것은 아니다. useMemo도 마찬가지이다. 오히려 성능은

차이가 없는데 코드가 길어지는 결과를 낳을 수도 있다. 그렇기 때문에 useMemo와 useCallback의 경우는 사용해서 

정말 컴포넌트를 최적화할 수 있을 때만 사용을 하도록 하자. React.memo의 경우도 모든 컴포넌트를 최적화하는 것이 아니라 최적화가 필요하겠다 라는 생각이 들 때 구현을 하면 된다.

반응형