0. 주요 변경사항 정리
MongoDB 연결 설정 추가
- 환경 변수 (
MONGODB_URI
) 설정 - 데이터베이스 연결 유틸리티 생성
- Mongoose 모델 정의
- 환경 변수 (
API 라우트 변경
- MongoDB CRUD 작업으로 변경
클라이언트 컴포넌트 수정
id
대신_id
사용- 날짜 형식 처리 추가
1. MongoDB 설정
MongoDB Atlas 가입하기
1-1. MongoDB Atlas 설정
1. MongoDB Atlas에서 연결 문자열을 가져오는 방법:
- "Connect" 버튼을 클릭합니다
- "Connect your application"을 선택합니다
- Driver는 "Node.js"를 선택합니다
- 거기서 보이는 연결 문자열을 복사합니다 (예:
mongodb+srv://username:<password>@cluster0...
)
2. 프로젝트에 환경 변수 설정:
- 프로젝트 루트 폴더에
.env
파일을 생성합니다 - 아래와 같이 작성합니다:
MONGODB_URI=mongodb+srv://username:<password>@cluster0.xxxxx.mongodb.net/<database>?retryWrites=true&w=majority
<password>
는 MongoDB Atlas에서 생성한 데이터베이스 사용자의 실제 비밀번호로 교체<database>
는 사용하고 싶은 데이터베이스 이름으로 교체 (예: "blog")
3. Network Access 설정:
- 왼쪽 메뉴에서 "Network Access"를 클릭합니다
- "Add IP Address" 버튼 클릭
- 개발 중이므로 "Allow Access from Anywhere"를 선택하고 "0.0.0.0/0" 입력
- Confirm 버튼 클릭
4. Database Access 설정:
- 왼쪽 메뉴에서 "Database Access"를 클릭합니다
- "Add New Database User" 버튼 클릭
- Username과 Password를 설정합니다 (이 정보는 연결 문자열에 사용됩니다)
- "Add User" 버튼 클릭
1-2. 프로젝트에 MongoDB 설치
npm install mongodb mongoose
1-3. 환경 변수 설정
.env
파일 생성:
MONGODB_URI=mongodb+srv://<username>:<password>@cluster0.xxxxx.mongodb.net/<database>?retryWrites=true&w=majority
2. MongoDB 연결 설정
- 연결 유틸리티는 "데이터베이스로 가는 도로" 같은 것
- 모델은 "데이터를 담을 상자의 설계도" 같은 것
2-1. MongoDB 연결 유틸리티 생성
역할 : 데이터 베이스 연결을 관리
주요 기능
- MongoDB와의 연결 상태를 관리 : 매 요청마다 새로운 연결을 만들지 않음
- 연결이 이미 있으면 재사용(캐싱) : 연결을 재사용하여 성능 향상
- 없으면 새로 연결 : 안정적인 데이터 베이스 연결 유지
작동방식
if (이미_연결되어있나?) { 기존_연결_재사용(); } else { 새로운_연결_생성(); }
src/lib/mongodb.js
파일 생성:
import mongoose from 'mongoose';
// MongoDB 연결 문자열을 환경변수에서 가져옴
const MONGODB_URI = process.env.MONGODB_URI;
// 연결 문자열이 없으면 에러 발생
if (!MONGODB_URI) {
throw new Error('MONGODB_URI must be defined');
}
// 전역 변수에 mongoose 연결 정보를 저장
// 이렇게 하면 서버가 재시작되어도 연결이 유지됨
let cached = global.mongoose;
// 처음 실행될 때는 cached가 없으므로 초기화
if (!cached) {
cached = global.mongoose = {
conn: null, // 현재 연결 객체
promise: null // 연결 시도중인 Promise
};
}
async function connectDB() {
// 이미 연결되어 있다면 그 연결을 재사용
if (cached.conn) {
return cached.conn;
}
// 연결 시도중이 아니라면 새로운 연결 시도
if (!cached.promise) {
const opts = {
bufferCommands: false, // 연결되기 전에 명령어 버퍼링 비활성화
};
// MongoDB 연결 시도
cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => {
return mongoose;
});
}
try {
// 연결이 완료될 때까지 대기
cached.conn = await cached.promise;
} catch (e) {
// 연결 실패시 promise 초기화하고 에러 던지기
cached.promise = null;
throw e;
}
return cached.conn;
}
export default connectDB;
2-2. Post 모델 생성
- 역할 : MongoDB의 데이터를 다루는 객체
- 주요 기능
- 게시글의 데이터 구조 정의 (제목, 내용, 날짜 등) : 일관된 데이터 구조 유지
- 필수 입력값 지정 : 데이터 유효성 검사 자동화
- 데이터 유효성 검사 규칙 설정 : MongoDB 작업을 더 쉽게 수행
- 예시
{ title: { // 제목 필드 type: String, // 문자열 타입 required: true, // 필수 입력 trim: true // 앞뒤 공백 제거 }, content: { // 내용 필드 type: String, required: true } }
models/Post.js
파일 생성:
import mongoose from 'mongoose';
// 이미 모델이 있다면 그것을 사용, 없다면 새로 생성
const PostSchema = new mongoose.Schema({
title: {
type: String,
required: [true, '제목을 입력해주세요.'], // 필수 입력
trim: true, // 앞뒤 공백 제거
},
content: {
type: String,
required: [true, '내용을 입력해주세요.'],
},
createdAt: {
type: Date,
default: Date.now, // 기본값은 현재 시간
}
});
// 모델이 이미 있다면 그것을 사용, 없다면 새로 생성
export default mongoose.models.Post || mongoose.model('Post', PostSchema);
3. API 라우트 수정
3-1. 전체 게시글 API (app/api/posts/route.js
)
import { NextResponse } from 'next/server';
// MongoDB 연결을 위한 유틸리티 함수 가져오기
import connectDB from '@/lib/mongodb';
// MongoDB의 Post 모델 가져오기
import Post from '@/models/Post';
export async function GET() {
try {
// MongoDB 연결
await connectDB();
// Post 모델을 사용해 모든 게시글을 찾고, 생성일 기준 내림차순 정렬
const posts = await Post.find({}).sort({ createdAt: -1 });
return NextResponse.json(posts);
} catch (error) {
return NextResponse.json(
{ error: '게시글을 불러오는데 실패했습니다.' },
{ status: 500 }
);
}
}
export async function POST(req) {
try {
// MongoDB 연결
await connectDB();
const data = await req.json();
if (!data.title || !data.content) {
return NextResponse.json(
{ error: '제목과 내용은 필수입니다.' },
{ status: 400 }
);
}
// Post 모델을 사용해 새 게시글 생성
const post = await Post.create(data);
return NextResponse.json(post, { status: 201 });
} catch (error) {
return NextResponse.json(
{ error: '게시글 작성에 실패했습니다.' },
{ status: 500 }
);
}
}
3-2. 개별 게시글 API (app/api/posts/[id]/route.js
)
import { NextResponse } from 'next/server';
import Post from '@/models/Post';
// MongoDB의 ObjectId 검증을 위한 mongoose import
import mongoose from 'mongoose';
// MongoDB 연결을 위한 유틸리티 함수 가져오기
import connectDB from '@/lib/mongodb';
// MongoDB의 ObjectId가 유효한지 검사하는 함수
const isValidObjectId = (id) => mongoose.Types.ObjectId.isValid(id);
export async function GET(req, { params }) {
try {
// MongoDB 연결
await connectDB();
// Next.js 13에서는 params를 비동기로 처리해야 함
const resolvedParams = await Promise.resolve(params);
// 게시글 ID가 유효한 MongoDB ObjectId 형식인지 검사
if (!isValidObjectId(resolvedParams.id)) {
return NextResponse.json(
{ error: '유효하지 않은 게시글 ID입니다.' },
{ status: 400 }
);
}
// Post 모델을 사용해 특정 ID의 게시글 찾기
const post = await Post.findById(resolvedParams.id);
if (!post) {
return NextResponse.json(
{ error: '게시글을 찾을 수 없습니다.' },
{ status: 404 }
);
}
return NextResponse.json(post);
} catch (error) {
return NextResponse.json(
{ error: '게시글을 불러오는데 실패했습니다.' },
{ status: 500 }
);
}
}
export async function PUT(req, { params }) {
try {
// MongoDB 연결
await connectDB();
const resolvedParams = await Promise.resolve(params);
// ID 유효성 검사
if (!isValidObjectId(resolvedParams.id)) {
return NextResponse.json(
{ error: '유효하지 않은 게시글 ID입니다.' },
{ status: 400 }
);
}
const data = await req.json();
// findByIdAndUpdate: ID로 게시글을 찾아 업데이트
// $set: MongoDB 업데이트 연산자, new: true는 업데이트된 문서 반환
const post = await Post.findByIdAndUpdate(
resolvedParams.id,
{ $set: data },
{ new: true, runValidators: true }
);
if (!post) {
return NextResponse.json(
{ error: '게시글을 찾을 수 없습니다.' },
{ status: 404 }
);
}
return NextResponse.json(post);
} catch (error) {
return NextResponse.json(
{ error: '게시글 수정에 실패했습니다.' },
{ status: 500 }
);
}
}
export async function DELETE(req, { params }) {
try {
// MongoDB 연결
await connectDB();
const resolvedParams = await Promise.resolve(params);
// ID 유효성 검사
if (!isValidObjectId(resolvedParams.id)) {
return NextResponse.json(
{ error: '유효하지 않은 게시글 ID입니다.' },
{ status: 400 }
);
}
// findByIdAndDelete: ID로 게시글을 찾아 삭제
const post = await Post.findByIdAndDelete(resolvedParams.id);
if (!post) {
return NextResponse.json(
{ error: '게시글을 찾을 수 없습니다.' },
{ status: 404 }
);
}
return NextResponse.json({ message: '게시글이 삭제되었습니다.' });
} catch (error) {
return NextResponse.json(
{ error: '게시글 삭제에 실패했습니다.' },
{ status: 500 }
);
}
}
4. 클라이언트 컴포넌트 수정
기존 클라이언트 컴포넌트의 대부분은 그대로 사용할 수 있습니다. MongoDB의 _id
를 사용하도록 수정이 필요한 부분만 변경하면 됩니다.
4-1. 게시글 목록 페이지 수정 (app/posts/page.js
)
// posts.map() 부분 수정
// mongodb는 자동으로 생성하는 고유 식별자를 _id로 사용
{posts.map((post) => (
<Link
key={post._id} // id 대신 _id 사용
href={`/posts/${post._id}`} // id 대신 _id 사용
className="cursor-pointer block" // block 추가하여 전체 영역 클릭 가능하게
>
<h2>{post.title}</h2>
<p>{post.content}</p>
<span>{new Date(post.createdAt).toLocaleDateString()}</span>
</Link>
))}
4-2. 게시글 상세 페이지 수정 (app/posts/[id]/page.js
)
// app/posts/[id]/edit/page.js
import { use } from "react";
import EditForm from "./editForm";
export default function EditPage({ params }) {
// next.js 13부터 params가 promise로 전달됨 (비동기 데이터)
// Promise는 바로 사용할 수 없음
// `use()` 훅을 사용하여 unwrap 해야 함
const resolvedParams = use(params);
return <EditForm postId={resolvedParams.id} />;
}
axios
로 받은 응답에는 여러 정보가 포함되어 있는데, 실제 데이터는data
속성에 들어있습니다.
{
data: {
// 실제 서버에서 받은 데이터
title: "게시글 제목",
content: "게시글 내용"
},
status: 200, // HTTP 상태 코드
statusText: "OK", // 상태 메시지
headers: {}, // 응답 헤더
config: {} // 요청 설정
}
// app/posts/[id]/editForm.js
'use client';
// ... imports 동일
export default function EditForm({ postId }) {
// ... router와 state 설정 동일
useEffect(() => {
const fetchPost = async () => {
try {
// MongoDB에 저장된 특정 게시글을 ID로 조회
const { data } = await axios.get(`/api/posts/${postId}`);
setTitle(data.title);
setContent(data.content);
} catch (error) {
console.error('Error fetching post:', error);
alert('게시글을 불러올 수 없습니다.');
router.push('/posts');
}
};
fetchPost();
}, [postId, router]);
const handleSubmit = async (e) => {
e.preventDefault();
try {
// MongoDB의 게시글 데이터 업데이트
await axios.put(`/api/posts/${postId}`, { title, content });
router.push('/posts');
} catch (error) {
console.error('Error updating post:', error);
alert('수정에 실패했습니다.');
}
};
// ... return 부분 동일
}
4-2. 날짜 형식 수정
날짜 포맷팅 함수는 여러 컴포넌트에서 재사용할 수 있으므로, utils
폴더에 따로 만드는 것이 좋습니다
// utils/formatDate.js
export const formatDate = (date) => {
// MongoDB의 Date 객체를 로컬 시간으로 변환
return new Date(date).toLocaleDateString('ko-KR', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
// app/posts/page.js 또는 다른 컴포넌트
import { formatDate } from '@/utils/formatDate';
// 컴포넌트 내부에서 사용
<span>{formatDate(post.createdAt)}</span>
))}
'Front > Node.js' 카테고리의 다른 글
프론트엔드 개발자를 위한 MongoDB & Mongoose 가이드 (0) | 2024.12.26 |
---|---|
프론트엔드 개발자를 위한 데이터베이스 초보자 가이드 (1) | 2024.12.26 |
Node.js 모듈과 객체 (0) | 2024.12.25 |
자바스크립트 비동기 처리 (2) | 2024.12.25 |
next.js로 CRUD API 서버 만들기 (1) | 2024.12.09 |