💡 문제

→ 계속 api 요청을 계속 보내는 문제가 있다.

→ 자바스크립트에서 함수도 객체로 취급이 되기 때문에 메모리 주소에 의한 참조 비교가 일어나기 때문에 컴포넌트가 랜더링될 때 마다 새로운 참조값으로 변경이 된다.

→ 그러면 useEffect() 함수가 호출되어 state 상태값이 바뀌고 그러면 다시 랜더링이 되고 그럼 또 다시 useEffect() 함수가 호출되는 악순환이 반복되는 것이 문제이다.

 

🗡 해결

→ useCallback을 사용해서 useCallback() hook 함수를 이용하면 컴포넌트가 다시 랜더링되더라도 fetchUser함수의 참조값을 동일하게 유지시켜서 한번만 호출하게 하자.

 

 

useCallback이 없으면 계속 api 요청을 보내게 된다.

한번만 api 요청을 보내면 되는데, 무한으로 보낸다.

 

useCallback()을 사용하면 한번만 api 요청을 보내게 된다.

useCallback()이 뭔가?

import axios from "axios";
import { useCallback } from "react";
import { atom, useRecoilState } from "recoil";

const currentUserData = atom({
    key: "currentUserData",
    default: null,
});

export default function useUserData() {
    const [userData, setUserData] = useRecoilState(currentUserData);

    const reFetch = useCallback(() => {
        axios
            .get(`/api/user`)
            .then(({ data }) => {
                setUserData(data.data);
                console.log("useUserData의 data", data);
            })
            .catch((error) => {
                setUserData(null);
                console.log(
                    "useUserData의 로그인 없어서 회원 데이터 가져오지 못함",
                    error
                );
            });
    }, []);

    return [userData, reFetch];
}

 

 

useCallback()
함수를 메모이제이션(memoization)하기 위해서 사용되는 hook 함수

첫번째 인자로 넘어온 함수를, 두번째 인자로 넘어온 배열 내의 값이 변경될 때까지 저장해놓고 재사용할 수 있게 해준다.

const memoizedCallback = useCallback(함수, 배열);
useCallback은 ( function() {
    인라인 콜백함수와
}, [의존성 값의 배열] ) 을 받는다.

의존성 배열인 deps에 변경을 감지해야할 값 [item] 을 넣어주게 되면 얘가 변경될 때마다 콜백 함수를 새로 생성한다.

useCallback(function(){
   함수내용 어쩌고저쩌고 
}, [item])

예를 들어, 어떤 React 컴포넌트 함수 안에 함수가 선언이 되어 있다면 이 함수는 해당 컴포넌트가 랜더링될 때 마다 새로운 함수가 생성되게된다.

const add = () => x + y;

하지만 useCallback()을 사용하면, 해당 컴포넌트가 랜더링되더라도 그 함수가 의존하는 값들이 바뀌지 않는 한 기존 함수를 계속해서 반환한다.

즉, x또는 y값이 바뀌면 새로운 함수가 생성되어 add변수에 할당되고, xy값이 동일하다면 다음 랜더링 때 이 함수를 재사용한다.

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

 

React가 리렌더링을 하는 조건

  • props가 변경되었을 때
  • state가 변경되었을 때
  • 부모 컴포넌트가 렌더링되었을 때
  • forceUpdate() 를 실행하였을 때

 

자바스크립트 함수 동등성

useCallback() hook 함수를 언제 사용해야하는지 제대로 이해하려면 먼저 자바스크립트에서 함수 간의 동등함이 어떻게 결정되는지 알 필요가 있다

브라우저 콘솔을 열고 다음과 같이 동일한 코드의 자바스크립트 함수가 동일한지 === 연산자를 통해 비교를 해보면 false가 반환될 것이다.

> const add1 = () => x + y;
undefined
> const add2 = () => x + y;
undefined
> add1 === add2
false

자바스크립트에서 함수도 객체로 취급이 되기 때문에 메모리 주소에 의한 참조 비교가 일어나기 때문이다.

이러한 자바스크립트 특성은 React 컴포넌트 함수 내에서 어떤 함수를 다른 함수의 인자로 넘기거나 자식 컴포넌트의 prop으로 넘길 때 예상치 못한 성능 문제로 이어질 수 있다.

 

의존 배열로 함수를 넘길 때

많은 React hook 함수들이 불필요한 작업을 줄이기 위해서 두 번째 인자로, 첫 번째 함수가 의존해야하는 배열을 받습니다.

예를 들어, useEffect() 함수는 두 번째 인자로 넘어온 의존 배열이 변경될 때만 첫 번째 인자로 넘어온 함수를 호출합니다.

예를 들어, 다음과 컴포넌트에서 API를 호출하는 코드는 fetchUser 함수가 변경될 때만 호출됩니다. 여기서 위에서 설명드린 자바스크립트가 함수의 동등성을 판단하는 방식 때문에 예상치 못한 무한 루프에 발생하게 됩니다. fetchUser는 함수이기 때문에, userId 값이 바뀌든 말든 컴포넌트가 랜더링될 때 마다 새로운 참조값으로 변경이 됩니다. 그러면 useEffect() 함수가 호출되어 user 상태값이 바뀌고 그러면 다시 랜더링이 되고 그럼 또 다시 useEffect() 함수가 호출되는 악순환이 반복됩니다.

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

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

  // ...
}

이와 같은 상황에서 useCallback()
hook 함수를 이용하면 컴포넌트가 다시 랜더링되더라도 fetchUser
함수의 참조값을 동일하게 유지시킬 수 있습니다. 따라서 의도했던 대로, useEffect()
에 넘어온 함수는 userId값이 변경되지 않는 한 재호출 되지 않게 됩니다.

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

function 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 Hooks: useCallback 사용법

 

React Hooks: useCallback 사용법

Engineering Blog by Dale Seo

www.daleseo.com

 

 

 

[React] useCallback을 사용해서 한번만 api 요청하게 하자