icon안동민 개발노트

로딩 상태와 에러 처리


 비동기 데이터 가져오기 과정에서 로딩 상태 관리와 에러 처리는 사용자 경험을 향상시키는 데 중요한 역할을 합니다.

 이 절에서는 React 애플리케이션에서 이러한 상황을 효과적으로 다루는 방법을 살펴보겠습니다.

로딩 상태 관리

 로딩 상태를 관리하는 가장 기본적인 방법은 state를 사용하는 것입니다.

import React, { useState, useEffect } from 'react';
 
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
 
  useEffect(() => {
    async function fetchUser() {
      setLoading(true);
      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);
      } finally {
        setLoading(false);
      }
    }
 
    fetchUser();
  }, [userId]);
 
  if (loading) {
    return <div>Loading...</div>;
  }
 
  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
    </div>
  );
}

 이 예제에서는 loading 상태를 사용하여 데이터 가져오기 진행 중임을 표시합니다.

 데이터 요청이 시작될 때 loadingtrue로 설정하고, 요청이 완료되면 false로 설정합니다.

로딩 인디케이터 구현

 사용자에게 더 나은 시각적 피드백을 제공하기 위해 커스텀 로딩 인디케이터를 구현할 수 있습니다.

function LoadingSpinner() {
  return (
    <div className="spinner">
      <div className="bounce1"></div>
      <div className="bounce2"></div>
      <div className="bounce3"></div>
    </div>
  );
}
 
function UserProfile({ userId }) {
  // ... 이전 코드와 동일 ...
 
  if (loading) {
    return <LoadingSpinner />;
  }
 
  // ... 나머지 코드 ...
}

 LoadingSpinner 컴포넌트는 CSS 애니메이션을 사용하여 동적인 로딩 표시를 제공할 수 있습니다.

에러 처리

 에러 처리는 try-catch 문을 사용하여 구현할 수 있습니다.

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
 
  useEffect(() => {
    async function fetchUser() {
      setLoading(true);
      setError(null);
      try {
        const response = await fetch(`https://api.example.com/users/${userId}`);
        if (!response.ok) {
          throw new Error('Failed to fetch user data');
        }
        const data = await response.json();
        setUser(data);
      } catch (error) {
        setError(error.message);
      } finally {
        setLoading(false);
      }
    }
 
    fetchUser();
  }, [userId]);
 
  if (loading) {
    return <LoadingSpinner />;
  }
 
  if (error) {
    return <div>Error: {error}</div>;
  }
 
  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
    </div>
  );
}

 이 예제에서는 에러가 발생하면 error 상태를 설정하고, 이를 사용자에게 표시합니다.

네트워크 에러 처리

 네트워크 에러와 같은 특정 에러 상황에 대해 더 구체적인 처리를 할 수 있습니다.

async function fetchUser() {
  setLoading(true);
  setError(null);
  try {
    const response = await fetch(`https://api.example.com/users/${userId}`);
    if (!response.ok) {
      if (response.status === 404) {
        throw new Error('User not found');
      } else {
        throw new Error('An error occurred while fetching the user');
      }
    }
    const data = await response.json();
    setUser(data);
  } catch (error) {
    if (error instanceof TypeError && error.message === 'Failed to fetch') {
      setError('Network error. Please check your internet connection.');
    } else {
      setError(error.message);
    }
  } finally {
    setLoading(false);
  }
}

 이 예제에서는 서버 응답 상태 코드와 네트워크 오류를 구분하여 처리합니다.

에러 경계(Error Boundaries) 구현

 컴포넌트 트리 전체에 대한 에러 처리를 위해 에러 경계 컴포넌트를 구현할 수 있습니다.

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
 
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
 
  componentDidCatch(error, errorInfo) {
    console.log('Error caught by ErrorBoundary:', error, errorInfo);
  }
 
  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }
 
    return this.props.children;
  }
}
 
function App() {
  return (
    <ErrorBoundary>
      <UserProfile userId={1} />
    </ErrorBoundary>
  );
}

 에러 경계는 하위 컴포넌트 트리에서 발생하는 JavaScript 에러를 포착하고, 대체 UI를 표시합니다.

재시도 메커니즘 구현

 일시적인 네트워크 문제를 해결하기 위해 재시도 메커니즘을 구현할 수 있습니다.

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
 
  async function fetchUserWithRetry(retries = 3) {
    setLoading(true);
    setError(null);
    for (let i = 0; i < retries; i++) {
      try {
        const response = await fetch(`https://api.example.com/users/${userId}`);
        if (!response.ok) {
          throw new Error('Failed to fetch user data');
        }
        const data = await response.json();
        setUser(data);
        return;
      } catch (error) {
        console.log(`Attempt ${i + 1} failed. Retrying...`);
        if (i === retries - 1) {
          setError('Failed to fetch user after multiple attempts');
        }
      }
    }
    setLoading(false);
  }
 
  useEffect(() => {
    fetchUserWithRetry();
  }, [userId]);
 
  if (loading) return <LoadingSpinner />;
  if (error) return <div>Error: {error}</div>;
 
  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
    </div>
  );
}

 이 예제에서는 데이터 가져오기에 실패할 경우 지정된 횟수만큼 재시도합니다.

 로딩 상태 관리와 에러 처리는 견고한 React 애플리케이션을 구축하는 데 필수적입니다.

 적절한 로딩 인디케이터를 사용하면 사용자에게 작업이 진행 중임을 알려줄 수 있고, 체계적인 에러 처리를 통해 문제가 발생했을 때 사용자에게 명확한 피드백을 제공할 수 있습니다.

 다양한 에러 상황(네트워크 오류, 서버 오류, 데이터 형식 오류 등)에 대비하고, 각 상황에 맞는 적절한 메시지를 표시하는 것이 중요합니다.

 또한, 에러 경계를 사용하여 예상치 못한 에러로부터 애플리케이션을 보호하고, 가능한 경우 재시도 메커니즘을 구현하여 일시적인 문제를 해결할 수 있습니다.

 이러한 기법들을 적절히 조합하여 사용하면, 더 안정적이고 사용자 친화적인 React 애플리케이션을 개발할 수 있습니다.