[이제와서 시작하는 React 마스터하기 #12] 에러 처리와 로딩 화면 - 사용자 경험 챙기기
[이제와서 시작하는 React 마스터하기 #12] 에러 처리와 로딩 화면 - 사용자 경험 챙기기
100일 챌린지 Day 12 - 앱이 깨지지 않게! 에러가 나도 괜찮은 화면을 보여주고, 로딩 중에도 멋진 화면을 만들어봐요. 🛡️
배울 내용
이번 시간에는 에러와 로딩 상태를 잘 처리하는 방법을 배워봐요.
- 에러가 났을 때 앱 전체가 멈추지 않게 하기
- Error Boundary로 에러 화면 예쁘게 보여주기
- Suspense로 로딩 화면 만들기
- 코드 분할(Code Splitting)로 앱 가볍게 만들기
시작하기 전에
이번 내용을 더 잘 이해하려면 아래 내용을 먼저 보고 오세요:
일상 비유로 이해하기
에러 처리와 로딩을 일상으로 비유하면 이렇습니다:
🏪 편의점 운영하기
- 에러 없이: 모든 물건이 진열돼 있고 손님이 쇼핑해요
- 에러 발생: 전기가 나가서 가게가 완전히 멈춰요 (최악!)
- Error Boundary: “임시 휴업” 안내문을 붙여요 (친절)
- Suspense: “잠시만 기다려주세요~” 안내문 (로딩 중)
🚗 자동차 계기판
- 에러: 엔진 경고등이 켜져요
- Error Boundary: 경고등 대신 “서비스센터 방문 권장” 메시지
- Loading: “시동 거는 중…” 표시
- Suspense: “내비게이션 로딩 중…” 스피너
1️⃣ 기본 에러 처리
에러가 나면 어떻게 되나요?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function BrokenComponent() {
const user = null;
return (
<div>
<h1>{user.name}</h1> {/* 💥 에러! user가 null이에요 */}
</div>
);
}
function App() {
return (
<div>
<h1>내 앱</h1>
<BrokenComponent />
<p>이 글은 절대 보이지 않아요</p> {/* 에러 때문에 앱 전체가 멈춤 */}
</div>
);
}
결과: 하얀 화면만 보여요! 사용자는 무슨 일이 일어났는지 몰라요.
조건문으로 에러 방지하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function SaferComponent() {
const user = null;
// ✅ 조건문으로 체크
if (!user) {
return <div>사용자 정보가 없습니다</div>;
}
return (
<div>
<h1>{user.name}</h1>
</div>
);
}
useState로 에러 상태 관리하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(response => {
if (!response.ok) {
throw new Error('사용자를 찾을 수 없습니다');
}
return response.json();
})
.then(data => {
setUser(data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, [userId]);
// 로딩 중
if (loading) {
return <div>로딩 중...</div>;
}
// 에러 발생
if (error) {
return (
<div style=>
<h2>오류가 발생했습니다</h2>
<p>{error}</p>
</div>
);
}
// 정상
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
패턴:
loading: 데이터를 가져오는 중error: 에러가 발생했을 때- 정상: 데이터가 있을 때
2️⃣ Error Boundary - 에러 안전망
Error Boundary란?
컴포넌트에서 에러가 나도 앱 전체가 멈추지 않게 하는 안전망이에요.
중요: Error Boundary는 클래스 컴포넌트로만 만들 수 있어요.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
// 에러가 발생하면 이 메서드가 호출돼요
static getDerivedStateFromError(error) {
return { hasError: true };
}
// 에러 정보를 기록할 수 있어요
componentDidCatch(error, errorInfo) {
console.error('에러 발생:', error);
console.error('에러 위치:', errorInfo.componentStack);
}
render() {
// 에러가 발생했으면 대체 UI를 보여줘요
if (this.state.hasError) {
return (
<div style=>
<h2>😢 문제가 발생했습니다</h2>
<p>잠시 후 다시 시도해주세요</p>
<button onClick={() => window.location.reload()}>
새로고침
</button>
</div>
);
}
// 정상이면 자식 컴포넌트를 렌더링해요
return this.props.children;
}
}
export default ErrorBoundary;
Error Boundary 사용하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import ErrorBoundary from './ErrorBoundary';
function BrokenComponent() {
throw new Error('일부러 에러를 냈어요!');
}
function App() {
return (
<div>
<h1>내 앱</h1>
{/* ErrorBoundary로 감싸기 */}
<ErrorBoundary>
<BrokenComponent />
</ErrorBoundary>
<p>이 글은 보여요! ✅</p>
</div>
);
}
결과: BrokenComponent에서 에러가 나도 앱의 다른 부분은 정상 작동해요!
실습: 사용자 카드 목록 보호하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { useState } from 'react';
import ErrorBoundary from './ErrorBoundary';
function UserCard({ user }) {
// 데이터가 이상하면 에러를 던져요
if (!user || !user.name) {
throw new Error('사용자 데이터가 올바르지 않습니다');
}
return (
<div style=>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
}
function UserList() {
const users = [
{ id: 1, name: '김철수', email: 'kim@example.com' },
{ id: 2, name: '이영희', email: 'lee@example.com' },
{ id: 3 }, // 💥 이름이 없어서 에러!
{ id: 4, name: '박민수', email: 'park@example.com' }
];
return (
<div>
<h2>사용자 목록</h2>
{users.map(user => (
// 각 카드를 ErrorBoundary로 보호
<ErrorBoundary key={user.id}>
<UserCard user={user} />
</ErrorBoundary>
))}
</div>
);
}
결과: 3번 카드만 에러 메시지가 보이고, 나머지는 정상적으로 표시돼요!
3️⃣ Suspense - 로딩 화면 쉽게 만들기
Suspense란?
컴포넌트가 준비될 때까지 로딩 화면을 보여주는 기능이에요.
lazy로 컴포넌트 지연 로딩하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Suspense, lazy } from 'react';
// lazy: 필요할 때만 컴포넌트를 불러와요
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<div>
<h1>내 앱</h1>
{/* Suspense로 감싸고 fallback 제공 */}
<Suspense fallback={<div>로딩 중...</div>}>
<HeavyComponent />
</Suspense>
</div>
);
}
동작 방식:
- 처음에는 “로딩 중…” 메시지가 보여요
- HeavyComponent가 불러와지면 실제 컴포넌트가 보여요
실습: 멋진 로딩 스피너 만들기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import { Suspense, lazy } from 'react';
// 로딩 스피너 컴포넌트
function LoadingSpinner() {
return (
<div style=>
<div style=></div>
<p style=>
페이지를 불러오는 중...
</p>
<style>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
</div>
);
}
// 페이지 컴포넌트들
const HomePage = lazy(() => import('./pages/HomePage'));
const AboutPage = lazy(() => import('./pages/AboutPage'));
const ProfilePage = lazy(() => import('./pages/ProfilePage'));
function App() {
const [page, setPage] = useState('home');
return (
<div>
<nav style=>
<button onClick={() => setPage('home')}>홈</button>
<button onClick={() => setPage('about')}>소개</button>
<button onClick={() => setPage('profile')}>프로필</button>
</nav>
<Suspense fallback={<LoadingSpinner />}>
{page === 'home' && <HomePage />}
{page === 'about' && <AboutPage />}
{page === 'profile' && <ProfilePage />}
</Suspense>
</div>
);
}
4️⃣ Error Boundary + Suspense 함께 사용하기
두 기능을 함께 사용하면 완벽해요!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { Suspense, lazy } from 'react';
import ErrorBoundary from './ErrorBoundary';
import LoadingSpinner from './LoadingSpinner';
const UserDashboard = lazy(() => import('./UserDashboard'));
const Analytics = lazy(() => import('./Analytics'));
const Settings = lazy(() => import('./Settings'));
function App() {
return (
<div style=>
<h1>대시보드</h1>
{/* 에러도 잡고, 로딩도 처리 */}
<ErrorBoundary>
<Suspense fallback={<LoadingSpinner />}>
<UserDashboard />
</Suspense>
</ErrorBoundary>
<div style=>
<ErrorBoundary>
<Suspense fallback={<div>분석 자료 로딩 중...</div>}>
<Analytics />
</Suspense>
</ErrorBoundary>
<ErrorBoundary>
<Suspense fallback={<div>설정 로딩 중...</div>}>
<Settings />
</Suspense>
</ErrorBoundary>
</div>
</div>
);
}
장점:
- 각 섹션이 독립적으로 로딩돼요
- 하나가 에러 나도 다른 부분은 작동해요
- 사용자가 무슨 일이 일어나는지 알 수 있어요
5️⃣ 종합 실습: 블로그 앱
모든 개념을 사용한 완전한 예제예요.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import { Suspense, lazy, useState } from 'react';
import ErrorBoundary from './ErrorBoundary';
// 지연 로딩 컴포넌트들
const PostList = lazy(() => import('./PostList'));
const PostDetail = lazy(() => import('./PostDetail'));
// 스켈레톤 로딩 (더 멋진 로딩 화면)
function PostListSkeleton() {
return (
<div>
{[1, 2, 3].map(i => (
<div key={i} style=>
<div style=></div>
<div style=></div>
</div>
))}
</div>
);
}
function PostDetailSkeleton() {
return (
<div style=>
<div style=></div>
<div style=></div>
<div style=></div>
</div>
);
}
function BlogApp() {
const [selectedPostId, setSelectedPostId] = useState(null);
return (
<div style=>
<header style=>
<h1>내 블로그</h1>
<button onClick={() => setSelectedPostId(null)}>
전체 글 보기
</button>
</header>
<main>
{selectedPostId ? (
// 글 상세 페이지
<ErrorBoundary>
<Suspense fallback={<PostDetailSkeleton />}>
<PostDetail
postId={selectedPostId}
onBack={() => setSelectedPostId(null)}
/>
</Suspense>
</ErrorBoundary>
) : (
// 글 목록 페이지
<ErrorBoundary>
<Suspense fallback={<PostListSkeleton />}>
<PostList onSelectPost={setSelectedPostId} />
</Suspense>
</ErrorBoundary>
)}
</main>
</div>
);
}
export default BlogApp;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// PostList.js
function PostList({ onSelectPost }) {
const posts = [
{ id: 1, title: 'React 시작하기', excerpt: 'React의 기초를 배워봐요' },
{ id: 2, title: 'Hook 마스터하기', excerpt: 'useState, useEffect 완벽 정복' },
{ id: 3, title: '에러 처리하기', excerpt: '안정적인 앱 만들기' }
];
return (
<div>
<h2>전체 글</h2>
{posts.map(post => (
<div
key={post.id}
onClick={() => onSelectPost(post.id)}
style=
>
<h3>{post.title}</h3>
<p style=>{post.excerpt}</p>
</div>
))}
</div>
);
}
export default PostList;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// PostDetail.js
function PostDetail({ postId, onBack }) {
// 실제로는 API에서 데이터를 가져와요
const post = {
id: postId,
title: `포스트 ${postId}`,
content: '여기에 긴 내용이 들어가요...',
author: '김철수',
date: '2024-01-15'
};
return (
<article>
<button onClick={onBack} style=>
← 목록으로
</button>
<h2>{post.title}</h2>
<div style=>
<span>작성자: {post.author}</span>
<span style=>날짜: {post.date}</span>
</div>
<div style=>
<p>{post.content}</p>
</div>
</article>
);
}
export default PostDetail;
흔한 실수와 해결 방법
실수 1: Error Boundary가 에러를 못 잡아요
1
2
3
4
5
6
7
8
// ❌ 이벤트 핸들러의 에러는 못 잡아요
function BrokenButton() {
const handleClick = () => {
throw new Error('클릭 에러!'); // Error Boundary가 못 잡아요
};
return <button onClick={handleClick}>클릭</button>;
}
해결:
1
2
3
4
5
6
7
8
9
10
11
12
13
// ✅ 이벤트 핸들러는 try/catch 사용
function SafeButton() {
const handleClick = () => {
try {
throw new Error('클릭 에러!');
} catch (error) {
console.error('에러 발생:', error);
alert('처리 중 문제가 발생했습니다');
}
};
return <button onClick={handleClick}>클릭</button>;
}
Error Boundary가 잡는 에러:
- 렌더링 중 에러
- 생명주기 메서드 에러
- 자식 컴포넌트의 constructor 에러
Error Boundary가 못 잡는 에러:
- 이벤트 핸들러 (onClick 등)
- 비동기 코드 (setTimeout, Promise)
- 서버 사이드 렌더링
- Error Boundary 자체의 에러
실수 2: Suspense에서 데이터 페칭
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ❌ 이렇게 하면 안 돼요
function BadComponent() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/data').then(res => setData(res));
}, []);
return <div>{data}</div>;
}
// Suspense는 이걸 감지 못 해요
<Suspense fallback={<div>로딩...</div>}>
<BadComponent />
</Suspense>
해결:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ✅ lazy 사용
const LazyComponent = lazy(() => import('./MyComponent'));
<Suspense fallback={<div>로딩...</div>}>
<LazyComponent />
</Suspense>
// ✅ 또는 수동으로 로딩 상태 관리
function GoodComponent() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(data => {
setData(data);
setLoading(false);
});
}, []);
if (loading) return <div>로딩 중...</div>;
return <div>{data}</div>;
}
실수 3: lazy를 컴포넌트 안에서 호출
1
2
3
4
5
6
7
8
9
10
// ❌ 렌더링마다 새로 만들어져요
function App() {
const LazyComponent = lazy(() => import('./Component')); // 잘못됨!
return (
<Suspense fallback={<div>로딩...</div>}>
<LazyComponent />
</Suspense>
);
}
해결:
1
2
3
4
5
6
7
8
9
10
// ✅ 컴포넌트 밖에서 한 번만 선언
const LazyComponent = lazy(() => import('./Component'));
function App() {
return (
<Suspense fallback={<div>로딩...</div>}>
<LazyComponent />
</Suspense>
);
}
정리
오늘 배운 내용
- try/catch와 조건문으로 기본 에러 처리를 할 수 있어요
- Error Boundary로 컴포넌트 에러를 잡을 수 있어요
- Suspense로 로딩 화면을 쉽게 만들 수 있어요
- lazy로 컴포넌트를 필요할 때만 불러올 수 있어요
- 에러와 로딩을 함께 처리할 수 있어요
핵심 정리
Error Boundary:
- 컴포넌트 에러를 잡아요
- 클래스 컴포넌트로만 만들 수 있어요
- 이벤트 핸들러 에러는 못 잡아요
Suspense:
- 로딩 화면을 쉽게 만들어요
- lazy와 함께 사용해요
- fallback으로 로딩 UI를 지정해요
다음 단계
- React Query로 더 쉽게 데이터 관리하기
- react-error-boundary 라이브러리 사용하기
- Sentry로 에러 모니터링하기
숙제
필수 과제
- Error Boundary 만들기
- ErrorBoundary 컴포넌트 작성
- “다시 시도” 버튼 추가
- 에러 메시지 표시
- 로딩 스피너 만들기
- LoadingSpinner 컴포넌트 작성
- CSS 애니메이션 사용
- Suspense fallback으로 사용
선택 과제
- 사용자 목록 앱
- lazy로 UserList 컴포넌트 만들기
- ErrorBoundary로 감싸기
- Suspense로 로딩 처리
- 다중 페이지 앱
- 3개 페이지 만들기 (lazy 사용)
- 각 페이지별 ErrorBoundary
- 페이지별 다른 로딩 화면
“에러는 피할 수 없어요. 하지만 잘 처리할 수 있어요! 💪”
좋은 앱은 에러가 나도 사용자에게 친절한 메시지를 보여줍니다.
React 완벽 가이드 시리즈
- React란 무엇인가? 시작하기 전 알아야 할 모든 것
- useState와 useEffect - 상태와 생명주기 기초
- 컴포넌트 이해하기 - 레고 블록으로 만드는 웹
- Props - 컴포넌트끼리 대화하기
- useEffect - 생명주기 이해하기
- 조건부/리스트 렌더링 - 똑똑하게 보여주기
- Context API - Props 지옥 탈출하기
- Custom Hooks - 나만의 Hook 만들기
- React Router - 페이지 이동하기
- 폼 처리 - 사용자 입력 받기
- 성능 최적화 - 똑똑하게 렌더링하기
- 에러 처리와 로딩 화면 - 사용자 경험 챙기기 ← 현재 글
- 상태 관리 라이브러리 - Redux, Zustand, Recoil 비교
- 서버 상태 관리 - React Query/TanStack Query 활용
- React와 TypeScript - 타입 안전한 React 개발
- Next.js 입문 - SSR/SSG와 풀스택 React
- React 테스팅 전략 - Jest, React Testing Library, E2E
- 고급 패턴과 베스트 프랙티스 - HOC, Render Props, Compound Components
- 실전 프로젝트 - Todo 앱에서 쇼핑몰까지
- React 배포와 DevOps - 빌드 최적화와 CI/CD
관련 자료
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.
