React useMemo, useCallback의 사용 이유와 사용법

개발자 베니·2022년 1월 20일
6
post-thumbnail

썸네일은 멋져서 가져왔따.

개요

이번에 정리해볼 글은 리액트 Hooks의 API들을 다뤄볼 것이다. 그 중에서도 성능 최적화에 관련된 두 가지를 작성해보고, 커스텀 훅에 대해서도 한 번 작성해보려 한다. 글에 대부분은 무엇을 어떻게 하는지를 알아보기보다는 먼저 왜 이 API들을 사용하는지를 알아본 후에 더 깊게 알아보는 순서로 이루어 질 것이다. 자신이 React에 대해서 많이 사용해봤다면 쉽게 읽을 수 있는 정도의 난이도이니 편하게 한번 읽어보자.

useMemo

useMemo는 리액트에 대해 공부하다보면 한 번쯤은 봤을 법한 API다. 공식 문서를 참고해서 알아보면 이 API는 "메모이제이션된 값을 반환한다." 라고 나와있다. 그렇다면 메모이제이션은 무엇일까? 메모이제이션은 이 전에 사용한 값을 그대로 재사용한다는 의미다. 전혀 어려운 말이 아니다.

잠깐 예시를 들어보자면 나는 내 사이트에 회원가입한 유저의 숫자를 어떠한 위치에 놓으려고 한다. 그렇다면 정확한 유저의 인원을 구해야하기 때문에 A라는 함수를 이용해서 유저의 숫자라는 값을 반환받았다.

그런데 유저에 관련되어 있지 않은 다른 상태를 관리하는 함수가 실행됬을때, A라는 함수가 또 작동하는 것을 발견했다. 나는 A라는 함수는 유저의 숫자가 늘어나거나 줄어들지 않는 이상 실행되지 않는 것이 이상적이라는 생각을 했다.

그 이유는 A라는 함수는 꽤 고성능의 함수이기에 값을 반환하는데 많은 자원을 소모하기 때문에, 사이트 성능에 있어서 좋지 않을 것이라는 생각을 했기 때문이다. 나는 이 문제를 해결하기 위해서 리액트 공식 문서에서 useMemo라는 API를 참고하게 됐다.

예시에서 발견한 문제는 유저의 숫자가 늘어나거나 줄어들지 않았음에도 불구하고, 다른 상태 관리 API에 의해서 페이지가 리렌더링 되면서 의도치 않게 A라는 고성능 함수가 한 번 더 실행된 것이다. 여기서 우리는 useMemo를 사용하는 이유를 짐작할 수 있다.

useMemo의 주 목적은 바로 성능 최적화다. A라는 함수는 고성능 고비용의 함수다. 그렇다면 당연하게도 여러번 호출하지 않는 것이 성능에 있어서 좋다.

이런 기능을 해줄 수 있는 것이 useMemo다. 사진의 예시를 보면서 어떻게 사용하는 지 간단하게 확인해보자.

useMemo의 주 목적은 바로 성능 최적화다.

사용법


우선 useMemo는 두 가지 파라미터를 일반적으로 받는다. 첫 번째는 연산을 해주는 함수를 넣어준다. 위의 예시를 참고해보면 A를 computeExpensiveValue라고 생각해보면 되겠다. 두 번째는 의존성 값의 배열이다.

말이 살짝 어려운데 쉽게 풀어보면 이 배열안에 들어간 값이 변하지 않을 경우에 이전에 사용한 값을 그대로 사용한다는 의미이다. 사진에서는 a,b라는 값에 의존한다는 의미라고 보면 되겠다.

위의 예시를 예를들어서 만들어보면 유저의 총 인원의 수를 의미하는 user라는 상태 변수를 넣어주면 될 것이다.

사용법은 이게 끝이다. 주의해야할 것은 의존성 배열에 적합한 값을 넣어주지 않는다면 렌더링마다 재 실행되게 될 수 있으니 배열안에 값을 잘 넣어주자. 또는 eslint-plugin-react-hooks 를 사용한다면 의존성 배열이 잘못되어 있다고 친절하게 알려주니 한 번 사용해보는 것도 좋을것이다.

공식 문서의 참고 사항

공식 문서를 참고해보면 useMemo의 설명 중에 굵은 글씨로 "useMemo는 성능 최적화를 위해서 사용될 수 있지만 의미상으로 보장이 있다고 생각하지는 마라." 라는 말이 있다.

useMemo는 분명히 성능 최적화를 해주고 우리의 웹 앱을 좀 더 빠르게 만들어줄 수 있다. 하지만 항상 그렇다고는 볼 수 없다는 뜻이다. 무조건 좋다라는 생각에 useMemo를 많이 사용하는 것이 무조건 성능이 좋아진다고 할 수 없다는 의미다.

공식 문서는 우선 useMemo를 사용하지 말고 동작할 수 있는 코드를 만들어보라고 권한다. 일단 만들어 보고 후에 리팩토링을 할 때 어느 부분이 고비용이 들어가고 동작하지 않아도 되는 부분인지를 확인 한 다음에 useMemo를 이용해서 관리를 해보는 것이 좋다.

물론 만들면서 이 부분은 무조건 메모이제이션을 사용해야 한다라는 판단을 할 수 있으면 좋겠지만, 사실 우리가 코드를 작성할 때 혼자서 모든 부분을 하지는 않는다. 처음에는 클린하게 잘 작성했지만 추후에 기능 추가를 해야하는 부분이 생겨서 이것 저것 바꾸다보면 이전에 사용했던 메모이제이션의 의존성 배열이 지저분해져서 잘 작동하지 않는다던지, 그런 일들이 발생할 수 있다.

항상 코드를 작성할 때 가능한 큰 그림을 보자. 경우에 따라서 메모이제이션을 사용 못 하는 경우가 생길수도 있는 법이다.

useCallback

useCallback을 한번 다뤄보겠다. 위의 개요를 읽어봤다면 이 API또한 성능 최적화를 위한 방법이라는 것을 알아낼 수 있다. useMemo와 차이점을 먼저 알아보자.

useMemo는 메모이제이션을 통해서 특정 값을 재사용하는 것이다. useCallback은 메모이제이션을 통해서 특정 함수를 재사용한다는 것이다. 둘의 차이점은 값을 재사용하는 것과 함수를 재사용하는 것의 차이다.

사실 함수를 선언하는 것은 리소스를 많이 차지하는 작업은 아니다. 그렇지만 리렌더링이 될 때마다 새로 만들어지는 것보다 이미 만든 함수를 필요할 때만 사용하는 것은 중요하다. 이런 것들 하나씩 아껴서 빠르고 강력한 UX가 탄생하는 것이다. 자원을 최소한으로 사용하는 것을 항상 목표로 두고 공부를 해보자. 개발자가 계속해서 공부하는 가장 큰 이유이기도 하다.

왜 사용하는지에 대해서는 이미 useMemo에서 충분히 다뤘기에 바로 사용법으로 가보겠다.

사용법

일단 간단하게 과거를 되돌아보자. 과거의 코드에 이미 우리는 이벤트 핸들러 함수를 여러번 만들어봤다. 보통 이름은 onRemove 혹은 handleRemove 이런 식으로 함수 이름을 지어줬던 것을 알고 있다. useCallback이 들어갈 부분이 이런식이다.

 const handleRemove = () => {
     // 유저를 전체 유저의 배열에서 지우는 코드.
     setUsers(users.filter(...));
     ...
 }

 const handleRemove = useCallback(() => {
     // 유저를 전체 유저의 배열에서 지우는 코드.
     setUsers(users.filter(...));
 }, [users])

코드 작성 예시에서 위의 부분은 우리가 평소에 작성하던 부분이다. 그리고 아래 부분이 useCallback API를 이용해 작성한 부분이다. 사실 뭐 크게 다르다고 볼 순 없다. 그렇지만 꼭 짚고 넘어가야할 부분은 의존성 배열이다.

handleRemove라는 함수 안에서 사용되는 props나 상태가 있다면, 꼭 의존성 배열 안에 넣어줘야 한다. 이 작업을 제대로 해주지 않으면 props나 상태가 가장 최신이라고 보장할 수 없는 상황이 발생한다. 이 부분만은 꼭 알고 가자.

일단 useCallback은 여기서 끝이다. 사실 근데 이렇게 바꿔줘도 크게 달라지는 것은 없다. 리렌더링이 되어도 똑같이 만들어지고 최적화가 이루어지지 않는다. 이제 이 부분을 개선하기 위해서 사용해야할 방법이 있다.

React.memo

아니 분명히 성능 최적화에서 두 가지만 다룬다고 했는데 왜 세 가지냐? 라고 물어볼 수 있지만, 어쩔 수 없다. useCallback 쓰려면 이놈도 필요하다. 그냥 쿨하게 봐라. 그리고 사용하는 방법도 그렇게 어렵지 않다. 코드로 한 번 보자.

import React from 'react';

const User = ({...}) => {
  return (
    <div>
      ...
    </div>
  );
};

export default React.memo(User);

일반적으로 우리가 Hook을 이용해서 컴포넌트를 이룬다면 이런 식으로 많이 볼텐데 React.memo를 사용하기 위해서는 그냥 맨 밑에 export 할때 React.memo(컴포넌트)를 넣어주면 된다.

useCallback으로 기존 핸들러를 바꿔주고 props로 영향을 받는 컴포넌트들에 저걸 다 추가해주면 된다. 정말 간단하지 않은가? 이게 끝이다. 이렇게 해주면 컴포넌트에서 리렌더링이 필요한 상황에서만 리렌더링을 해준다.

근데 리렌더링이 필요한 상황에서만 사용을 할 수 있다는 말은 곧 리렌더링을 방지해준다고 해석하는 사람도 있을 것 같다.

그렇게 사용하지마라.
내가 한 말 아님, 공식 문서가 그랬음

이건 내 의견이 아니라 공식 문서가 그랬다. 자세하게는 안 나와있지만 그런 식으로 사용하면 버그가 발생할 수 있다로 정리되어 있다. React.memo는 오직 성능 최적화를 위해서만 사용이 된다. 만약 렌더링을 방지하기 위한 목적이라면 다른 방법을 한 번 찾아보자.

정리

원래 커스텀훅이랑 다른 것들도 조금씩 적어보려 했는데 생각보다 길게 나와서 여기까지만 적겠다. 커스텀훅은 따로 정리를해서 올리겠다. 끝내기 전에 마지막으로 정리해보자면 useMemo와 useCallback은 성능 최적화를 위한 API다.

그 중에 중요한 개념인 메모이제이션은 이전에 사용한 값이 렌더링 후에도 변화하지 않았다면 굳이 다시 만들지 않고 이 전에 사용한 값을 사용하겠다는 의미다.

useMemo는 메모이제이션된 값을 반환하고 useCallback은 메모이제이션된 콜백 함수를 반환한다. 여기서 useCallback은 선언만 해준다고 바로 적용이 되는 것이 아니고, React.memo를 같이 사용해야 비로소 성능 최적화를 이룰 수 있다.

주의 사항은 useMemo는 성능 최적화를 이룰 수 있는 무기지만, 그렇다고 바로 사용하지는 말자. 먼저 성능 최적화에 관련된 부분들을 생각하지 말고 만들고 동작이 되는 것을 확인한 후에 성능 최적화 API들을 이용해보자. 바로 사용해서 코드를 작성하는 것 보다는 느리겠지만, 정석이 왜 정석이겠는가? 빠르게 가려다가 더 많이 돌아가버릴지도 모를 일이니 신중하게 코드를 작성하자.

useCallback은 선언한다고 바로 적용되지 않는다. 늘 React.memo를 사용하는 것을 잊지말자. 그리고 의존성 배열을 충분히 관리 해줘야한다. 대충하고 넘어가면 시간만 낭비한 걸지도 모른다. 그리고 React.memo를 렌더링 방지의 목적으로 사용하지 말자. 버그날 수 있다. 여기까지 끝.

0개의 댓글