개요
Firebase Auth만 사용하는 이유
Firebase Authentication만 사용한다면 정적 페이지로 충분합니다!
- ✅ 정적 페이지로 가능한 것: 로그인/로그아웃, 사용자 기본 정보 (이메일, 이름, 프로필 사진)
- ❌ 동적 페이지가 필요한 경우: 사용자 추가 정보 저장, 복잡한 비즈니스 로직, 서버사이드 검증
언제 동적 페이지를 고려해야 할까요?
동적 페이지 (서버 필요) 고려 시점:
- 📊 Firestore/Database: 사용자의 추가 정보, 앱 데이터 저장
- 🔐 서버사이드 검증: 민감한 데이터 처리, 관리자 권한 확인
- 🤖 복잡한 로직: 결제 처리, 외부 API 연동, 데이터 분석
- 📧 백그라운드 작업: 이메일 발송, 배치 처리, 스케줄링
현재 가이드 범위: Firebase Auth만 사용하는 정적 페이지 구현
1단계: Firebase 프로젝트 생성 및 기본 설정
1.1 Firebase 프로젝트 생성
- Firebase Console 접속
- "프로젝트 추가" 클릭
- 프로젝트 정보 입력:
프로젝트 이름: bookplay-production
프로젝트 ID: bookplay-production-[자동생성번호]
프로젝트 이름: bookplay-production
프로젝트 ID: bookplay-production
프로젝트 번호: 902982228800
웹 API 키: 이 프로젝트에 웹 API 키가 없습니다.
- Google Analytics 설정: 필요에 따라 선택 (Auth만 사용시 불필요)
1.2 프로젝트 정보 확인
생성 완료 후 기록할 정보:
프로젝트 ID: bookplay-production-xxxxx
웹 API 키: AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXX
Auth 도메인: bookplay-production-xxxxx.firebaseapp.com
1.3 필요한 API 활성화 (GCP Console)
- Google Cloud Console 접속
- 프로젝트 선택: 생성한 Firebase 프로젝트
- APIs & Services → 라이브러리에서 다음 API 활성화:
✅ Firebase Authentication API
✅ Identity and Access Management (IAM) API
API 활성화느
2단계: Firebase Authentication 설정
2.1 Authentication 서비스 활성화
- Authentication → 시작하기
- Sign-in method 탭으로 이동
- Google 제공업체 설정:
✅ 사용 설정
프로젝트 지원 이메일: your-email@gmail.com
프로젝트 공개용 이름: BookPlay Production
- 저장 클릭
2.2 웹 앱 등록
- 프로젝트 개요 → 앱 추가 → 웹
- 앱 정보 입력:
앱 닉네임: BookPlay Web App
✅ 이 앱의 Firebase Hosting도 설정합니다 (체크)
- 앱 등록 후 설정 정보 복사 및 저장
2.3 승인된 도메인 설정
- Authentication → 설정 → 승인된 도메인
- 기본 설정 확인:
✅ localhost (로컬 개발용)
✅ bookplay-production-xxxxx.firebaseapp.com (Firebase Hosting)
✅ bookplay-production-xxxxx.web.app (Firebase Hosting)
3단계: 로컬 테스트를 위한 정적 구성
3.1 프로젝트 구조 생성
# 프로젝트 폴더 생성
mkdir my-auth-app
cd my-auth-app
# 폴더 구조 생성
mkdir public
mkdir public/js
mkdir public/css
mkdir public/assets
최종 구조:
my-auth-app/
├── public/
│ ├── index.html
│ ├── login.html
│ ├── dashboard.html
│ ├── js/
│ │ ├── firebase-config.js
│ │ ├── auth.js
│ │ └── app.js
│ ├── css/
│ │ └── style.css
│ └── assets/
└── firebase.json (나중에 생성)
3.2 Firebase 설정 파일
// public/js/firebase-config.js
import { initializeApp } from 'https://www.gstatic.com/firebasejs/10.0.0/firebase-app.js';
import { getAuth } from 'https://www.gstatic.com/firebasejs/10.0.0/firebase-auth.js';
const firebaseConfig = {
apiKey: "your-api-key",
authDomain: "your-project.firebaseapp.com",
projectId: "your-project-id",
storageBucket: "your-project.appspot.com",
messagingSenderId: "123456789",
appId: "your-app-id"
};
export const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
3.3 인증 로직 구현
// public/js/auth.js
import { auth } from './firebase-config.js';
import {
GoogleAuthProvider,
signInWithPopup,
signOut,
onAuthStateChanged
} from 'https://www.gstatic.com/firebasejs/10.0.0/firebase-auth.js';
const googleProvider = new GoogleAuthProvider();
// Google 로그인
export const signInWithGoogle = async () => {
try {
const result = await signInWithPopup(auth, googleProvider);
console.log('로그인 성공:', result.user);
return result.user;
} catch (error) {
console.error('로그인 실패:', error);
throw error;
}
};
// 로그아웃
export const logout = async () => {
try {
await signOut(auth);
console.log('로그아웃 성공');
} catch (error) {
console.error('로그아웃 실패:', error);
throw error;
}
};
// 인증 상태 변화 감지
export const onAuthChange = (callback) => {
return onAuthStateChanged(auth, callback);
};
// 현재 사용자 정보 가져오기
export const getCurrentUser = () => {
return auth.currentUser;
};
3.4 메인 앱 로직
// public/js/app.js
import { signInWithGoogle, logout, onAuthChange, getCurrentUser } from './auth.js';
// DOM 요소
const loginBtn = document.getElementById('login-btn');
const logoutBtn = document.getElementById('logout-btn');
const userInfo = document.getElementById('user-info');
const loginSection = document.getElementById('login-section');
const userSection = document.getElementById('user-section');
// 페이지 로드 시 초기화
document.addEventListener('DOMContentLoaded', () => {
initApp();
});
function initApp() {
// 이벤트 리스너 등록
loginBtn?.addEventListener('click', handleLogin);
logoutBtn?.addEventListener('click', handleLogout);
// 인증 상태 변화 감지
onAuthChange((user) => {
if (user) {
showUserSection(user);
} else {
showLoginSection();
}
});
}
// 로그인 처리
async function handleLogin() {
try {
loginBtn.disabled = true;
loginBtn.textContent = '로그인 중...';
const user = await signInWithGoogle();
console.log('로그인 성공:', user);
} catch (error) {
alert('로그인에 실패했습니다: ' + error.message);
console.error('로그인 에러:', error);
} finally {
loginBtn.disabled = false;
loginBtn.textContent = 'Google로 로그인';
}
}
// 로그아웃 처리
async function handleLogout() {
try {
logoutBtn.disabled = true;
logoutBtn.textContent = '로그아웃 중...';
await logout();
console.log('로그아웃 성공');
} catch (error) {
alert('로그아웃에 실패했습니다: ' + error.message);
console.error('로그아웃 에러:', error);
} finally {
logoutBtn.disabled = false;
logoutBtn.textContent = '로그아웃';
}
}
// 사용자 섹션 표시
function showUserSection(user) {
if (loginSection) loginSection.style.display = 'none';
if (userSection) userSection.style.display = 'block';
if (userInfo) {
userInfo.innerHTML = `
<div class="user-profile">
<img src="${user.photoURL || '/assets/default-avatar.png'}"
alt="프로필" class="profile-img">
<div class="user-details">
<h2>환영합니다, ${user.displayName || '사용자'}님!</h2>
<p class="user-email">${user.email}</p>
<p class="user-id">UID: ${user.uid}</p>
</div>
</div>
`;
}
}
// 로그인 섹션 표시
function showLoginSection() {
if (loginSection) loginSection.style.display = 'block';
if (userSection) userSection.style.display = 'none';
}
3.5 HTML 파일
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Firebase Auth Test</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="container">
<header>
<h1>🔥 Firebase Auth 테스트</h1>
</header>
<div id="login-section" class="auth-section">
<h2>로그인이 필요합니다</h2>
<p>Google 계정으로 로그인해주세요.</p>
<button id="login-btn" class="btn btn-google">
<span>🔑 Google로 로그인</span>
</button>
</div>
<div id="user-section" class="auth-section" style="display: none;">
<div id="user-info"></div>
<div class="actions">
<button id="logout-btn" class="btn btn-secondary">로그아웃</button>
</div>
</div>
</div>
<script type="module" src="js/app.js"></script>
</body>
</html>
3.6 스타일 파일
/* public/css/style.css */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
background: white;
border-radius: 20px;
padding: 40px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
max-width: 500px;
width: 90%;
text-align: center;
}
header h1 {
color: #333;
margin-bottom: 30px;
font-size: 2rem;
}
.auth-section {
padding: 20px 0;
}
.auth-section h2 {
color: #555;
margin-bottom: 15px;
}
.auth-section p {
color: #666;
margin-bottom: 25px;
line-height: 1.6;
}
.btn {
padding: 15px 30px;
border: none;
border-radius: 50px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 200px;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-google {
background: #4285f4;
color: white;
}
.btn-google:hover:not(:disabled) {
background: #3367d6;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(66, 133, 244, 0.3);
}
.btn-secondary {
background: #6c757d;
color: white;
margin-top: 20px;
}
.btn-secondary:hover:not(:disabled) {
background: #545b62;
transform: translateY(-2px);
}
.user-profile {
display: flex;
align-items: center;
text-align: left;
background: #f8f9fa;
padding: 20px;
border-radius: 15px;
margin-bottom: 20px;
}
.profile-img {
width: 80px;
height: 80px;
border-radius: 50%;
margin-right: 20px;
border: 3px solid #4285f4;
}
.user-details h2 {
color: #333;
margin-bottom: 5px;
font-size: 1.4rem;
}
.user-email {
color: #666;
font-size: 1rem;
margin-bottom: 5px;
}
.user-id {
color: #999;
font-size: 0.8rem;
font-family: monospace;
}
.actions {
text-align: center;
}
@media (max-width: 600px) {
.container {
padding: 30px 20px;
}
.user-profile {
flex-direction: column;
text-align: center;
}
.profile-img {
margin-right: 0;
margin-bottom: 15px;
}
}
3.7 로컬 테스트 실행
# 간단한 HTTP 서버 실행 (여러 방법 중 선택)
# 방법 1: Python (가장 간단)
cd public
python -m http.server 8000
# 브라우저에서 http://localhost:8000 접속
# 방법 2: Node.js (npx 사용)
cd public
npx serve -s . -p 8000
# 방법 3: Live Server (VS Code 확장)
# VS Code에서 index.html 우클릭 → "Open with Live Server"
테스트 체크리스트:
4단계: GCP에 정적 배포
4.1 Firebase CLI 설치 및 로그인
# Firebase CLI 설치
npm install -g firebase-tools
# Firebase 로그인
firebase login
# 로그인 확인
firebase projects:list
4.2 Firebase 프로젝트 초기화
# 프로젝트 루트 폴더에서 실행
firebase init hosting
# 설정 선택
? Select a default Firebase project: [생성한 프로젝트 선택]
? What do you want to use as your public directory? public
? Configure as a single-page app (rewrite all urls to /index.html)? Yes
? Set up automatic builds and deploys with GitHub? No
? File public/index.html already exists. Overwrite? No
4.3 Firebase 설정 파일
{
"hosting": {
"public": "public",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [ {
"source": "**",
"destination": "/index.html"
} ],
"headers": [ {
"source": "**/*.@(eot|otf|ttf|ttc|woff|font.css)",
"headers": [ {
"key": "Access-Control-Allow-Origin",
"value": "*"
} ]
}, {
"source": "**/*.@(js|css)",
"headers": [ {
"key": "Cache-Control",
"value": "max-age=604800"
} ]
} ]
}
}
4.4 배포 실행
# 배포 전 로컬 프리뷰 (선택사항)
firebase serve --only hosting
# 실제 배포
firebase deploy --only hosting
# 배포 완료 후 표시되는 URL들:
# ✔ Deploy complete!
# Project Console: https://console.firebase.google.com/project/your-project/overview
# Hosting URL: https://your-project.web.app
4.5 배포 테스트
배포된 URL에서 다음 사항 확인:
4.6 배포 URL 관리
# 현재 배포된 사이트 정보 확인
firebase hosting:sites:list
# 커스텀 도메인 추가 (선택사항)
firebase hosting:sites:create your-custom-domain
# 배포 히스토리 확인
firebase hosting:clone your-project:source your-project:dest