icon안동민 개발노트

useEffect를 이용한 데이터 가져오기


 React의 useEffect 훅은 컴포넌트의 생명주기와 관련된 부수 효과를 처리하는 데 사용됩니다.

 이는 외부 API에서 데이터를 가져오는 작업에 특히 유용합니다.

컴포넌트 마운트 시 데이터 페칭

 가장 기본적인 useEffect 사용법은 컴포넌트가 마운트될 때 데이터를 가져오는 것입니다.

import React, { useState, useEffect } from 'react';
 
function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
 
  useEffect(() => {
    fetch('https://api.example.com/users')
      .then(response => response.json())
      .then(data => {
        setUsers(data);
        setLoading(false);
      })
      .catch(error => {
        setError(error);
        setLoading(false);
      });
  }, []); // 빈 의존성 배열은 이 효과가 마운트 시에만 실행됨을 의미합니다
 
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
 
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

 이 예제에서 useEffect는 컴포넌트가 마운트될 때 한 번만 실행됩니다.

 데이터 페칭 상태를 관리하기 위해 loading과 error 상태도 함께 사용합니다.

의존성 배열을 이용한 조건부 데이터 페칭

 때로는 특정 값이 변경될 때마다 데이터를 다시 가져와야 할 수 있습니다.

 이럴 때 useEffect의 의존성 배열을 활용할 수 있습니다.

function UserPosts({ userId }) {
  const [posts, setPosts] = useState([]);
 
  useEffect(() => {
    fetch(`https://api.example.com/users/${userId}/posts`)
      .then(response => response.json())
      .then(data => setPosts(data));
  }, [userId]); // userId가 변경될 때마다 이 효과가 실행됩니다
 
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

 이 예제에서는 userId가 변경될 때마다 해당 사용자의 게시물을 새로 가져옵니다.

비동기 함수 처리

 useEffect 내에서 직접 async 함수를 사용할 수 없기 때문에, 별도의 async 함수를 정의하고 이를 호출하는 방식을 사용합니다.

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
 
  useEffect(() => {
    async function fetchUser() {
      try {
        const response = await fetch(`https://api.example.com/users/${userId}`);
        const data = await response.json();
        setUser(data);
      } catch (error) {
        console.error('Error fetching user:', error);
      }
    }
 
    fetchUser();
  }, [userId]);
 
  if (!user) return <div>Loading...</div>;
 
  return <div>{user.name}</div>;
}

 이 방식을 사용하면 async / await 문법의 장점을 활용하면서도 useEffect의 규칙을 준수할 수 있습니다.

Race Condition 문제와 해결

 데이터 페칭 시 발생할 수 있는 주요 문제 중 하나는 race condition입니다.

 이는 여러 비동기 작업이 동시에 진행될 때, 작업의 완료 순서가 시작 순서와 다를 수 있어 발생하는 문제입니다.

function UserInfo({ userId }) {
  const [user, setUser] = useState(null);
 
  useEffect(() => {
    let isCurrent = true;
 
    async function fetchUser() {
      try {
        const response = await fetch(`https://api.example.com/users/${userId}`);
        const data = await response.json();
        if (isCurrent) {
          setUser(data);
        }
      } catch (error) {
        if (isCurrent) {
          console.error('Error fetching user:', error);
        }
      }
    }
 
    fetchUser();
 
    return () => {
      isCurrent = false;
    };
  }, [userId]);
 
  if (!user) return <div>Loading...</div>;
 
  return <div>{user.name}</div>;
}

 이 예제에서는 isCurrent 플래그를 사용하여 컴포넌트가 언마운트되거나 userId가 변경된 후에 도착한 응답을 무시합니다.

 이를 통해 race condition 문제를 방지할 수 있습니다.

useEffect 클린업 함수

 useEffect의 클린업 함수는 컴포넌트가 언마운트되거나 의존성이 변경되어 효과가 다시 실행되기 전에 호출됩니다.

 이는 리소스 누수를 방지하고 불필요한 작업을 취소하는 데 중요합니다.

function LiveData() {
  const [data, setData] = useState(null);
 
  useEffect(() => {
    const source = new EventSource('https://api.example.com/live-data');
 
    source.onmessage = (event) => {
      setData(JSON.parse(event.data));
    };
 
    return () => {
      source.close(); // 컴포넌트 언마운트 시 연결 종료
    };
  }, []);
 
  return <div>{data ? `Live data: ${data}` : 'Waiting for data...'}</div>;
}

 이 예제에서 클린업 함수는 EventSource 연결을 종료하여 리소스 누수를 방지합니다.

 useEffect를 사용한 데이터 페칭은 React 애플리케이션에서 매우 일반적인 패턴입니다. 그러나 이 방식에는 몇 가지 한계가 있습니다.

  1. 중복 요청 방지가 어려울 수 있습니다.
  2. 캐싱을 구현하기 복잡할 수 있습니다.
  3. 서버 사이드 렌더링과의 통합이 쉽지 않을 수 있습니다.

 이러한 한계를 극복하기 위해 React Query, SWR 같은 데이터 페칭 라이브러리를 사용하거나, React Suspense for Data Fetching (아직 실험적 기능)을 사용할 수 있습니다.

 그럼에도 불구하고 useEffect를 이용한 데이터 페칭은 여전히 유효하고 널리 사용되는 방법입니다.

 특히 간단한 데이터 페칭 시나리오나 커스텀 로직이 필요한 경우에 유용합니다.

 중요한 것은 race condition, 메모리 누수 등의 잠재적인 문제를 인식하고 적절히 처리하는 것입니다.

 마지막으로, useEffect를 사용할 때는 항상 의존성 배열을 신중히 관리해야 합니다. 필요한 의존성을 모두 포함시키되, 불필요한 리렌더링을 피하기 위해 과도한 의존성 포함은 피해야 합니다.

 ESLint의 exhaustive-deps 규칙을 활용하면 이를 효과적으로 관리할 수 있습니다.