Next.js를 사용하여 블로그 애플리케이션을 만들어보겠습니다. 블로그 애플리케이션은 다음과 같은 기능을 구현할 수 있습니다.
블로그 애플리케이션은 다음과 같은 구조로 구성됩니다. components
디렉토리에 레이아웃 컴포넌트와 포스트 관련 컴포넌트를 생성하고, pages
디렉토리에 메인 페이지와 포스트 페이지를 생성할 수 있습니다.
public
디렉토리에 이미지와 스타일 파일을 저장할 수 있습니다.
app-blog-firebase/
├── app/
│ ├── posts/
│ │ ├── page.js # 게시글 목록
│ │ ├── write/
│ │ │ └── page.js # 글쓰기 페이지
│ │ └── [id]/
│ │ ├── page.js # 상세 페이지
│ │ └── edit/
│ │ └── page.js # 수정 페이지
└── lib/
│ ├── firebase/
│ │ ├── firebase.js # Firebase 연결 파일
│ │ ├── auth.jsx # Firebase 인증 provider
│ │ └── firestore.js # Firestore CRUD api 파일
└── components/
│ ├── layout/
│ │ └── Header.jsx # 헤더 컴포넌트
├── .env.local # 환경 변수 파일
</>
아이콘을 클릭합니다.// src/lib/firebase/firebase.js
// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
// Your web app's Firebase configuration
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
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
npm install firebase
# or
yarn add firebase
# .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
// src/lib/firebase/firebase.js
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from 'firebase/firestore'
// Your web app's Firebase configuration
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
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
// 인증관련
export const auth = getAuth(app);
// 데이터 베이스 관련
export const db = getFirestore(app)
// src/lib/firebase/auth.js
"use client"
import { createContext, useContext, useEffect, useState } from 'react';
import { auth } from './firebase';
import {
signInWithEmailAndPassword,
createUserWithEmailAndPassword,
signOut,
onAuthStateChanged,
GoogleAuthProvider,
signInWithPopup
} from 'firebase/auth';
// AuthContext 컨텍스트 생성
const AuthContext = createContext({
user: null,
login: async () => {},
register: async () => {},
logout: async () => {},
loginWithGoogle: async () => {}
});
// AuthProvider 컴포넌트 정의
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
// 사용자 상태 업데이트
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, setUser);
return () => unsubscribe();
}, []);
// 로그인 함수
const login = async (email, password) => {
await signInWithEmailAndPassword(auth, email, password);
};
// 회원가입 함수
const register = async (email, password) => {
await createUserWithEmailAndPassword(auth, email, password);
};
// 로그아웃 함수
const logout = async () => {
await signOut(auth);
};
// Google 로그인 함수
const loginWithGoogle = async () => {
const provider = new GoogleAuthProvider();
try {
await signInWithPopup(auth, provider);
} catch (error) {
console.error('Google 로그인 실패:', error);
}
};
return (
<AuthContext.Provider value={{
user,
login,
register,
logout,
loginWithGoogle
}}>
{children}
</AuthContext.Provider>
);
};
// 사용자 정의 훅
export const useAuth = () => useContext(AuthContext);
// app/layout.js
import localFont from "next/font/local";
import "./globals.css";
import Header from "@/components/layout/Header";
import { AuthProvider } from "@/lib/firebase/auth";
(...)
export default function RootLayout({ children }) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<AuthProvider> // AuthProvider로 감싸기
<Header />
{children}
</AuthProvider>
</body>
</html>
);
}
이제 로그인 및 회원가입 기능이 포함된 헤더를 만들어보겠습니다.
// components/Layout/Header.tsx
'use client'
import Link from 'next/link'
import { useAuth } from '@/lib/firebase/auth'
const Header = () => {
const { user, loginWithGoogle, logout } = useAuth()
return (
<header className="flex justify-between items-center p-4">
<h1>
<Link href="/">logo</Link>
</h1>
<nav>
<ul className="flex space-x-4">
<li>
<Link href="/about">About</Link>
</li>
<li>
<Link href="/post">Post</Link>
</li>
</ul>
</nav>
<div className="space-x-4">
{user ? (
<button onClick={logout} className="p-2 bg-red-500 rounded">
로그아웃
</button>
) : (
<>
<button onClick={loginWithGoogle} className="p-2 bg-blue-500 rounded">
Google로 로그인
</button>
<Link href="/login">
<button className="p-2 bg-green-500 rounded">로그인</button>
</Link>
<Link href="/signup">
<button className="p-2 bg-yellow-500 rounded">회원가입</button>
</Link>
</>
)}
</div>
</header>
)
}
export default Header
// app/(auth)/login/page.jsx
"use client"
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { useAuth } from '@/lib/firebase/auth'
const Login = () => {
const { login, loginWithGoogle } = useAuth()
const router = useRouter()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const handleLogin = async (event) => {
event.preventDefault()
try {
await login(email, password)
router.push('/')
} catch (err) {
setError('로그인 실패: 잘못된 이메일 또는 비밀번호입니다.')
}
}
return (
<div className='container mx-auto'>
<h2>로그인</h2>
{error && <p>{error}</p>}
<form onSubmit={handleLogin}>
<label>
이메일:
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
</label>
<label>
비밀번호:
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required />
</label>
<button type="submit">로그인</button>
</form>
<button onClick={loginWithGoogle}>Google로 로그인</button>
</div>
)
}
export default Login
// app/(auth)/signup/page.jsx
"use client";
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/lib/firebase/auth';
import { signInWithPopup } from 'firebase/auth';
const Signup = () => {
const { register } = useAuth();
const router = useRouter();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
// 이메일 회원가입 처리
const handleRegister = async (event) => {
event.preventDefault();
try {
await register(email, password);
router.push('/');
} catch (err) {
setError('회원가입 실패: 이메일 형식이 올바르지 않거나 비밀번호가 너무 짧습니다.');
}
};
// Google 로그인 처리
const handleGoogleLogin = async () => {
try {
const result = await signInWithPopup(auth, googleProvider);
console.log('Google 로그인 성공:', result.user); // 로그인 성공 시 사용자 정보
router.push('/'); // 로그인 성공 시 메인 페이지로 이동
} catch (err) {
console.error('Google 로그인 실패:', err);
setError('Google 로그인 실패: 다시 시도해주세요.');
}
};
return (
<div className='container mx-auto'>
<h2>회원가입</h2>
{error && <p style={{ color: 'red' }}>{error}</p>}
<form onSubmit={handleRegister}>
<label>
이메일:
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</label>
<label>
비밀번호:
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</label>
<button type="submit">회원가입</button>
</form>
<hr />
<button onClick={handleGoogleLogin}>Google로 로그인</button> {/* Google 로그인 버튼 */}
</div>
);
};
export default Signup;
firebase firestore를 사용하여 데이터베이스를 생성하고, 데이터를 추가, 조회, 수정, 삭제하는 방법을 알아보겠습니다.
테스트 모드
로 설정합니다.// src/lib/firebase/firestore.js
import { db } from './firebase'
import { collection, getDocs, addDoc, doc, updateDoc, deleteDoc } from 'firebase/firestore'
// 추가 (Create)
export async function addDocument(collectionName, data) {
try {
const docRef = await addDoc(collection(db, collectionName), data)
console.log('Document written with ID:', docRef.id)
} catch (e) {
console.error('Error adding document:', e)
}
}
// 조회 (Read)
export async function getAllDocuments(collectionName) {
const querySnapshot = await getDocs(collection(db, collectionName))
querySnapshot.forEach((doc) => {
console.log(`${doc.id} =>`, doc.data())
})
}
// 수정 (Update)
export async function updateDocument(collectionName, docId, newData) {
const docRef = doc(db, collectionName, docId)
try {
await updateDoc(docRef, newData)
console.log('Document updated')
} catch (e) {
console.error('Error updating document:', e)
}
}
// 삭제 (Delete)
export async function deleteDocument(collectionName, docId) {
const docRef = doc(db, collectionName, docId)
try {
await deleteDoc(docRef)
console.log('Document deleted')
} catch (e) {
console.error('Error deleting document:', e)
}
}
데이터를 조회하고 수정, 삭제 버튼을 추가하여 데이터를 수정하거나 삭제할 수 있습니다.
// src/app/social/page.jsx
"use client"
import React, { useState, useEffect } from 'react'
import { collection, getDocs, query, orderBy } from 'firebase/firestore'
import SocialEditPage from './[id]/edit/page'
import { db } from '@/lib/firebase/firebase'
import { deleteDocument } from '@/lib/firebase/firestore'
import Link from 'next/link'
/**
* 게시글 목록 페이지
* - 게시글 목록 조회
* - 게시글 수정/삭제 기능
* - 게시글 상세 페이지 링크
*/
const SocialPage = () => {
// 게시글 목록 상태
const [posts, setPosts] = useState([])
// 현재 수정 중인 게시글 ID
const [editingPostId, setEditingPostId] = useState(null)
/**
* Firestore에서 게시글 목록을 가져오는 함수
* - 제목 기준 오름차순 정렬
* - 제목과 내용만 가져옴
*/
const fetchPosts = async () => {
try {
const postsQuery = query(
collection(db, 'posts'),
orderBy('title')
)
const querySnapshot = await getDocs(postsQuery)
const postsData = querySnapshot.docs.map((doc) => ({
id: doc.id,
title: doc.data().title,
content: doc.data().content,
}))
setPosts(postsData)
} catch (error) {
console.error('게시글 목록 조회 실패:', error)
alert('게시글 목록을 불러오는데 실패했습니다.')
}
}
// 컴포넌트 마운트 시 게시글 목록 조회
useEffect(() => {
fetchPosts()
}, [])
/**
* 게시글 삭제 처리 함수
* @param {string} id - 삭제할 게시글 ID
*/
const handleDelete = async (id) => {
if (window.confirm('정말 삭제하시겠습니까?')) {
try {
await deleteDocument('posts', id)
fetchPosts() // 목록 새로고침
} catch (error) {
console.error('게시글 삭제 실패:', error)
alert('게시글 삭제에 실패했습니다.')
}
}
}
/**
* 게시글 수정 완료 처리 함수
* - 수정 모드 종료
* - 목록 새로고침
*/
const handleUpdate = () => {
setEditingPostId(null)
fetchPosts()
}
return (
<div className="max-w-4xl mx-auto p-4">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold">게시글 목록</h2>
<Link
href="/social/write"
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
글쓰기
</Link>
</div>
{editingPostId ? (
// 수정 모드
<SocialEditPage
id={editingPostId}
initialTitle={posts.find((post) => post.id === editingPostId)?.title ?? ''}
initialContent={posts.find((post) => post.id === editingPostId)?.content ?? ''}
onCancel={() => setEditingPostId(null)}
onUpdate={handleUpdate}
/>
) : (
// 목록 모드
<>
{posts.length > 0 ? (
<ul className="space-y-4">
{posts.map((post) => (
<li
key={post.id}
className="border rounded-lg p-4 hover:shadow-md transition-shadow"
>
<Link
href={`/social/${post.id}`}
className="block"
>
<h3 className="text-xl font-semibold mb-2">{post.title}</h3>
<p className="text-gray-600 mb-4 line-clamp-2">{post.content}</p>
</Link>
</li>
))}
</ul>
) : (
<p className="text-center text-gray-500 py-8">
게시글이 없습니다.
</p>
)}
</>
)}
</div>
)
}
export default SocialPage
// src/app/social/write/page.jsx
"use client"
import { addDocument } from '@/lib/firebase/firestore'
import React, { useState } from 'react'
const SocialWritePage = () => {
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
// 폼 제출 시 Firestore에 데이터를 추가하는 함수 호출
const handleSubmit = async (event) => {
event.preventDefault()
// 입력된 데이터 유효성 검사
if (title.trim() === '' || content.trim() === '') {
alert('제목과 내용을 입력하세요!')
return
}
// Firestore에 문서 추가
try {
await addDocument('posts', { title, content })
alert('새 게시물이 추가되었습니다!')
setTitle('')
setContent('')
} catch (error) {
console.error('게시물 추가 중 오류 발생:', error)
alert('게시물 추가에 실패했습니다.')
}
}
return (
<div>
<h1>게시물 추가하기</h1>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="title">제목:</label>
<input id="title" type="text" value={title} onChange={(e) => setTitle(e.target.value)} />
</div>
<div>
<label htmlFor="content">내용:</label>
<textarea id="content" value={content} onChange={(e) => setContent(e.target.value)} />
</div>
<button type="submit">추가하기</button>
</form>
</div>
)
}
export default SocialWritePage
// src/app/social/[id]/page.jsx
import { doc, getDoc } from 'firebase/firestore'
import { db } from '@/lib/firebase/firebase'
import Link from 'next/link'
import { notFound } from 'next/navigation'
async function SocialDetailPage({ params }) {
// Fetch data directly in the component
const docRef = doc(db, 'posts', params.id)
const docSnap = await getDoc(docRef)
if (!docSnap.exists()) {
notFound()
}
const post = docSnap.data()
return (
<div className="p-4">
<h2 className="text-2xl font-bold mb-4">{post.title}</h2>
<p className="mb-4">{post.content}</p>
<Link
href="/social"
className="text-blue-500 hover:text-blue-700"
>
Back to list
</Link>
</div>
)
}
// Generate static params (replaces getStaticPaths)
export async function generateStaticParams() {
// If you want to statically generate some paths at build time
// you can fetch the IDs here and return them
return []
}
export default SocialDetailPage
// src/app/social/[id]/edit/page.jsx
import { updateDocument } from '@/lib/firebase/firestore'
import React, { useState, useEffect } from 'react'
const SocialEditPage = ({ id, initialTitle, initialContent, onCancel, onUpdate }) => {
const [title, setTitle] = useState(initialTitle)
const [content, setContent] = useState(initialContent)
const handleUpdate = async (event) => {
event.preventDefault()
await updateDocument('posts', id, { title, content })
onUpdate() // 성공 시 부모 컴포넌트에 알리기 위해 사용
}
return (
<div>
<h2>Edit Post</h2>
<form onSubmit={handleUpdate}>
<div>
<label htmlFor="title">Title:</label>
<input id="title" type="text" value={title} onChange={(e) => setTitle(e.target.value)} />
</div>
<div>
<label htmlFor="content">Content:</label>
<textarea id="content" value={content} onChange={(e) => setContent(e.target.value)} />
</div>
<button type="submit">Update</button>
<button type="button" onClick={onCancel}>
Cancel
</button>
</form>
</div>
)
}
export default SocialEditPage