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 설정 없이 바로 배포 가능