async/await로 완성하는 비동기 조회 마스터 클래스
Promise 지옥에서 벗어나기
Angular + Firebase 프로젝트에서 async/await를 사용해 비동기 데이터를 조회하는 방법을 실제 프로덕션 코드와 함께 설명한다. Observable과 Promise의 차이, async/await 패턴, 에러 처리까지 모든 것을 다룬다.
목차
1. async/await란 무엇인가?
async/await는 Promise를 더 쉽게 사용할 수 있게 해주는 문법이다. 비동기 코드를 동기 코드처럼 읽기 쉽게 작성할 수 있다.
1.1 Promise의 문제점
Promise를 중첩해서 사용하면 코드가 복잡해진다:
// ❌ Promise 중첩 (콜백 지옥)
getUser().then((user) => {
getData(user.id).then((data) => {
getAddress(data.addressId).then((address) => {
console.log(address);
});
});
});
1.2 async/await의 해결책
async/await를 사용하면 동기 코드처럼 읽기 쉽다:
// ✅ async/await (깔끔한 코드)
async function loadData() {
const user = await getUser();
const data = await getData(user.id);
const address = await getAddress(data.addressId);
console.log(address);
}
async 함수는 항상 Promise를 반환하고, await는 Promise가 완료될 때까지 기다린다.2. Observable vs Promise vs async/await
Angular 프로젝트에서는 Observable과 Promise를 모두 사용한다. 각각의 특징을 이해하고 적절히 선택해야 한다.
| 타입 | 특징 | 사용 시기 |
|---|---|---|
Observable |
여러 값을 스트림으로 전달, 실시간 업데이트 가능 | Firestore 실시간 리스너, HTTP 요청 |
Promise |
단일 값을 한 번만 반환 | 한 번만 실행되는 작업 |
async/await |
Promise를 동기 코드처럼 작성 | 순차적 비동기 작업 |
2.1 Observable을 Promise로 변환
Firestore의 collection$는 Observable을 반환하지만, 한 번만 조회하려면 Promise로 변환해야 한다:
import { lastValueFrom, take } from 'rxjs';
// Observable을 Promise로 변환
async getUser() {
return await lastValueFrom(this.user$.pipe(take(1)));
}
take(1)을 사용하면 첫 번째 값만 가져온 후 구독이 자동으로 해제된다.3. 기본 async/await 사용법
async/await의 기본 문법을 익힌다.
3.1 async 함수 선언
// 함수 선언
async function fetchData() {
// ...
}
// 화살표 함수
const fetchData = async () => {
// ...
};
// 클래스 메서드
class DataService {
async fetchData() {
// ...
}
}
3.2 await 사용
async function loadUser() {
// Promise가 완료될 때까지 기다림
const user = await getUser();
console.log(user);
// 다음 코드는 user가 로드된 후 실행됨
const data = await getData(user.id);
return data;
}
await는 async 함수 안에서만 사용할 수 있다.4. Firestore 비동기 조회 구현
실제 프로덕션 환경에서 사용하는 Firestore 비동기 조회 패턴을 설명한다.
4.1 toCollection$ 메서드 구현
한 번만 조회하는 경우 Promise 기반 메서드를 사용한다:
import { Injectable } from '@angular/core';
import { Firestore, collection, query, where, getDocs } from '@angular/fire/firestore';
@Injectable({
providedIn: 'root',
})
export class DbService {
constructor(public firestore: Firestore) {}
/**
* Promise 기반 컬렉션 조회 (한 번만 실행)
* @param collectionName 컬렉션 이름
* @param queryFn 쿼리 함수 (선택사항)
* @returns Promise<any[]>
*/
async toCollection$(collectionName: string, queryFn?: Function): Promise<any[]> {
const result: any[] = [];
if (queryFn) {
// 쿼리가 있는 경우
const queryData = this.parseQuery(queryFn);
const q = query(collection(this.firestore, collectionName), ...queryData);
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
result.push({ id: doc.id, ...doc.data() });
});
return result;
} else {
// 전체 조회
const querySnapshot = await getDocs(
collection(this.firestore, collectionName)
);
querySnapshot.forEach((doc) => {
result.push({ id: doc.id, ...doc.data() });
});
return result;
}
}
/**
* 쿼리 파싱 헬퍼
*/
private parseQuery(queryFn: Function): any[] {
const queryArray: any[] = [];
const queryRef = {
where: (field: string, operator: any, value: any) => {
queryArray.push(where(field, operator, value));
return queryRef;
},
orderBy: (field: string, direction?: 'asc' | 'desc') => {
queryArray.push(orderBy(field, direction || 'asc'));
return queryRef;
},
limit: (count: number) => {
queryArray.push(limit(count));
return queryRef;
},
};
queryFn(queryRef);
return queryArray;
}
}
4.2 사용 예제
컴포넌트에서 사용하는 방법:
import { Component, OnInit } from '@angular/core';
import { DbService } from './core/services/db.service';
@Component({
selector: 'app-users',
template: `
<div *ngFor="let user of users">
{{ user.name }} - {{ user.email }}
</div>
`,
})
export class UsersComponent implements OnInit {
users: any[] = [];
constructor(private db: DbService) {}
async ngOnInit() {
// 쿼리 없이 전체 조회
this.users = await this.db.toCollection$('users');
// 또는 쿼리와 함께 조회
this.users = await this.db.toCollection$('users', (ref) =>
ref.where('active', '==', true).orderBy('createdAt', 'desc').limit(10)
);
}
}
4.3 이메일 중복 체크 예제
회원가입 시 이메일 중복을 체크하는 경우:
async checkDuplicateEmail(email: string): Promise<boolean> {
const usedEmail = await this.db.toCollection$('members', (ref) =>
ref.where('email', '==', email).where('exitSwitch', '==', false)
);
return usedEmail.length > 0;
}
// 사용 예제
async registerUser(email: string, password: string) {
const isDuplicate = await this.checkDuplicateEmail(email);
if (isDuplicate) {
throw new Error('이미 가입된 이메일입니다');
}
// 회원가입 진행
await this.auth.registerUser(email, password);
}
5. 순차적 데이터 로딩
데이터를 순서대로 로딩해야 하는 경우 async/await를 사용하면 간단하다.
5.1 순차적 로딩 구현
// 순차적 데이터 로딩
async loadAllDataSequentially() {
try {
// 1. 멤버 데이터 로딩
await this.setMemberData();
this.loadingProgress.member = true;
console.log('멤버 데이터 로딩 완료');
// 2. 공지사항 데이터 로딩
await this.loadNoticesData();
this.loadingProgress.notices = true;
console.log('공지사항 데이터 로딩 완료');
// 3. 순위 데이터 로딩
await this.getData();
await this.getAddress();
this.rankingDataReady = true;
console.log('순위 데이터 로딩 완료!');
// 4. 모든 데이터 로딩 완료
this.loadingProgress.applications = true;
console.log('모든 데이터 로딩 완료!');
} catch (error) {
console.error('데이터 로딩 중 오류:', error);
// 오류가 발생해도 기본 데이터는 표시
this.dataReady = true;
}
}
5.2 각 단계별 구현
async setMemberData() {
const user = await this.auth.getUser();
if (user) {
this.member = await this.db.toDoc$(`members/${user.uid}`);
}
}
async loadNoticesData() {
this.notices$ = this.db.collection$('notices', (ref) =>
ref.where('active', '==', true).orderBy('createdAt', 'desc').limit(5)
);
}
6. 병렬 데이터 로딩
여러 데이터를 동시에 로딩하면 성능이 향상된다. Promise.all()을 사용한다.
6.1 Promise.all() 사용
// 병렬 데이터 로딩
async loadAllDataInParallel() {
try {
// 모든 데이터를 동시에 로딩
const [users, notices, categories] = await Promise.all([
this.db.toCollection$('users'),
this.db.toCollection$('notices', (ref) =>
ref.where('active', '==', true).limit(5)
),
this.db.toCollection$('categories'),
]);
this.users = users;
this.notices = notices;
this.categories = categories;
console.log('모든 데이터 로딩 완료!');
} catch (error) {
console.error('데이터 로딩 중 오류:', error);
}
}
6.2 Promise.allSettled() 사용
일부가 실패해도 나머지를 계속 실행하려면 Promise.allSettled()를 사용한다:
async loadDataWithErrorHandling() {
const results = await Promise.allSettled([
this.db.toCollection$('users'),
this.db.toCollection$('notices'),
this.db.toCollection$('categories'),
]);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`데이터 ${index} 로딩 성공:`, result.value);
} else {
console.error(`데이터 ${index} 로딩 실패:`, result.reason);
}
});
}
Promise.all(): 하나라도 실패하면 전체 실패Promise.allSettled(): 일부 실패해도 나머지 계속 실행
7. 에러 처리와 최적화
실제 프로덕션 환경에서는 에러 처리가 필수다.
7.1 try-catch 에러 처리
async loadUserData(userId: string) {
try {
const user = await this.db.toDoc$(`members/${userId}`);
if (!user) {
throw new Error('사용자를 찾을 수 없습니다');
}
return user;
} catch (error: any) {
console.error('사용자 데이터 로딩 실패:', error);
// 에러 타입에 따른 처리
if (error.code === 'permission-denied') {
console.error('권한이 없습니다');
} else if (error.code === 'not-found') {
console.error('데이터를 찾을 수 없습니다');
}
// 기본값 반환 또는 에러 재발생
return null;
// 또는
// throw error;
}
}
7.2 타임아웃 처리
응답이 너무 오래 걸리면 타임아웃을 설정한다:
async loadDataWithTimeout(timeoutMs: number = 5000) {
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('타임아웃')), timeoutMs)
);
try {
const data = await Promise.race([
this.db.toCollection$('users'),
timeoutPromise,
]);
return data;
} catch (error) {
console.error('데이터 로딩 타임아웃:', error);
return [];
}
}
7.3 재시도 로직
네트워크 오류 시 자동으로 재시도하는 로직:
async loadDataWithRetry(maxRetries: number = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const data = await this.db.toCollection$('users');
return data;
} catch (error) {
if (i === maxRetries - 1) {
throw error; // 마지막 시도에서 실패하면 에러 발생
}
// 재시도 전 대기
await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1)));
console.log(`재시도 ${i + 1}/${maxRetries}`);
}
}
}
7.4 성능 최적화 팁
- 병렬 로딩 활용: 독립적인 데이터는
Promise.all()로 동시에 로딩 - 필요한 데이터만 조회:
limit()와where()로 필터링 - 캐싱 전략: 자주 조회하는 데이터는 로컬에 캐싱
- 로딩 상태 표시: 사용자 경험을 위해 로딩 인디케이터 표시
마무리
async/await를 사용하면 비동기 코드를 동기 코드처럼 읽기 쉽게 작성할 수 있다. 하지만 Observable과 Promise의 차이를 이해하고, 적절한 상황에 맞는 패턴을 선택하는 것이 중요하다.
실시간 업데이트가 필요한 경우 Observable을, 한 번만 조회하는 경우 async/await를 사용하자. 순차적 로딩과 병렬 로딩을 적절히 조합하면 성능과 가독성을 모두 확보할 수 있다.
async함수는 항상 Promise를 반환await는 Promise가 완료될 때까지 대기- 순차적 로딩:
await를 순서대로 사용 - 병렬 로딩:
Promise.all()사용 - 에러 처리:
try-catch필수
'개발 > FRONT' 카테고리의 다른 글
| 휴대폰 본인인증 완벽 구현 가이드 - 아임포트로 끝내는 인증 시스템 예제 (0) | 2025.12.21 |
|---|---|
| Firebase Cloud Messaging(FCM) 완벽 설정 가이드 - 푸시 알림의 모든 것 (0) | 2025.12.21 |
| Firebase Storage 파일 업로드 완벽 마스터하기 - 실전 코드로 배우는 업로드 전략 (0) | 2025.12.21 |
| Ionic 프로젝트의 핵심 설정 파일, ionic.config.json 완벽 가이드 (0) | 2025.12.21 |
| React와 Redux로 데이터 관리하기: 기초 예제 (0) | 2024.07.10 |