로딩 상태와 에러 처리
데이터 페칭 과정에서 발생하는 로딩 상태와 에러를 효과적으로 처리하고 사용자에게 적절하게 피드백하는 방법에 대해 더 깊이 있게 다루겠습니다.
데이터 페칭은 비동기 작업이므로, 네트워크 지연, 서버 응답 없음, 데이터 형식 오류 등 다양한 문제가 발생할 수 있습니다. 이러한 상황들을 사용자에게 명확하게 전달하는 것은 좋은 사용자 경험(UX)을 제공하는 데 매우 중요합니다.
로딩 상태 (Loading State) 관리
사용자가 데이터를 기다리는 동안 애플리케이션이 멈춰있는 것처럼 보인다면 사용자 경험이 저해됩니다. 데이터가 로딩 중임을 명확히 표시하여 사용자가 기다리고 있음을 인지하게 해야 합니다.
구현 방법
useState
훅을 사용하여loading
상태(true
/false
)를 관리합니다.- 데이터 요청 시작 시
loading
을true
로 설정합니다. - 데이터 요청 완료 시 (성공 또는 실패와 무관하게)
loading
을false
로 설정합니다. loading
상태가true
일 때 스피너, 스켈레톤 UI, 또는 "데이터 로딩 중..."과 같은 메시지를 렌더링합니다.
import React, { useState, useEffect } from 'react';
function DataFetcherWithLoading() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true); // 초기값은 true (마운트 시 바로 로딩 시작)
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true); // 💡 요청 시작 시 로딩 상태 true
setError(null); // 이전 에러 초기화
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1'); // 예시 API
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err); // 💡 에러 발생 시 에러 상태 업데이트
} finally {
setLoading(false); // 💡 요청 완료 (성공/실패 무관) 시 로딩 상태 false
}
};
fetchData();
}, []);
if (loading) {
// 💡 로딩 중일 때 사용자에게 피드백 제공
return (
<div style={{ textAlign: 'center', padding: '30px', fontSize: '1.2em', color: '#555' }}>
<p>데이터를 불러오는 중입니다...</p>
{/* 간단한 스피너 CSS 예시 */}
<div style={{
border: '4px solid rgba(0, 0, 0, 0.1)',
borderTop: '4px solid #3498db',
borderRadius: '50%',
width: '30px',
height: '30px',
animation: 'spin 1s linear infinite',
margin: '20px auto',
}}></div>
<style>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
</div>
);
}
if (error) {
// 💡 에러 발생 시 에러 메시지 표시
return (
<div style={{ textAlign: 'center', padding: '30px', fontSize: '1.2em', color: 'red', border: '1px solid #e74c3c', borderRadius: '8px', backgroundColor: '#fdebeb' }}>
<p>데이터 로딩 중 오류가 발생했습니다!</p>
<p>오류 메시지: {error.message}</p>
<button
onClick={() => window.location.reload()} // 간단한 재시도 (실제로는 더 정교한 로직 필요)
style={{ marginTop: '20px', padding: '10px 20px', backgroundColor: '#e74c3c', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }}
>
다시 시도
</button>
</div>
);
}
return (
<div style={{ maxWidth: '600px', margin: '20px auto', padding: '25px', border: '1px solid #ddd', borderRadius: '8px', boxShadow: '0 2px 5px rgba(0,0,0,0.05)', backgroundColor: '#fdfdfd' }}>
<h2 style={{ textAlign: 'center', color: '#2c3e50', marginBottom: '20px' }}>로딩/에러 처리된 데이터</h2>
<h3 style={{ color: '#3498db', marginBottom: '10px' }}>{data.title}</h3>
<p>{data.body}</p>
</div>
);
}
export default DataFetcherWithLoading;
App.js
에 이 컴포넌트를 추가하여 테스트할 수 있습니다.
에러 처리 (Error Handling)
네트워크 요청은 항상 성공하는 것이 아닙니다. 다음과 같은 다양한 이유로 실패할 수 있습니다.
- 네트워크 연결 없음
- 서버 응답 없음
- HTTP 상태 코드 4xx (클라이언트 오류) 또는 5xx (서버 오류)
- 응답 데이터 파싱 오류
- API 키 만료 등 백엔드 로직 오류
구현 방법
useState
훅을 사용하여error
상태(null
또는Error
객체)를 관리합니다.try...catch
블록을 사용하여 비동기 함수 내에서 발생할 수 있는 에러를 포착합니다.fetch
API의 경우,response.ok
(HTTP 상태 코드가 200-299 범위인지 여부)를 확인하여 서버 응답이 성공적인지 확인해야 합니다.response.ok
가false
이면 직접Error
를 던져catch
블록에서 처리하도록 합니다.catch
블록에서error
상태를 업데이트하고, 사용자에게 친화적인 에러 메시지를 표시합니다.- 필요에 따라 에러 발생 시 재시도 버튼을 제공하거나, 로깅 시스템에 에러를 기록합니다.
코드 예시 (위 DataFetcherWithLoading.js
에 포함되어 있습니다)
const fetchData = async () => {
try {
setLoading(true);
setError(null); // 이전 에러 상태 초기화
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
// 💡 응답이 성공적인지 확인 (HTTP 상태 코드 200-299)
if (!response.ok) {
// 💡 성공적이지 않으면 에러를 던져 catch 블록으로 보냄
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err); // 💡 발생한 에러를 상태에 저장
} finally {
setLoading(false); // 💡 성공/실패 여부와 관계없이 로딩 종료
}
};
에러 메시지 표시
if (error) {
return (
<div style={{ textAlign: 'center', padding: '30px', fontSize: '1.2em', color: 'red', border: '1px solid #e74c3c', borderRadius: '8px', backgroundColor: '#fdebeb' }}>
<p>데이터 로딩 중 오류가 발생했습니다!</p>
<p>오류 메시지: {error.message}</p>
</div>
);
}
의존성 배열과 데이터 페칭 최적화
데이터 페칭 시 useEffect
의 의존성 배열을 올바르게 사용하는 것은 매우 중요합니다.
- 빈 배열 (
[]
): 컴포넌트가 처음 마운트될 때 한 번만 요청합니다. 정적 데이터나 초기 로딩에 적합합니다. - 변수 포함 (
[id, category]
): 특정props
나state
값이 변경될 때마다 데이터를 다시 가져옵니다. 예를 들어, 사용자 ID나 검색 카테고리가 변경될 때 유용합니다.
주의사항
- 함수나 객체 참조: 의존성 배열에 함수나 객체를 직접 넣으면, 해당 함수나 객체가 매 렌더링마다 새로 생성될 때마다
useEffect
가 불필요하게 재실행될 수 있습니다. 이를 방지하려면useCallback
(함수)이나useMemo
(객체) 훅을 사용하여 의존성을 안정화해야 합니다. 데이터 페칭 함수의 경우, 대부분// 잘못된 예시 (fetchData가 매 렌더링마다 새로 생성되어 무한 루프 가능성) useEffect(() => { const fetchData = async () => { /* ... */ }; fetchData(); }, [fetchData]); // 🚨 fetchData가 의존성에 포함되면 안 됨 // 올바른 예시 1: useEffect 내부에 함수 정의 (일반적) useEffect(() => { const fetchData = async () => { /* ... */ }; fetchData(); }, []); // 빈 배열: 함수는 내부에서 정의되므로 외부 의존성 아님 // 올바른 예시 2: useCallback으로 함수 안정화 const fetchData = useCallback(async () => { /* ... */ }, []); useEffect(() => { fetchData(); }, [fetchData]); // fetchData가 안정적이므로 안전
useEffect
내부에 정의하는 것이 일반적이고 간결합니다.
데이터 페칭 로직의 재사용: 커스텀 훅
여러 컴포넌트에서 비슷한 데이터 페칭 로직(로딩, 에러 처리, 데이터 상태 관리)이 반복된다면, 이를 커스텀 훅(Custom Hook) 으로 분리하여 재사용성과 가독성을 높일 수 있습니다.
useFetch
커스텀 훅 예시
import { useState, useEffect } from 'react';
const useFetch = (url, options) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url, { ...options, signal });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
setData(json);
} catch (err) {
if (err.name === 'AbortError') {
console.log('Fetch aborted');
} else {
setError(err);
}
} finally {
setLoading(false);
}
};
if (url) { // URL이 유효할 때만 fetch 실행 (옵션)
fetchData();
} else {
setLoading(false);
}
return () => {
controller.abort();
};
}, [url, options]); // URL이나 옵션이 변경될 때마다 재실행
return { data, loading, error };
};
export default useFetch;
useFetch
커스텀 훅 사용 예시
import React from 'react';
import { useParams } from 'react-router-dom';
import useFetch from '../hooks/useFetch'; // 커스텀 훅 임포트
function PostDetailWithHook() {
const { postId } = useParams();
const { data: post, loading, error } = useFetch(
`https://jsonplaceholder.typicode.com/posts/${postId}`
); // 훅 사용!
if (loading) {
return <div style={{ textAlign: 'center', padding: '20px' }}>게시글을 불러오는 중...</div>;
}
if (error) {
return <div style={{ textAlign: 'center', padding: '20px', color: 'red' }}>오류 발생: {error.message}</div>;
}
if (!post) {
return <div style={{ textAlign: 'center', padding: '20px' }}>게시글을 찾을 수 없습니다.</div>;
}
return (
<div style={{ maxWidth: '600px', margin: '20px auto', padding: '25px', border: '1px solid #ddd', borderRadius: '8px', boxShadow: '0 2px 5px rgba(0,0,0,0.05)', backgroundColor: '#fdfdfd' }}>
<h2 style={{ textAlign: 'center', color: '#2c3e50', marginBottom: '20px' }}>{post.title}</h2>
<p>{post.body}</p>
</div>
);
}
export default PostDetailWithHook;
이처럼 커스텀 훅을 사용하면 각 컴포넌트에서 반복되는 로딩/에러 처리 로직을 깔끔하게 분리할 수 있습니다.
"로딩 상태와 에러 처리"는 여기까지입니다. 이 장에서는 비동기 데이터 페칭 과정에서 발생하는 로딩 상태와 에러를 효과적으로 관리하고 사용자에게 피드백하는 중요성에 대해 배웠습니다. useState
와 try...catch
를 이용한 기본적인 구현 방법부터, useEffect
의 의존성 배열 사용 시 주의사항, 그리고 커스텀 훅을 통한 로직 재사용까지 심화된 내용을 다루었습니다.
이제 여러분은 리액트 애플리케이션에서 견고하고 사용자 친화적인 데이터 페칭 로직을 구현할 수 있는 기초를 마련했습니다. 다음 장에서는 axios
와 같은 인기 있는 HTTP 클라이언트 라이브러리를 사용하여 데이터 페칭을 더욱 편리하게 만드는 방법을 알아보겠습니다.