Express에서 MongoDB를 사용한 관계 데이터 관리 예제 : 리뷰 추가 및 삭제
이 글에서는 Express와 MongoDB를 활용하여 캠프장(campgrounds)과 리뷰(reviews) 간의 관계 데이터를 처리하는 방법을 다룹니다. 관계형 데이터베이스와 달리, MongoDB에서는 문서 간 관계를 직접 정의하고 관리해야 합니다. 이를 통해 리뷰 데이터를 추가 및 삭제하는 방법을 살펴보겠습니다.
리뷰 추가 (POST 요청)
라우터 코드
app.post('/campgrounds/:id/reviews', validateReview, async (req, res) => {
const campground = await Campground.findById(req.params.id); // 캠프장 문서 가져오기
const newReview = new Review(req.body.review); // 리뷰 생성
campground.reviews.push(newReview); // 캠프장에 리뷰 ObjectId 추가
await newReview.save(); // 리뷰 저장
await campground.save(); // 캠프장 문서 업데이트
res.redirect(`/campgrounds/${campground._id}`); // 캠프장 상세 페이지로 리다이렉트
});
동작 방식
Campground.findById
- 리뷰를 추가할 대상 캠프장 문서를 가져옵니다.
new Review
- 클라이언트 요청에서 전달된 데이터를 기반으로 새로운 리뷰 문서를 생성합니다.
campground.reviews.push(newReview)
- 생성된 리뷰 문서의 ObjectId를 캠프장의
reviews
배열에 추가합니다.
- 생성된 리뷰 문서의 ObjectId를 캠프장의
newReview.save()
- 리뷰 데이터를 MongoDB에 저장합니다.
campground.save()
- 수정된 캠프장 데이터를 저장합니다.
- 리다이렉트
- 리뷰가 추가된 캠프장 상세 페이지로 리다이렉트합니다.
리뷰 삭제 (DELETE 요청)
라우터 코드
app.delete('/campgrounds/:id/reviews/:reviewId', async (req, res) => {
const { id, reviewId } = req.params;
await Campground.findByIdAndUpdate(id, { $pull: { reviews: reviewId } }); // 캠프장 문서에서 리뷰 ObjectId 제거
await Review.findByIdAndDelete(reviewId); // 리뷰 문서 삭제
res.redirect(`/campgrounds/${id}`); // 캠프장 상세 페이지로 리다이렉트
});
동작 방식
$pull
연산자Campground
문서의reviews
배열에서 특정 리뷰의 ObjectId를 제거합니다.
Review.findByIdAndDelete
- MongoDB에서 해당 리뷰 문서를 삭제합니다.
- 리다이렉트
- 삭제가 완료된 캠프장 상세 페이지로 리다이렉트합니다.
캠프장 삭제 시 연결된 리뷰 삭제
캠프장을 삭제할 경우 해당 캠프장에 연결된 모든 리뷰도 함께 삭제하려면 Mongoose 미들웨어를 사용할 수 있습니다. 다음은 이를 구현한 코드입니다.
Mongoose 미들웨어 코드
CampgroundSchema.post('findOneAndDelete', async function (doc) {
if (doc) {
await Review.deleteMany({ _id: { $in: doc.reviews } });
}
});
동작 방식
findOneAndDelete
훅findOneAndDelete
메서드가 호출된 후에 실행됩니다.
doc
확인- 삭제된 캠프장 문서가 존재할 경우에만 실행됩니다.
Review.deleteMany
- 삭제된 캠프장의
reviews
배열에 포함된 모든 리뷰를 삭제합니다.
- 삭제된 캠프장의
이 코드를 통해 캠프장을 삭제할 때 연결된 리뷰 데이터도 자동으로 정리할 수 있습니다.
리뷰를 표시하고 삭제 버튼 제공 (EJS 템플릿)
EJS 템플릿 코드
<h2>Leave a Review</h2>
<form action="/campgrounds/<%= campground._id %>/reviews" method="post">
<label for="rating">Rating</label>
<input type="range" id="rating" name="review[rating]" min="0" max="5" step="1">
<label for="body">Review</label>
<textarea id="body" name="review[body]" required></textarea>
<button type="submit">Submit</button>
</form>
<% for (let review of campground.reviews) { %>
<div>
<h5>Rating: <%= review.rating %></h5>
<p><%= review.body %></p>
<form action="/campgrounds/<%= campground._id %>/reviews/<%= review._id %>?_method=DELETE" method="post">
<button type="submit">Delete</button>
</form>
</div>
<% } %>
동작 방식
- 리뷰 추가 폼
POST /campgrounds/:id/reviews
요청을 통해 리뷰 데이터를 서버로 전달합니다.
- 리뷰 표시
campground.reviews
배열을 반복하여 각 리뷰 데이터를 표시합니다.
- 삭제 버튼
- 각 리뷰 항목에
DELETE
요청을 보내는 버튼을 제공합니다.
- 각 리뷰 항목에
데이터 관계 모델링
Campground 모델
const CampgroundSchema = new mongoose.Schema({
name: String,
reviews: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Review'
}
]
});
Review 모델
const ReviewSchema = new mongoose.Schema({
rating: Number,
body: String
});
Campground
는 리뷰 문서들의 ObjectId 배열(reviews
)을 통해Review
와 1:N 관계를 가집니다.
주의사항 및 팁
- 트랜잭션 사용
- 리뷰 추가 및 삭제는 두 개의 문서를 다루므로, 데이터 정합성을 보장하려면 MongoDB 트랜잭션을 활용할 수 있습니다.
- 미들웨어
validateReview
와 같은 미들웨어를 활용하여 리뷰 데이터의 유효성을 검사합니다.
- 에러 핸들링
- 데이터베이스 요청 실패에 대비한 에러 처리 로직을 추가하면 더욱 안정적인 애플리케이션을 구현할 수 있습니다.
이 예제는 MongoDB의 문서 간 관계를 다루는 기본적인 방법을 보여줍니다. 이 패턴은 확장성이 뛰어나며, 더 복잡한 애플리케이션 구조에도 응용할 수 있습니다.
Leave a comment