[React] useCallback

설명

useCallback 은 특정 함수를 새로 만들지 않고 재사용하고 싶을때 사용

useMemo의 함수 버전이라고 생각하면 된다.

함수를 선언하는 것 자체는 사실 메모리도, CPU 도 리소스를 많이 차지 하는 작업은 아니기 때문에 함수를 새로 선언한다고 해서 그 자체 만으로 큰 부하가 생길일은 없다.

자바스크립트에서 함수도 객체로 취급이 되기 때문에 메모리 주소에 의한 참조 비교가 일어난다. 이러한 자바스크립트 특성은 React 컴포넌트 함수 내에서 어떤 함수를 다른 함수의 인자로 넘기거나 자식 컴포넌트의 prop으로 넘길 때 예상치 못한 성능 문제로 이어질 수 있다.

아래와 같은 상황은 크게 있을리 없어보이지만 알아는 두자.

import React, { useState, useEffect } from "react";

const Profile = ({ userId }) => {
  const [user, setUser] = useState(null);

  const fetchUser = () => {
    fetch(`https://your-api.com/users/${userId}`)
      .then((response) => response.json())
      .then(({ user }) => user);
  }

  useEffect(() => {
    fetchUser().then((user) => setUser(user));
  }, [fetchUser]);

  // ...
}

랜더링 될 때 마다 메모리 참조값이 변경되기 때문에 위와 같은 경우 무한루프에 빠진다. 아래와 같은 변경이 필요

import React, { useState, useEffect, useCallback } from "react";

const Profile = ({ userId }) => {
  const [user, setUser] = useState(null);

  const fetchUser = useCallback(
    () => {
      fetch(`https://your-api.com/users/${userId}`)
        .then((response) => response.json())
        .then(({ user }) => user)
    },[userId]
  );

  useEffect(() => {
    fetchUser().then((user) => setUser(user));
  }, [fetchUser]);

  // ...
}

사용

React.memo()와 함께 사용하여 자식 컴포넌트의 불필요한 랜더링을 줄일 수 있다.

예를 들어 방이름(room), 조명 켜짐 여부(on), 조명 제어 함수(toggle)를 prop으로 Light 컴포넌트를 작성한다.

React.memo() 함수로 감싸주면 해당 컴포넌트 함수는 props 값이 변경되지 않는 한 다시 호출되지 않는다.

import React from "react";

const Light = ({ room, on, toggle }) => {
  console.log({ room, on });
  return (
    <button onClick={toggle}>
      {room} {on ? "💡" : "⬛"}
    </button>
  );
}

export default React.memo(Light);

다음으로 3개의 방의 스위치를 중앙 제어해주는 SmartHome 컴포넌트를 작성.

import React, { useState, useCallback } from "react";

const SmartHome = () => {
  const [masterOn, setMasterOn] = useState(false);
  const [kitchenOn, setKitchenOn] = useState(false);
  const [bathOn, setBathOn] = useState(false);

  const toggleMaster = () => setMasterOn(!masterOn);
  const toggleKitchen = () => setKitchenOn(!kitchenOn);
  const toggleBath = () => setBathOn(!bathOn);

  return (
    <>
      <Light room="침실" on={masterOn} toggle={toggleMaster} />
      <Light room="주방" on={kitchenOn} toggle={toggleKitchen} />
      <Light room="욕실" on={bathOn} toggle={toggleBath} />
    </>
  );
}

export default SmartHome;

자식 컴포넌트인 Light에 React.memo를 사용했기에 스위치를 토글하면 부모 컴포넌트와 각각의 자식 컴포넌트만 랜더링이 될 것 같지만 모든 자식 컴포넌트에서 랜더링이 일어난다.

그 이유는 조명을 제어할 때 쓰이는 toggleMaster(), toggleKitchen(), toggleBath() 함수의 참조값이 SmartHome 컴포넌트가 랜더링될 때마다 모두 바뀌어버리기 때문

이 문제를 해결하려면 모든 조명 제어 함수를 useCallback() hook 함수로 감싸고 두 번째 인자로 각 함수가 의존하고 있는 상태를 배열로 넘겨야한다.

아래와 같이 변경하면 해당 자식 컴포넌트만 랜더링 된다.

import React, { useState, useCallback } from "react";

const SmartHome = () => {
  const [masterOn, setMasterOn] = useState(false);
  const [kitchenOn, setKitchenOn] = useState(false);
  const [bathOn, setBathOn] = useState(false);

  const toggleMaster = useCallback(() => {
    setMasterOn(!masterOn)
  }, [masterOn]);

  const toggleKitchen = useCallback(() => {
    setKitchenOn(!kitchenOn)
  },[kitchenOn]);

  const toggleBath = useCallback(() => {
    setBathOn(!bathOn)
  }, [bathOn]);


  return (
    <>
      <Light room="침실" on={masterOn} toggle={toggleMaster} />
      <Light room="주방" on={kitchenOn} toggle={toggleKitchen} />
      <Light room="욕실" on={bathOn} toggle={toggleBath} />
    </>
  );
}

export default SmartHome;

links

social