포스트

[Angular 마스터하기] Day 15 - 에러 처리와 로딩 상태

[Angular 마스터하기] Day 15 - 에러 처리와 로딩 상태

이제와서 시작하는 Angular 마스터하기 - Day 15 “안정적인 앱을 위한 에러 처리와 로딩 상태 관리! 🛡️”

오늘 배울 내용

  • HTTP 에러 처리
  • 로딩 상태 관리
  • try-catch와 catchError
  • 실전: 안전한 데이터 페칭

1. 로딩 상태 관리

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
import { Component, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-data-loader',
  template: `
    <div class="container">
      <button (click)="loadData()">데이터 로드</button>

      @if (isLoading()) {
        <div class="loading">⏳ 로딩 중...</div>
      } @else if (error()) {
        <div class="error">❌ 에러: {{ error() }}</div>
      } @else if (data()) {
        <div class="success">✅ 데이터: {{ data() | json }}</div>
      }
    </div>
  `,

  styles: [`
    .loading { color: #2196F3; }
    .error { color: #f44336; }
    .success { color: #4CAF50; }
  `]
})
export class DataLoaderComponent {
  private http = inject(HttpClient);

  data = signal<any>(null);
  isLoading = signal(false);
  error = signal<string | null>(null);

  loadData() {
    this.isLoading.set(true);
    this.error.set(null);

    this.http.get('https://jsonplaceholder.typicode.com/posts/1')
      .subscribe({
        next: (response) => {
          this.data.set(response);
          this.isLoading.set(false);
        },
        error: (err) => {
          this.error.set(err.message);
          this.isLoading.set(false);
        }
      });
  }
}

2. catchError 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { catchError, of } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class SafeDataService {
  private http = inject(HttpClient);

  getData() {
    return this.http.get('https://api.example.com/data').pipe(
      catchError(error => {
        console.error('에러 발생:', error);
        return of({ error: true, message: error.message });
      })
    );
  }
}

3. 재시도 로직

1
2
3
4
5
6
7
8
9
10
import { retry, delay } from 'rxjs';

loadDataWithRetry() {
  this.http.get('https://api.example.com/data').pipe(
    retry({ count: 3, delay: 1000 })  // 3번 재시도, 1초 대기
  ).subscribe({
    next: (data) => console.log('성공:', data),
    error: (err) => console.error('3번 시도 후 실패:', err)
  });
}

4. 실전 예제

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
@Component({
  selector: 'app-user-list',
  template: `
    <div class="user-list">
      <h1>사용자 목록</h1>

      <button (click)="loadUsers()" [disabled]="isLoading()">
        {{ isLoading() ? '로딩 중...' : '새로고침' }}
      </button>

      @if (isLoading()) {
        <div class="spinner">
          <div class="loader"></div>
          <p>데이터를 불러오는 중...</p>
        </div>
      } @else if (error()) {
        <div class="error-box">
          <h3>😢 오류가 발생했습니다</h3>
          <p>{{ error() }}</p>
          <button (click)="loadUsers()">다시 시도</button>
        </div>
      } @else {
        <div class="users">
          @for (user of users(); track user.id) {
            <div class="user-card">
              <h3>{{ user.name }}</h3>
              <p>{{ user.email }}</p>
            </div>
          }
        </div>
      }
    </div>
  `,

  styles: [`
    .spinner {
      text-align: center;
      padding: 40px;
    }
    .loader {
      border: 4px solid #f3f3f3;
      border-top: 4px solid #667eea;
      border-radius: 50%;
      width: 40px;
      height: 40px;
      animation: spin 1s linear infinite;
      margin: 0 auto;
    }
    @keyframes spin {
      0% { transform: rotate(0deg); }
      100% { transform: rotate(360deg); }
    }
    .error-box {
      background: #ffebee;
      padding: 30px;
      border-radius: 10px;
      text-align: center;
    }
  `]
})
export class UserListComponent {
  private http = inject(HttpClient);

  users = signal<any[]>([]);
  isLoading = signal(false);
  error = signal<string | null>(null);

  ngOnInit() {
    this.loadUsers();
  }

  loadUsers() {
    this.isLoading.set(true);
    this.error.set(null);

    this.http.get<any[]>('https://jsonplaceholder.typicode.com/users')
      .pipe(
        retry({ count: 2, delay: 1000 }),
        catchError(err => {
          this.error.set(err.message || '알 수 없는 오류');
          this.isLoading.set(false);
          return of([]);
        })
      )
      .subscribe(data => {
        this.users.set(data);
        this.isLoading.set(false);
      });
  }
}

📝 정리

Phase 3 완료! 🎉

축하합니다! Phase 3를 모두 완료했습니다:

  • ✅ Day 11: 폼 다루기
  • ✅ Day 12: Reactive Forms
  • ✅ Day 13: 파이프
  • ✅ Day 14: 컴포넌트 생명주기
  • ✅ Day 15: 에러 처리

체크리스트

  • 로딩 상태를 관리할 수 있나요?
  • 에러를 처리할 수 있나요?
  • 재시도 로직을 구현할 수 있나요?

📚 다음 학습

Phase 4 시작!


“안정적인 앱은 에러 처리에서 시작됩니다!” 🛡️

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.