2. Firebase 설정하는 법
2025년 10월 5일
이제 블로그 기능을 구현하고자 합니다.
보통 포트폴리오 사이트는 정적페이지로 배포하며, 게시글은 md 파일을 만들며 커밋하고 배포하는 식으로 하면 쉽게 구현할 수 있고, 서버리스라 문제가 될 요소들도 없을 것입니다.
하지만 저는 파이어베이스를 연결하여 제 아이디 비밀번호로만 admin페이지 접근이 가능하게 하고 이를 기반으로 실제 블로그 사이트들처럼 글들을 관리하고 작성할 수 있는 시스템을 만들고자 합니다.
Next.js 포트폴리오에 Firebase 연결하기: 백엔드 구축과 동적 렌더링 전환
1. Firebase 연결 배경
포트폴리오를 Next.js로 전환한 후, 제가 생각하는 블로그 기능을 추가하기 위해 백엔드가 필요했습니다. 별도 서버 구축 대신 Firebase를 선택한 이유:
- 빠른 개발: 서버 구축 없이 바로 시작
- 실시간 데이터베이스: Firestore로 콘텐츠 관리
- 파일 저장소: Storage로 이미지 관리
- 인증 시스템: Authentication으로 관리자 인증
- 무료 티어: 초기 비용 부담 적음
- 백엔드 구현 실력이 없음(가장 중요;;)
2. Firebase 프로젝트 설정
2.1 Firebase 프로젝트 생성
- Firebase Console 접속
- "프로젝트 추가" 클릭
- 프로젝트 이름 입력 (예:
dowankim-portfolio) - Google Analytics 설정 (선택사항입니다)
- 프로젝트 생성 완료
2.2 웹 앱 추가
- Firebase 프로젝트 대시보드에서 "웹" 아이콘 클릭
- 앱 닉네임 입력
- Firebase Hosting 설정 (선택사항, 이 프로젝트에서는 사용 안 함)
- Firebase SDK 설정 정보 복사
복사된 설정 정보 예시:
const firebaseConfig = { apiKey: "AIzaSy...", authDomain: "dowankim-portfolio.firebaseapp.com", projectId: "dowankim-portfolio", storageBucket: "dowankim-portfolio.appspot.com", messagingSenderId: "123456789", appId: "1:123456789:web:abcdef" }
2.3 Firebase 서비스 활성화
Firestore Database
- 왼쪽 메뉴에서 "Firestore Database" 선택
- "데이터베이스 만들기" 클릭
- 프로덕션 모드 선택 (보안 규칙은 나중에 설정)
- 위치 선택 (예:
asia-northeast3- 서울)
Storage
- 왼쪽 메뉴에서 "Storage" 선택
- "시작하기" 클릭
- 보안 규칙은 나중에 설정
- 위치 선택 (Firestore와 동일하게)
Authentication
- 왼쪽 메뉴에서 "Authentication" 선택
- "시작하기" 클릭
- "이메일/비밀번호" 로그인 방법 활성화
- 관리자 계정 생성 (내가 쓸 아이디 비번 추가해놓기)
3. Next.js 프로젝트에 Firebase 설치
3.1 Firebase SDK 설치
npm install firebase
3.2 프로젝트 구조 생성
lib/
├── firebase.ts # Firebase 초기화
├── auth.ts # 인증 관련 함수
└── blog.ts # 블로그 관련 Firestore 함수
types/
└── blog.ts # 타입 정의
4. Firebase 초기화 설정
4.1 환경 변수 설정
프로젝트 루트에 .env.local 파일 생성:
NEXT_PUBLIC_FIREBASE_API_KEY=your_api_key NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your_auth_domain NEXT_PUBLIC_FIREBASE_PROJECT_ID=your_project_id NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=your_storage_bucket NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=your_messaging_sender_id NEXT_PUBLIC_FIREBASE_APP_ID=your_app_id
주의사항:
NEXT_PUBLIC_접두사 필수 (클라이언트에서 접근 가능).env.local은.gitignore에 포함되어 있어야 함- 실제 값은 Firebase Console에서 복사한 값으로 대체
4.2 Firebase 초기화 파일 생성
lib/firebase.ts:
import { initializeApp, getApps, FirebaseApp } from 'firebase/app' import { getAuth, Auth } from 'firebase/auth' import { getFirestore, Firestore } from 'firebase/firestore' import { getStorage, FirebaseStorage } from 'firebase/storage' const firebaseConfig = { apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET, messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, } // Firebase 초기화 (이미 초기화되어 있으면 재초기화 방지) let app: FirebaseApp if (getApps().length === 0) { app = initializeApp(firebaseConfig) } else { app = getApps()[0] } // 서비스 인스턴스 생성 export const auth: Auth = getAuth(app) export const db: Firestore = getFirestore(app) export const storage: FirebaseStorage = getStorage(app) export default app
핵심 포인트:
- 환경 변수 사용:
process.env.NEXT_PUBLIC_*로 설정값 읽기 - 중복 초기화 방지:
getApps()로 이미 초기화된 앱이 있으면 재사용- 개발 환경에서 Hot Module Replacement 시 에러 방지
- 프로덕션에서도 안정적으로 작동
4.3 타입 정의
types/blog.ts:
import { Timestamp } from 'firebase/firestore' export interface BlogPost { id?: string title: string content: string // 마크다운 텍스트 images: string[] // Storage URL 배열 createdAt: Timestamp | Date updatedAt: Timestamp | Date author: string // 사용자 UID tags?: string[] published: boolean slug?: string // URL 친화적인 제목 } export interface Project { id?: string tag: string // 태그 이름 (프로젝트 이름) description: string // 프로젝트 소개글 createdAt: Timestamp | Date updatedAt: Timestamp | Date }
5. 기본 Firestore 함수 구현
5.1 블로그 포스트 관련 함수
lib/blog.ts의 기본 구조:
import { collection, doc, getDocs, getDoc, addDoc, updateDoc, deleteDoc, query, orderBy, where, Timestamp } from 'firebase/firestore' import { ref, uploadBytes, getDownloadURL } from 'firebase/storage' import { db, storage } from './firebase' import { BlogPost, Project } from '@/types/blog' const POSTS_COLLECTION = 'posts' const PROJECTS_COLLECTION = 'projects' // 모든 포스트 가져오기 (공개된 것만) export const getPublishedPosts = async (): Promise<BlogPost[]> => { const q = query( collection(db, POSTS_COLLECTION), where('published', '==', true), orderBy('createdAt', 'desc') ) const snapshot = await getDocs(q) return snapshot.docs.map(doc => ({ id: doc.id, ...doc.data(), createdAt: doc.data().createdAt.toDate(), updatedAt: doc.data().updatedAt.toDate(), })) as BlogPost[] } // 모든 포스트 가져오기 (관리자용) export const getAllPosts = async (): Promise<BlogPost[]> => { try { const q = query( collection(db, POSTS_COLLECTION), orderBy('createdAt', 'desc') ) const snapshot = await getDocs(q) return snapshot.docs.map(doc => ({ id: doc.id, ...doc.data(), createdAt: doc.data().createdAt.toDate(), updatedAt: doc.data().updatedAt.toDate(), })) as BlogPost[] } catch (error) { console.error('포스트 가져오기 실패:', error) return [] } } // 단일 포스트 가져오기 export const getPost = async (id: string): Promise<BlogPost | null> => { const docRef = doc(db, POSTS_COLLECTION, id) const docSnap = await getDoc(docRef) if (!docSnap.exists()) { return null } return { id: docSnap.id, ...docSnap.data(), createdAt: docSnap.data().createdAt.toDate(), updatedAt: docSnap.data().updatedAt.toDate(), } as BlogPost } // 이미지 업로드 export const uploadImage = async (file: File, postId: string): Promise<string> => { const fileName = `${postId}/${Date.now()}_${file.name}` const storageRef = ref(storage, `blog-images/${fileName}`) await uploadBytes(storageRef, file) return await getDownloadURL(storageRef) } // 포스트 생성 export const createPost = async ( postData: Omit<BlogPost, 'id' | 'createdAt' | 'updatedAt'> ): Promise<string> => { const now = Timestamp.now() const docRef = await addDoc(collection(db, POSTS_COLLECTION), { ...postData, createdAt: now, updatedAt: now, }) return docRef.id }
5.2 인증 관련 함수
lib/auth.ts:
import { signInWithEmailAndPassword, signOut, User, onAuthStateChanged } from 'firebase/auth' import { auth } from './firebase' type AuthResult = { user: User | null error: string | null } const getErrorMessage = (error: unknown): string => { if (error instanceof Error) { return error.message } return '알 수 없는 오류가 발생했습니다.' } export const login = async (email: string, password: string): Promise<AuthResult> => { try { const userCredential = await signInWithEmailAndPassword(auth, email, password) return { user: userCredential.user, error: null } } catch (error: unknown) { return { user: null, error: getErrorMessage(error) } } } export const logout = async (): Promise<{ error: string | null }> => { try { await signOut(auth) return { error: null } } catch (error: unknown) { return { error: getErrorMessage(error) } } } export const getCurrentUser = (): Promise<User | null> => { return new Promise((resolve) => { const unsubscribe = onAuthStateChanged(auth, (user) => { unsubscribe() resolve(user) }) }) }
6. Firebase 보안 규칙 설정
6.1 Firestore 보안 규칙
Firebase Console → Firestore Database → 규칙:
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { // posts 컬렉션 규칙 match /posts/{postId} { // 공개된 포스트는 모든 사용자가 읽기 가능 allow read: if resource.data.published == true; // 인증된 사용자는 모든 포스트 읽기 가능 (어드민용) allow read: if request.auth != null; // 인증된 사용자만 쓰기 가능 allow write: if request.auth != null; } // projects 컬렉션 규칙 match /projects/{projectId} { // 모든 사용자가 읽기 가능 allow read: if true; // 인증된 사용자만 쓰기 가능 allow write: if request.auth != null; } } }
6.2 Storage 보안 규칙
Firebase Console → Storage → 규칙:
rules_version = '2'; service firebase.storage { match /b/{bucket}/o { // blog-images 경로의 이미지 match /blog-images/{allPaths=**} { // 모든 사용자가 읽기 가능 allow read: if true; // 인증된 사용자만 쓰기 가능 allow write: if request.auth != null; } } }
7. 정적 사이트에서 동적 페이지로 전환
7.1 문제 상황
Firebase를 연결한 후, 블로그 기능을 추가하려 했지만 문제가 발생했습니다:
- Next.js가 기본적으로 정적 사이트 생성(SSG) 모드로 빌드됨
- Firebase 데이터는 런타임에 가져와야 하는데, 빌드 시점에 모든 페이지를 생성하려고 시도
- 동적 라우트(
/blog/[tag],/blog/[tag]/[slug])가 제대로 작동하지 않음
7.2 원인 분석
next.config.js에 output: 'export' 설정이 있으면:
- 모든 페이지를 정적 HTML 파일로 생성
- 서버 사이드 렌더링 불가
- 동적 데이터를 빌드 시점에만 가져올 수 있음
기존 설정 (문제):
const nextConfig = { output: 'export', // ← 이 설정이 문제였음 reactStrictMode: true, }
7.3 해결 방법
1단계: next.config.js 수정
next.config.js:
/** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, images: { unoptimized: true, }, // output: 'export' 제거 ← 이 줄 삭제! } module.exports = nextConfig
변경 사항:
output: 'export'제거 → 서버 사이드 렌더링 활성화images.unoptimized: true유지 (GitHub Pages 호환을 위해)
2단계: 동적 라우트에서 generateStaticParams 제거
기존 방식 (문제):
// app/blog/[tag]/page.tsx export async function generateStaticParams() { // 빌드 시점에 모든 태그의 페이지를 생성하려고 시도 const tags = await getAllTags() return tags.map(tag => ({ tag })) } export default async function TagPage({ params }: PageProps) { // ... }
새로운 방식 (해결):
// app/blog/[tag]/page.tsx export default async function TagPage({ params }: PageProps) { const decodedTag = decodeURIComponent(params.tag) const project = await getProjectByTag(decodedTag) const posts = await getPostsByTag(decodedTag) // 런타임에 Firebase에서 데이터 가져오기 // ... }
변경 사항:
generateStaticParams()완전히 제거- 모든 페이지가 런타임에 동적으로 렌더링됨
- 요청 시점에 Firebase에서 최신 데이터를 가져옴
3단계: 배포 플랫폼 변경 (GitHub Pages → Vercel)
GitHub Pages는 정적 사이트만 지원하므로, 서버 사이드 렌더링을 지원하는 Vercel로 전환:
-
Vercel 프로젝트 생성
- Vercel에 로그인
- GitHub 저장소 연결
- Framework Preset: Next.js 선택
-
환경 변수 설정
- Vercel 프로젝트 설정 → Environment Variables
- Firebase 환경 변수 추가:
NEXT_PUBLIC_FIREBASE_API_KEY=... NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=... NEXT_PUBLIC_FIREBASE_PROJECT_ID=... NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=... NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=... NEXT_PUBLIC_FIREBASE_APP_ID=...
-
vercel.json설정
vercel.json:
{ "buildCommand": "npm run build", "devCommand": "npm run dev", "installCommand": "npm install", "framework": "nextjs", "regions": ["icn1"] }
7.4 전환 후 장점
-
동적 데이터 반영
- 새로운 블로그 글이 즉시 반영됨 (재배포 불필요)
- Firebase에서 실시간으로 데이터 가져오기
-
동적 라우트 지원
- 빌드 타임에 모든 경로를 생성할 필요 없음
- 런타임에 동적으로 라우트 처리
-
성능 최적화
- 필요한 페이지만 렌더링
- 자동 스케일링 지원 (Vercel)
-
개발 경험 개선
- 로컬과 배포 환경의 동작이 일치
- Hot Module Replacement 정상 작동
7.5 주의사항(기억하기)
-
환경 변수 설정 필수
- Vercel 프로젝트 설정에서 Firebase 환경 변수 추가 필수
- 환경 변수가 없으면 Firebase 초기화 실패
-
Firestore 인덱스 설정
- 복합 쿼리(
where+orderBy) 사용 시 인덱스 필요 - Firebase Console에서 인덱스 생성 필요
- 복합 쿼리(
-
보안 규칙 확인
- Firestore와 Storage 보안 규칙이 올바르게 설정되어 있는지 확인
- 테스트 모드로 설정하면 모든 사용자가 읽기/쓰기 가능 (위험)
8. 마무리
Firebase를 Next.js 프로젝트에 연결하고, 정적 사이트에서 동적 페이지로 전환했습니다:
- Firebase 프로젝트 생성 및 서비스 활성화
- Firebase SDK 설치 및 초기화
- 타입 정의 및 기본 함수 구현
- 보안 규칙 설정
- 정적 사이트에서 동적 렌더링으로 전환
- Vercel 배포로 서버 사이드 렌더링 지원
서버 구현을 다음과 같이 설정할 수 있었습니다. 사실 잘 되는지 확실하지 않습니다.. 블로그 페이지와 관리자 페이지 , 기능을 구현해 봐야 이것이 제대로 되는지 확인할 수 있을 것 같습니다.