Angular + Ionic + Capacitor에서 Firebase 완벽 연동 가이드
모바일 앱 개발에서 백엔드 인프라 구축은 시간과 비용이 많이 드는 작업이다. Firebase를 사용하면 인증, 데이터베이스, 스토리지, 푸시 알림 등을 빠르게 구현할 수 있다.
이 글에서는 Angular + Ionic + Capacitor 프로젝트에 Firebase를 연동하는 방법을 실제 프로덕션 환경에서 사용하는 패턴으로 설명한다.
목차
1. Firebase 프로젝트 생성
먼저 Firebase Console에 접속해서 새 프로젝트를 생성한다.
1.1 프로젝트 생성
- 1Firebase Console에서 "프로젝트 추가" 클릭
- 2프로젝트 이름 입력 (예:
[projectId]) - 3Google Analytics 설정 (선택사항)
- 4프로젝트 생성 완료
1.2 웹 앱 등록
- 1프로젝트 대시보드에서 웹 아이콘(</>) 클릭
- 2앱 닉네임 입력
- 3Firebase Hosting 설정은 선택사항
- 4Firebase SDK 설정 정보 복사 (나중에 사용)
복사한 설정 정보는 다음과 같은 형태다:
const firebaseConfig = {
apiKey: "[apiKey]",
authDomain: "[authDomain]",
projectId: "[projectId]",
storageBucket: "[storageBucket]",
messagingSenderId: "681712569882",
appId: "[appId]",
measurementId: "[measurementId]"
};
1.3 Firestore Database 활성화
- 1좌측 메뉴에서 "Firestore Database" 선택
- 2"데이터베이스 만들기" 클릭
- 3보안 규칙 모드 선택:
- 테스트 모드: 개발 중에만 사용 (모든 읽기/쓰기 허용)
- 프로덕션 모드: 실제 서비스용 (규칙 설정 필요)
- 4위치 선택 (가장 가까운 리전 선택, 예:
asia-northeast3)
1.4 Authentication 활성화
- 1좌측 메뉴에서 "Authentication" 선택
- 2"시작하기" 클릭
- 3사용할 로그인 제공업체 활성화:
- 이메일/비밀번호
- Apple
- 등등
2. 필요한 패키지 설치
Angular 프로젝트에서 Firebase를 사용하려면 @angular/fire와 firebase 패키지가 필요하다.
npm install firebase @angular/fire
버전 호환성 확인:
- Angular 16:
@angular/fire@^7.6.1,firebase@^9.23.0 - Angular 17+:
@angular/fire@^17.x,firebase@^10.x
3. 환경 변수 설정
Firebase 설정 정보는 환경 변수로 관리하는 것이 좋다. Angular는 environment.ts 파일을 사용한다.
3.1 개발 환경 설정
src/environments/environment.ts 파일을 생성한다:
export const environment = {
production: false,
firebaseConfig: {
apiKey: '[apiKey]',
authDomain: '[authDomain]',
projectId: '[projectId]',
storageBucket: '[storageBucket]',
messagingSenderId: '681712569882',
appId: '[appId]',
measurementId: '[measurementId]',
},
};
3.2 프로덕션 환경 설정
src/environments/environment.prod.ts 파일을 생성한다:
export const environment = {
production: true,
firebaseConfig: {
apiKey: '[apiKey]',
authDomain: '[authDomain]',
projectId: '[projectId]',
storageBucket: '[storageBucket]',
messagingSenderId: '681712569882',
appId: '[appId]',
measurementId: '[measurementId]',
},
};
- 실제 프로젝트에서는
apiKey를 환경 변수나 CI/CD 시크릿으로 관리한다 - Git에 커밋할 때는
.gitignore에 환경 파일을 추가하거나, 실제 키 값은 제거한다
4. Firebase 초기화
Angular의 @angular/fire는 모듈 방식으로 Firebase를 초기화한다. app.module.ts에서 설정한다.
4.1 AppModule 설정
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { RouteReuseStrategy } from '@angular/router';
// Firebase imports
import { environment } from 'src/environments/environment';
import {
FirestoreModule,
getFirestore,
provideFirestore,
} from '@angular/fire/firestore';
import {
FirebaseAppModule,
getApp,
initializeApp,
provideFirebaseApp,
} from '@angular/fire/app';
import {
getAuth,
indexedDBLocalPersistence,
initializeAuth,
provideAuth,
} from '@angular/fire/auth';
import { provideStorage, getStorage } from '@angular/fire/storage';
import { provideFunctions, getFunctions } from '@angular/fire/functions';
import { Capacitor } from '@capacitor/core';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
IonicModule.forRoot({ mode: 'ios' }),
FirestoreModule,
FirebaseAppModule,
// Firebase 초기화
provideFirebaseApp(() => initializeApp(environment.firebaseConfig)),
// Authentication 초기화 (네이티브 플랫폼 대응)
provideAuth(() => {
if (Capacitor.isNativePlatform()) {
// iOS/Android에서는 indexedDB 사용
return initializeAuth(getApp(), {
persistence: indexedDBLocalPersistence,
});
} else {
// 웹에서는 기본 Auth 사용
return getAuth();
}
}),
// Firestore 초기화
provideFirestore(() => getFirestore()),
// Storage 초기화
provideStorage(() => getStorage()),
// Functions 초기화
provideFunctions(() => getFunctions()),
],
providers: [
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
],
bootstrap: [AppComponent],
})
export class AppModule {}
4.2 Capacitor 네이티브 플랫폼 대응
Ionic + Capacitor로 iOS/Android 앱을 빌드할 때는 Authentication의 persistence 설정이 중요하다. 웹에서는 localStorage를 사용하지만, 네이티브 앱에서는 indexedDBLocalPersistence를 사용해야 한다.
5. Firestore 서비스 구현
Firestore를 사용하기 위한 서비스 클래스를 만든다. 이 서비스는 CRUD 작업과 쿼리를 추상화한다.
5.1 DbService 기본 구조
import { Injectable } from '@angular/core';
import {
Firestore,
collection,
collectionData,
doc,
docData,
addDoc,
deleteDoc,
query,
where,
orderBy,
limit,
getDoc,
getDocs,
setDoc,
} from '@angular/fire/firestore';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class DbService {
constructor(public firestore: Firestore) {}
// 컬렉션 전체 조회 (Observable)
collection$(collectionName: string, queryFn?: Function): Observable<any[]> {
const collectionRef = collection(this.firestore, collectionName);
if (queryFn) {
const queryConstraints = this.parseQuery(queryFn);
const q = query(collectionRef, ...queryConstraints);
return collectionData(q, { idField: 'id' });
}
return collectionData(collectionRef, { idField: 'id' });
}
// 단일 문서 조회 (Observable)
doc$(path: string): Observable<any> {
const docRef = doc(this.firestore, path);
return docData(docRef, { idField: 'id' });
}
// 컬렉션에 문서 추가
async add(collectionName: string, data: any): Promise<string> {
const collectionRef = collection(this.firestore, collectionName);
const docRef = await addDoc(collectionRef, data);
return docRef.id;
}
// 문서 업데이트
async updateAt(path: string, data: any): Promise<void> {
const docRef = doc(this.firestore, path);
await setDoc(docRef, data, { merge: true });
}
// 문서 삭제
async delete(path: string): Promise<void> {
const docRef = doc(this.firestore, path);
await deleteDoc(docRef);
}
// 쿼리 파싱 헬퍼
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;
}
// Promise 기반 컬렉션 조회 (한 번만 실행)
async toCollection$(collectionName: string, queryFn?: Function): Promise<any[]> {
const collectionRef = collection(this.firestore, collectionName);
const result = [];
if (queryFn) {
const queryConstraints = this.parseQuery(queryFn);
const q = query(collectionRef, ...queryConstraints);
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
result.push({ id: doc.id, ...doc.data() });
});
} else {
const querySnapshot = await getDocs(collectionRef);
querySnapshot.forEach((doc) => {
result.push({ id: doc.id, ...doc.data() });
});
}
return result;
}
}
5.2 실제 사용 예제
import { Component, OnInit } from '@angular/core';
import { DbService } from './core/services/db.service';
import { Observable } from 'rxjs';
interface User {
id: string;
name: string;
email: string;
createdAt: Date;
}
@Component({
selector: 'app-users',
template: `
<ion-list>
<ion-item *ngFor="let user of users$ | async">
{{ user.name }} - {{ user.email }}
</ion-item>
</ion-list>
`,
})
export class UsersPage implements OnInit {
users$: Observable<User[]>;
constructor(private db: DbService) {}
ngOnInit() {
// 실시간 조회 (변경사항 자동 반영)
this.users$ = this.db.collection$('users', (ref) =>
ref.where('active', '==', true).orderBy('createdAt', 'desc').limit(10)
);
}
async addUser() {
await this.db.add('users', {
name: '홍길동',
email: 'hong@example.com',
active: true,
createdAt: new Date(),
});
}
async updateUser(userId: string) {
await this.db.updateAt(`users/${userId}`, {
name: '홍길동 수정',
});
}
async deleteUser(userId: string) {
await this.db.delete(`users/${userId}`);
}
}
5.3 문서 조인 (Join) 구현
Firestore는 NoSQL이라 JOIN이 없지만, 수동으로 문서를 조인할 수 있다:
// leftJoinDocument 헬퍼 함수
export const leftJoinDocument =
(firestore: Firestore, field: string, collection: string) => (source: Observable<any[]>) =>
defer(() => {
let collectionData: any[];
const cache = new Map();
return source.pipe(
switchMap((data) => {
cache.clear();
collectionData = data;
const reads$ = [];
let i = 0;
for (const dData of collectionData) {
if (!dData[field] || cache.get(dData[field])) {
continue;
}
reads$.push(
docData(doc(firestore, `${collection}/${dData[field]}`), {
idField: 'id',
})
);
cache.set(dData[field], i);
i++;
}
return reads$.length ? combineLatest(reads$) : of([]);
}),
map((joins) =>
collectionData.map((v) => {
const joinIdx = cache.get(v[field]);
return { ...v, [field]: joins[joinIdx] || null };
})
)
);
});
// 사용 예제
this.posts$ = this.db.collection$('posts').pipe(
leftJoinDocument(this.db.firestore, 'userId', 'users')
);
6. Authentication 사용
Firebase Authentication을 사용해서 이메일/비밀번호, 소셜 로그인을 구현한다.
6.1 AuthService 구현
import { Injectable } from '@angular/core';
import { Auth, createUserWithEmailAndPassword, signInWithEmailAndPassword, signOut, User } from '@angular/fire/auth';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class AuthService {
user$: Observable<User | null>;
constructor(private auth: Auth) {
// 사용자 상태를 Observable로 제공
this.user$ = new Observable((observer) => {
this.auth.onAuthStateChanged((user) => {
observer.next(user);
});
});
}
// 회원가입
async registerUser(email: string, password: string) {
try {
const userCredential = await createUserWithEmailAndPassword(
this.auth,
email,
password
);
return userCredential.user;
} catch (error) {
console.error('회원가입 실패:', error);
throw error;
}
}
// 로그인
async loginUser(email: string, password: string) {
try {
const userCredential = await signInWithEmailAndPassword(
this.auth,
email,
password
);
return userCredential.user;
} catch (error) {
console.error('로그인 실패:', error);
throw error;
}
}
// 로그아웃
async logout() {
await signOut(this.auth);
}
// 현재 사용자 가져오기 (Promise)
async getUser(): Promise<User | null> {
return new Promise((resolve) => {
this.auth.onAuthStateChanged((user) => {
resolve(user);
});
});
}
}
6.2 사용 예제
import { Component } from '@angular/core';
import { AuthService } from './core/services/auth.service';
import { DbService } from './core/services/db.service';
@Component({
selector: 'app-login',
template: `
<ion-input [(ngModel)]="email" placeholder="이메일"></ion-input>
<ion-input [(ngModel)]="password" type="password" placeholder="비밀번호"></ion-input>
<ion-button (click)="login()">로그인</ion-button>
`,
})
export class LoginPage {
email = '';
password = '';
constructor(
private auth: AuthService,
private db: DbService
) {}
async login() {
try {
const user = await this.auth.loginUser(this.email, this.password);
console.log('로그인 성공:', user);
// Firestore에 사용자 정보 저장
await this.db.updateAt(`members/${user.uid}`, {
email: user.email,
lastLoginAt: new Date(),
});
} catch (error) {
console.error('로그인 실패:', error);
}
}
}
7. Storage 사용
이미지나 파일을 업로드할 때 Firebase Storage를 사용한다.
7.1 StorageService 구현
import { Injectable } from '@angular/core';
import { Storage, ref, uploadBytes, getDownloadURL, deleteObject } from '@angular/fire/storage';
@Injectable({
providedIn: 'root',
})
export class StorageService {
constructor(private storage: Storage) {}
// 파일 업로드
async uploadFile(path: string, file: File): Promise<string> {
const storageRef = ref(this.storage, path);
await uploadBytes(storageRef, file);
const downloadURL = await getDownloadURL(storageRef);
return downloadURL;
}
// 파일 삭제
async deleteFile(path: string): Promise<void> {
const storageRef = ref(this.storage, path);
await deleteObject(storageRef);
}
}
7.2 사용 예제
import { Component } from '@angular/core';
import { StorageService } from './core/services/storage.service';
@Component({
selector: 'app-upload',
template: `
<input type="file" (change)="onFileSelected($event)" accept="image/*">
<ion-button (click)="upload()">업로드</ion-button>
`,
})
export class UploadPage {
selectedFile: File | null = null;
constructor(private storage: StorageService) {}
onFileSelected(event: any) {
this.selectedFile = event.target.files[0];
}
async upload() {
if (!this.selectedFile) return;
const path = `images/${Date.now()}_${this.selectedFile.name}`;
const downloadURL = await this.storage.uploadFile(path, this.selectedFile);
console.log('업로드 완료:', downloadURL);
}
}
8. 실전 팁과 주의사항
8.1 Firestore 보안 규칙
프로덕션 환경에서는 반드시 보안 규칙을 설정한다:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// 사용자 자신의 데이터만 읽기/쓰기 가능
match /members/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
// 공개 데이터는 읽기만 가능
match /public/{document=**} {
allow read: if true;
allow write: if request.auth != null;
}
}
}
8.2 인덱스 설정
복합 쿼리를 사용할 때는 Firestore 인덱스가 필요하다. 콘솔에서 자동으로 생성하거나 firestore.indexes.json 파일로 관리한다:
{
"indexes": [
{
"collectionGroup": "applications",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "cityAddress", "order": "ASCENDING" },
{ "fieldPath": "dateCreated", "order": "DESCENDING" }
]
}
]
}
8.3 네트워크 오프라인 대응
Firestore는 기본적으로 오프라인 캐싱을 지원한다. 하지만 명시적으로 설정할 수 있다:
import { enableIndexedDbPersistence } from '@angular/fire/firestore';
enableIndexedDbPersistence(getFirestore())
.catch((err) => {
if (err.code == 'failed-precondition') {
console.warn('다중 탭에서 오프라인 지속성 사용 불가');
} else if (err.code == 'unimplemented') {
console.warn('브라우저가 오프라인 지속성을 지원하지 않음');
}
});
8.4 에러 처리
Firebase 에러는 코드로 구분할 수 있다:
import { FirebaseError } from '@angular/fire/app';
try {
await this.auth.loginUser(email, password);
} catch (error) {
if (error instanceof FirebaseError) {
switch (error.code) {
case 'auth/user-not-found':
console.error('사용자를 찾을 수 없습니다');
break;
case 'auth/wrong-password':
console.error('비밀번호가 잘못되었습니다');
break;
case 'auth/too-many-requests':
console.error('너무 많은 요청입니다. 잠시 후 다시 시도하세요');
break;
default:
console.error('알 수 없는 오류:', error.message);
}
}
}
8.5 성능 최적화
- 필요한 필드만 조회:
select()사용 (현재 버전에서는 제한적) - 페이지네이션:
limit()와startAfter()사용 - 캐싱 전략: 자주 조회하는 데이터는 로컬에 캐싱
- 배치 작업: 여러 문서를 한 번에 처리할 때
batch()사용
import { writeBatch, doc } from '@angular/fire/firestore';
async batchUpdate() {
const batch = writeBatch(this.firestore);
batch.update(doc(this.firestore, 'users/user1'), { name: '홍길동' });
batch.update(doc(this.firestore, 'users/user2'), { name: '김철수' });
await batch.commit();
}
마무리
이 가이드에서는 Angular + Ionic + Capacitor 프로젝트에 Firebase를 연동하는 방법을 설명했다. 실제 프로덕션 환경에서 사용하는 패턴과 주의사항을 포함했다.
Firebase는 빠르게 프로토타입을 만들 수 있게 해주지만, 규모가 커지면 비용과 성능을 고려해야 한다. Firestore의 읽기/쓰기 비용, Storage의 저장 용량, Functions의 실행 시간 등을 모니터링하면서 최적화하는 것이 중요하다.
추가로 궁금한 점이 있으면 Firebase 공식 문서나 커뮤니티를 참고하자.
'개발 > BACK' 카테고리의 다른 글
| WEB-INF 정적 리소스 경로 찾기 - 로컬환경과 배포환경의 차이점 (0) | 2025.12.24 |
|---|---|
| Linux vi / vim 명령어 총 정리 (0) | 2025.12.24 |
| Spring Boot 환경에서 "Java Error Occurred During Initialization of Boot Layer" 에러 해결 방법 (0) | 2024.07.12 |
| 리눅스에서 "Error Occurred During Initialization of VM" 에러 해결 방법 (0) | 2024.07.12 |
| MySQL 내장 함수를 통해 비밀번호 암호화 하기 (0) | 2024.07.12 |