useCallback()

리액트 성능 최적화와 관련된 내용을 살펴볼때마다 useMemo()와 달리 제대로 이해되지 않는 부분이 많았습니다. 이와 관련하여 알게된 내용을 정리합니다.

일반적으로 useMemo()와 useCallback()은 다음과 같은 방식으로 이해됩니다.

  • useMemo() - 복잡한 함수의 결과값을 기억
  • useCallback() - 함수를 기억 (두 번째 인자값이 변하지 않으면 함수를 새로 생성하지 않음)

컴포넌트 내에 다음과 같은 함수가 선언되어 있으면 컴포넌트가 랜더링 될 때 마다 새로운 함수가 생성됩니다.

const add = () => x+y;

하지만 useCallback()을 사용하면 해당 컴포넌트가 랜더링 되더라도 x, y값이 변하지 않으면 동일한 함수를 재사용하도록 할 수 있습니다.

const add = useCallback(() => x+y, [x,y]);

위와 같은 방식으로 useCallback은 두 번째 인자의 값을 기준으로 함수를 새로 생성하지 않도록 도와줍니다.

그런데 여기서 의문점이 생깁니다.

  • 함수를 새로 생성하는 것을 막는 것이 성능상 유의미한가?
  • 결과값을 useMemo()로 기억해놓으면 useCallback()은 필요 없는 것이 아닌가?

직관력이 뛰어난 개발자들은 위의 의문이 들지 않을 수 있지만, 저는 위의 의문점들이 계속해서 머리에 맴돌며 점점 더 이해가 되지 않았습니다.

최적화 방법

결론부터 말해보겠습니다.

  • 함수를 새로 생성하는 것을 막는 것이 성능상 유의미한가? -> 유의미하지 않다.
  • 결과값을 useMemo()로 기억해놓으면 useCallback()은 필요 없는 것이 아닌가? -> 필요하다.

위와 같은 의문점은 useCallback()이 "함수의 재사용"으로 최적화를 한다는 착각에서 나옵니다.

useCallback()의 최적화 핵심은 "리랜더링"이 일어나지 않도록 하는 것입니다.

코드를 통해 살펴봅시다.

function User({ userId, login, toggle }) {
  console.log({ userId, login });
  return (
  	<button onClick={toggle}>
      {userId} {login ? "login" : "logout"}
    </button>
  )
}

이제 React.memo를 이용해 props가 동일하다면 다시 호출되지 않도록 하여 성능 최적화를 진행합니다.

User = React.memo(User);

다음으로 친구들의 상태를 관리하는 FriendsState컴포넌트를 작성합니다.

function FriendsState() {
 const [friend1, setFriend1] = useState(false);
 const [friend2, setFriend2] = useState(false);
 const [friend3, setFriend3] = useState(false);
  
 const toggleFriend1 = () => setFriend1(!friend1);
 const toggleFriend2 = () => setFriend1(!friend2);
 const toggleFriend3 = () => setFriend1(!friend3);
  
 return (
   <>
   	<User userId="friend1" login={friend1} toggle={toggleFriend1} />
    <User userId="friend2" login={friend2} toggle={toggleFriend2} />
    <User userId="friend3" login={friend3} toggle={toggleFriend3} />
   </>
 );
}

이제 친구 목록의 아무나 로그인 상태를 바꿔봅시다. 상태가 변한 컴포넌트만 호출하고 싶지만 모든 컴포넌트가 전부 호출되는 것을 확인할 수 있습니다.

{userId: "friend1", login: true}
{userId: "friend2", login: false}
{userId: "friend3", login: false}

이런 일이 벌어지는 이유는 FriendsStatus컴포넌트가 랜더링 될 때 마다 toggleFriend{n}에 해당하는 함수의 참조값이 모두 바뀌어 버리기 때문입니다.

즉, User컴포넌트 입장에선 props의 toggle이 변화하니 계속 새로 호출이 되는 것이죠.

문제 해결을 위해 useCallback()을 사용해봅시다.

function FriendsState() {
 const [friend1, setFriend1] = useState(false);
 const [friend2, setFriend2] = useState(false);
 const [friend3, setFriend3] = useState(false);
  
 const toggleFriend1 = useCallback(() => setFriend1(!friend1), [friend1]);
 const toggleFriend2 = useCallback(() => setFriend2(!friend2), [friend2]);
 const toggleFriend3 = useCallback(() => setFriend3(!friend3), [friend3]);
  
 return (
   <>
   	<User userId="friend1" login={friend1} toggle={toggleFriend1} />
    <User userId="friend2" login={friend2} toggle={toggleFriend2} />
    <User userId="friend3" login={friend3} toggle={toggleFriend3} />
   </>
 );
}

이제 친구의 상태를 변경하면 해당하는 User 컴포넌트만 호출되는 것을 확인할 수 있습니다.

{userId: "friend1", login: true}

이처럼 useCallback()함수는 컴포넌트의 랜더링을 최적화 할 때 사용됩니다. 그러나 위에서 살펴본 것 처럼 useCallback()을 잘못 사용하면 오히려 최적화에 방해가 될 수 있습니다.

최적화의 초점이 "함수의 재사용"이 아닌 "그로 인한 랜더링 최적화"에 있다는 사실을 기억해야 합니다.

출처

profile
웹 개발을 공부하고 있는 윤석주입니다.

2개의 댓글

comment-user-thumbnail
2022년 6월 9일

혹시 async / await에 대한 글은 없을까요?

1개의 답글