ChatGPT와 Next.js 공부
Next.js + Firebase 블로그 만들기
그랜파 개발자
2025. 5. 11. 14:04
Next.js + Firebase 블로그 만들기
ChatGPT의 말:
좋습니다! 아래는 Next.js 14 (App Router 기반) + Firebase로 구현하는 기본 블로그 앱 구축 가이드입니다. 이 프로젝트는 인증, 글 작성 및 Firestore에 저장하는 기능을 포함합니다.
🧱 목표 기능
- Google 로그인
- 글 작성 (제목, 내용)
- 글 목록 보기
- 글 상세 페이지
- Firestore에 글 저장
- Firebase Auth 연동
📁 1. 폴더 구조
my-blog/
├── app/
│ ├── page.tsx # 홈 (글 목록)
│ ├── write/page.tsx # 글쓰기 페이지
│ ├── post/[id]/page.tsx # 글 상세 페이지
│ └── login/page.tsx # 로그인 페이지
├── components/
│ └── PostCard.tsx
├── lib/
│ └── firebase.ts
├── types/
│ └── post.ts
├── .env.local
├── tailwind.config.js
├── next.config.js
└── package.json
⚙️ 2. 기본 설정
✅ 설치
npx create-next-app@latest my-blog --app --ts
cd my-blog
npm install firebase tailwindcss@3 postcss autoprefixer
npx tailwindcss init -p
✅ tailwind.config.js
module.exports = {
content: ["./app/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [],
};
✅ globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
🔐 3. Firebase 설정
✅ .env.local
NEXT_PUBLIC_FIREBASE_API_KEY=...
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=...
NEXT_PUBLIC_FIREBASE_PROJECT_ID=...
✅ lib/firebase.ts
import { initializeApp } from "firebase/app";
import { getAuth, GoogleAuthProvider } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
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!,
};
const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
export const provider = new GoogleAuthProvider();
export const db = getFirestore(app);
📚 4. 타입 정의
// types/post.ts
export interface Post {
id: string;
title: string;
content: string;
author: string;
createdAt: number;
}
📝 5. 페이지 구현 예시
✅ app/page.tsx (홈)
"use client";
import { useEffect, useState } from "react";
import { db } from "@/lib/firebase";
import { collection, getDocs } from "firebase/firestore";
import Link from "next/link";
export default function HomePage() {
const [posts, setPosts] = useState([]);
useEffect(() => {
const loadPosts = async () => {
const snapshot = await getDocs(collection(db, "posts"));
const items = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
setPosts(items);
};
loadPosts();
}, []);
return (
<div className="p-8">
<h1 className="text-2xl font-bold mb-4">블로그 글 목록</h1>
<ul className="space-y-4">
{posts.map((post: any) => (
<li key={post.id}>
<Link href={`/post/${post.id}`} className="text-blue-600 underline">
{post.title}
</Link>
</li>
))}
</ul>
</div>
);
}
✅ app/write/page.tsx (글쓰기)
"use client";
import { useState } from "react";
import { db, auth } from "@/lib/firebase";
import { collection, addDoc, serverTimestamp } from "firebase/firestore";
import { useRouter } from "next/navigation";
export default function WritePage() {
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const router = useRouter();
const handleSubmit = async () => {
if (!auth.currentUser) return alert("로그인이 필요합니다");
await addDoc(collection(db, "posts"), {
title,
content,
author: auth.currentUser.displayName,
createdAt: serverTimestamp(),
});
router.push("/");
};
return (
<div className="p-8">
<h1 className="text-xl font-bold">글쓰기</h1>
<input className="border p-2 block my-2 w-full" value={title} onChange={(e) => setTitle(e.target.value)} placeholder="제목" />
<textarea className="border p-2 block my-2 w-full h-40" value={content} onChange={(e) => setContent(e.target.value)} placeholder="내용" />
<button onClick={handleSubmit} className="bg-blue-500 text-white px-4 py-2 rounded">
저장
</button>
</div>
);
}
✅ app/post/[id]/page.tsx (상세)
"use client";
import { useEffect, useState } from "react";
import { doc, getDoc } from "firebase/firestore";
import { db } from "@/lib/firebase";
export default function PostPage({ params }: { params: { id: string } }) {
const [post, setPost] = useState<any>(null);
useEffect(() => {
const load = async () => {
const snap = await getDoc(doc(db, "posts", params.id));
if (snap.exists()) setPost({ id: snap.id, ...snap.data() });
};
load();
}, [params.id]);
if (!post) return <div>Loading...</div>;
return (
<div className="p-8">
<h1 className="text-2xl font-bold">{post.title}</h1>
<p className="text-sm text-gray-500">작성자: {post.author}</p>
<div className="mt-4 whitespace-pre-line">{post.content}</div>
</div>
);
}
🔑 로그인 기능 예시
// app/login/page.tsx
"use client";
import { auth, provider } from "@/lib/firebase";
import { signInWithPopup } from "firebase/auth";
export default function LoginPage() {
const login = async () => {
await signInWithPopup(auth, provider);
alert("로그인 성공!");
};
return (
<div className="p-8">
<button onClick={login} className="bg-gray-800 text-white px-4 py-2 rounded">
Google 로그인
</button>
</div>
);
}
🚀 배포
- Vercel 또는 Firebase Hosting 사용 가능
- Vercel은 next.config.js 설정 없이 바로 배포 가능