useEffect 내부에 있던 async/await 함수 밖으로 빼기: useCallback으로 함수 성능 최적화하기

summereuna🐥·2024년 7월 3일
0

📝 React 정리

목록 보기
4/4

useEffect 내부에 정의된 async/await을 사용하여 데이터를 fecth하는 getData 함수를 다른 곳에서도 재사용할 수 있게 하려면 함수를 컴포넌트 외부로 추출해야 한다.

함수를 메모이제이션하는 useCallback() 훅 사용


일반적으로 함수를 컴포넌트 외부로 추출하려면 useCallback을 사용하여 함수를 정의고 재사용할 수 있다.

  • useCallback은 React에서 제공하는 훅으로, 함수를 메모이제이션하여 같은 함수 인스턴스를 재사용할 수 있게 해준다. 이는 성능 최적화 및 일관된 동작을 보장하는 데 유용하다.
import React, { useEffect, useState, useCallback } from "react";
import "./App.css";

const DataList = ({ data }) => (
  <div>
    {data.map((user) => (
      <ul key={user.id} style={{ border: "1px solid black" }}>
        <li>{user.userId}</li>
        <li>{user.title}</li>
        <li>{user.body}</li>
      </ul>
    ))}
  </div>
);

function App() {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  // 🔥 useCallback을 사용하여 getData 함수 정의
  const getData = useCallback(async () => {
    try {
      const response = await fetch(
        "https://jsonplaceholder.typicode.com/posts"
      );
      if (!response.ok) {
        throw new Error("네트워크 응답이 실패했습니다.");
      }
      const data = await response.json();
      setData(data);
      setLoading(false);
    } catch (error) {
      setError(error.message);
      setLoading(false);
    }
  }, []);

  useEffect(() => {
    getData(); // 🔥 getData 함수를 useEffect 내에서 호출
  }, [getData]); // 🔥 useEffect의 의존성 배열에 getData 추가

  if (loading) {
    return <p>Loading...</p>;
  }

  if (error) {
    return <p>에러 발생: {error}</p>;
  }

  return (
    <>
      <div>비동기 연습</div>
      <DataList data={data} />
    </>
  );
}

export default App;

1. useCallback 사용

useCallback을 사용하여 fetch를 통해 데이터를 가져오는 비동기 함수인 getData 함수를 정의한다.

2. useEffect 내에서 getData 함수 호출

getData 함수를 useEffect 내에서 호출하여 컴포넌트가 마운트될 때 데이터를 가져오게 한다.
이 때 useCallback으로 메모이제이션된 함수를 사용하므로, 함수의 인스턴스가 재사용된다.

3. useEffect의 의존성 배열에 getData 함수를 추가

getData 함수가 업데이트될 때마다 새로운 데이터를 가져오기 위해, useEffect의 의존성 배열에 getData 함수를 추가하여 getData 함수가 변경될 때마다 useEffect가 실행되도록 한다.

  • 함수를 메모이제이션 하여 사용하면, 함수가 변경되지 않는 한 컴포넌트 재렌더링 시에도 동일한 함수 인스턴스가 사용된다는 이점이 있다.

현재 getData 함수 내부에서 외부의 상태나 프롭스를 의존하지 않고 있기 때문에 useCallback의 의존성 배열에는 아무것도 추가할 필요가 없다.

useCallback의 의존성 배열


useCallback의 의존성 배열은 해당 함수가 의존하는 외부 변수나 상태를 포함해야 한다.
의존성 배열에 포함된 값이 변경될 때만 함수가 새로 생성되고, 그렇지 않으면 이전에 메모이제이션된 함수를 재사용한다.

예를 들어, 다음과 같은 상황에서는 의존성 배열에 값을 추가해야 할 수도 있다.

  1. 외부 상태나 프롭스에 의존하는 경우
const getData = useCallback(() => {
  // 외부 상태나 프롭스 사용 예시
}, [externalState, props.someProp]);
  1. 콜백 함수가 의존하는 상태나 함수
const handleCallback = useCallback(() => {
  // 콜백 함수 내부에서 사용하는 상태나 함수
}, [dependentState, dependentFunction]);
  1. 컴포넌트 내부의 로컬 상태를 의존하는 경우
const getData = useCallback(() => {
  // 로컬 상태 사용 예시
}, [localState]);

위와 같은 상황이 없고, 함수가 완전히 독립적이라면 의존성 배열을 빈 배열로 두면 되는데, useCallback의 의존성 배열이 빈 배열일 경우 React는 컴포넌트가 처음 렌더링될 때 한 번만 해당 함수를 생성하고, 그 이후에는 동일한 함수를 재사용한다.

useCallback 사용 시 주의 사항


코드 최적화를 위한 useCallback 훅은 적절하게 사용하는 것이 중요하다. 너무 남발하면 오히려 성능 저하의 원인이 될 수 있다.

적절한 useCallback 사용 패턴

1. 의존성 관리

useCallback은 해당 함수가 의존하는 상태나 변수가 변경될 때만 새로운 함수를 생성하고, 그렇지 않으면 이전 함수를 재사용한다. 따라서 함수가 의존하지 않는 상수 함수나 완전히 독립적인 함수는 useCallback로 감싸지 않아도 된다.

렌더링 최적화

컴포넌트의 렌더링을 최적화하려면 주로 렌더링 성능에 영향을 미치는 부분에서 useCallback을 사용한다.
예를 들어, 💡 자식 컴포넌트에 콜백 함수로 넘기는 함수들이나 useEffect 내에서 사용되는 콜백 함수 등이다.

남발하지 않기

모든 함수에 대해 일괄적으로 useCallback을 사용하는 것은 필요하지 않다.
컴포넌트에서 사용하는 대부분의 함수는 렌더링에 직접적인 영향을 주지 않고, 매번 새로운 함수가 생성되더라도 성능에 큰 영향을 미치지 않는다.

가독성 유지

코드의 가독성을 유지하는 것도 중요하다.
useCallback을 남용하면 코드의 복잡성이 증가할 수 있으며, 오히려 코드를 이해하기 어려워질 수 있다.

결론

useCallback은 성능 최적화를 위해 중요한 도구이지만, 적절하게 사용하는 것이 중요하다.
주로 렌더링 성능에 직접적인 영향을 미치는 부분에서 사용하고, 함수의 독립성과 가독성을 고려하여 사용하는 것이 좋다.

예제: 버튼 컴포넌트


🤔 자식 컴포넌트에 콜백 함수로 넘기는 함수들?!
Button 컴포넌트를 만들어 두고 onClick에 할당할 함수를 props로 받는다고 한다면,
부모 컴포넌트에서 Button 컴포넌트로 함수를 내려 보낼 때 useCallback으로 감싸서 내려 보내야 할까?

Button 컴포넌트에서 onClick에 할당할 함수를 props로 받는 경우, 이 함수가 렌더링 성능에 영향을 미칠 수 있으므로 적절한 경우에는 useCallback을 사용하는 것이 좋다.

Button 컴포넌트

const Button = ({ onClick, children }) => {
  return <button onClick={onClick}>{children}</button>;
};

export default Button;

부모 컴포넌트에서 Button 컴포넌트로 콜백함수를 보낼 경우

import { useCallback } from "react";
import Button from "./components/Button";

const Card = () => {
  const onDeleteClick = useCallback(() => {
    // 버튼 클릭 시 수행할 작업
  }, []);

  return (
    <div>
      <Button onClick={onDeleteClick}>
        삭제
      </Button>
    </div>
  );
};

export default Card;

1. 함수 의존성

onDeleteClick 함수가 어떤 외부 상태나 프롭스에 의존하는지 고려해야 한다.
만약 onDeleteClick 함수가 의존하는 상태가 있다면, (예를 들면 id 값을 받는 다든가) 해당 상태를 의존성 배열에 추가하여 필요할 때만 함수가 새로 생성되도록 할 수 있다.

const onDeleteClick = useCallback((id) => {
  dispatch(deleteTodo(id));
}, [id]);

2. 렌더링 최적화

onDeleteClick 함수가 자주 변경되지 않고, 반복적으로 동일한 함수가 사용되어야 할 때 useCallback을 사용한다. 이는 컴포넌트의 불필요한 재렌더링을 방지하여 성능을 최적화하는 데 도움이 된다.

3. 가독성

코드의 가독성을 유지하기 위해 너무 남발하지 말자!
모든 콜백 함수에 useCallback을 적용할 필요는 없다. 주로 성능에 영향을 미치는 부분에서 사용하는 것이 좋다.

결론

Button 컴포넌트와 같이 자주 사용되는 하위 컴포넌트의 콜백 함수useCallback을 사용하여 성능 최적화를 고려하는 것이 좋다.

  • 함수가 자주 변경되지 않고
  • 반복적으로 사용될 때

메모이제이션을 통해 성능을 개선할 수 있다.

적용하기


Button 컴포넌트는 onClick 프롭으로 아래 함수를 받고 있다고 하자.

  • addTodo
  • deleteTodo
  • updateTodo

적용하기

  1. addTodo, deleteTodo, updateTodo 모두 useCallback으로 감싸는 것이 좋다.
    왜냐하면 이 함수 모두, 함수가 렌더링될 때마다 새로운 함수를 생성할 필요가 없기 때문이다.
    useCallback으로 함수를 감싸면 ✅ 매번 동일한 함수 인스턴스를 재사용하여 성능을 최적화할 수 있다.

  2. 이에 더해 deleteTodo와 updateTodo의 경우에는 id값을 매개변수로 받아야 하기 때문에 콜백함수로 보내야 한다. 따라서 useCallback으로 감싸주는 것이 성능을 최적화 하는데 좋다.

  3. 하지만!! 다시 한번 생각해보자!
    handleAddTodo 함수는 매우 간단하고 외부 상태나 프롭스에 의존하지 않는 단순한 콜백 함수이다.
    따라서 useCallback을 사용하지 않아도 괜찮다.
    React는 이러한 경우에 자동으로 함수를 재사용하므로 추가적인 메모이제이션 작업이 필요하지 않다.

이렇게 하면 컴포넌트가 리렌더링될 때마다 새로운 함수가 생성되지 않고, 기존 함수를 재사용할 수 있다.

import { useCallback } from "react";
import Button from "./components/Button";

const Card = () => {
  // addTodo는 굳이굳이!! useCallback으로 감싸지 않아도 된다.
  const handleAddTodo = () => {
    addTodo("새로운 할 일");
  };

  // deleteTodo와 updateTodo는 useCallback으로 감싸주는 것이 좋다.
  const handleDeleteTodo = useCallback((id) => {
    deleteTodo(id);
  }, [deleteTodo]);

  const handleUpdateTodo = useCallback((id, updatedText) => {
    updateTodo(id, updatedText);
  }, [updateTodo]);

  return (
    <div>
      <Button onClick={handleAddTodo}>
        추가
        <Button>
      <Button onClick={(id) => handleDeleteTodo(id)}>
        삭제
        <Button>
      <Button onClick={(id) => handleUpdateTodo(id, "업데이트된 todo")}>
        수정
        <Button>
    </div>
  );
};


export default Card;

결론

useCallback을 사용하기 위한 최적의 조건

1. 함수가 매번 새로운 인스턴스를 생성할 필요가 없을 때

즉, 함수가 렌더링될 때마다 새로운 함수를 생성할 필요가 없는 경우에 useCallback을 사용한다.
이는 함수의 메모이제이션을 통해 성능을 최적화할 수 있다.

2. 동일한 함수 인스턴스를 재사용하여 성능을 최적화할 수 있는 경우

함수가 자주 호출되지만, 함수 내부의 로직이나 의존성이 바뀌지 않는 한, 동일한 함수 인스턴스를 재사용하여 메모리 사용을 줄이고 성능을 개선할 수 있다.

useCallback의 사용 사례

일반적으로 다음과 같은 상황에서 useCallback을 사용하는 것이 적합하다.

1. 자식 컴포넌트로 넘기는 콜백 함수

부모 컴포넌트에서 자식 컴포넌트로 콜백 함수를 전달할 때, 자식 컴포넌트는 이 콜백 함수를 프롭스로 받아 사용합니다. 이때 useCallback을 사용하여 콜백 함수를 메모이제이션하면, 자식 컴포넌트가 리렌더링될 때마다 새로운 함수가 생성되지 않고 기존 함수를 재사용할 수 있다.

2. 의존성 배열에 의존하는 함수

함수가 외부 상태나 프롭스에 의존하는 경우, 해당 상태나 프롭스가 변경될 때마다 함수를 새로 생성해야 한다. 이때 useCallback의 의존성 배열에 해당 변수를 추가하여 새로운 함수가 필요한 시점에만 생성되게 한다.

useCallback 사용하지 말아야 할 경우

1. 단순한 한 줄짜리 콜백 함수

함수가 매우 간단하고, 렌더링 성능에 큰 영향을 미치지 않는 경우 useCallback을 사용할 필요가 없다. React는 이러한 함수를 자동으로 재사용하므로 추가적인 메모이제이션 작업이 필요하지 않다.

2. 의존성이 없는 함수

함수가 외부 상태나 프롭스에 의존하지 않고, 정적인 경우에도 useCallback을 사용할 필요가 없습니다.

profile
Always have hope🍀 & constant passion🔥

0개의 댓글