티스토리 뷰
회원과 게시글 연동하기
▶ 회원-게시글 연동 기능 설명
1) 게시글 작성 시 로그인된 회원 정보를 작성자로 추가
2) 게시글-작성자는 populate하여 사용하도록 구현
3) 게시글 수정, 삭제 시 로그인된 유저와 작성자가 일치하는지 확인
4) 작성자의 게시글 모아 보기 기능 구현
▶ 회원-게시글 연동 기능 구현하기
1) PostSchema 수정
- PostSchema에 author 추가
- populate를 사용하기 위해 ObjectID 사용
- ref를 유저 모델의 이름인 'User'로 선언
...
author: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true,
},
...
2) 게시글 등록 요청 수정 (게시글 등록 시 author 추가)
- req.user는 strategy에서 최소한의 정보로 저장한 shortId, email, username만 가지고 있음
- Post 생성 시 user의 ObjectID를 전달해야 하는데, 이를 위해 User모델에서 shortId로 회원을 검색하여 한 번 더 검증
- type: ObjectID로 선언한 필드에 객체가 주어지면 자동으로 ObjectID 사용함
const author = await User.find({
shortId: req.user.shortId,
});
if (!author) {
throw new Error('No User');
}
await Post.create({
title,
content,
author,
});
3) 게시글에 작성자 연동
- 게시글 find 시 populate를 추가하여 ObjectID로 저장된 author를 각 게시글에 주입
- view를 작성할 때 post.author.{field}로 사용 가능
--- ./routes/posts.js ---
router.get('/', ... {
...
const posts = await Post.find({})
...
.populate('author');
res.render('posts/list', { posts });
--- ./views/posts/list.pug ---
...
td post.author.name
4) 게시글 수정, 삭제 시 유저 확인
- 게시글 수정, 삭제 시 작성자를 populate하여 로그인된 사용자와 일치하는지 확인
const post = await Post.find({
shortId,
}).populate('author'); // find를 할때 post.author.shortId 사용 가능
if (post.author.shortId !== req.user.shortId) {
throw new Error('Not Authorized');
}
▶ 작성자 게시글 모아보기 기능 구현하기
기본적으로 MongoDB는 Document 검색 시, 전체 문서를 하나씩 확인
→ 매우 비효율적인 검색 수행
→ 데이터가 많아질 경우 속도 저하의 가장 큰 원인이 됨
* Index
- MongoDB는 검색을 위해 Document를 정렬하여 저장하는 기능을 제공
- Index를 설정하면 주어진 쿼리를 효율적으로 수행하여 성능을 향상시킬 수 있음
※ 다중 키, 좌표, 텍스트 등의 특별한 값으로 정리되는 인덱스도 제공
1) author에 index 설정
- PostSchema의 author 속성에 index: true 옵션을 사용하면 mongoose가 자동으로 MongoDB에 인덱스를 생성
※ 이미 데이터가 많은 상태에서 인덱스를 추가할 시 작업 시간이 길어져 MongoDB가 응답하지 않을 수 있음
→ 예상되는 인덱스를 미리 추가하는 것이 좋음
author: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true,
index: true,
},
2) 회원 게시글 라우팅 추가하기
- RESTful 한 구성을 위해 경로를 "/users/{userId}/posts"로 구성
- 게시글 목록 view는 기존에 작성한 posts/list.pug를 재활용
--- ./routes/users.js ---
...
router.get('/:shortId/posts', ... => {
...
const { shortId } = req.params;
const user = await User.find({ shortId });
const posts = await Post.find({ author: user }).populate('author');
res.render('posts/list', { posts, user });
});
...
3) 게시글 목록 화면 수정
- 게시글 목록 화면을 재활용하기 위해 수정
- 유저의 게시글인 경우 "###의 게시글"이라는 제목 사용
- 게시글의 사용자 이름에 유저의 게시글 link 추가
h2= user ? `${user.name}의 게시글`: "전체 게시글"
...
...
td: a(href=`/users/${post.author.shortId}`)= post.author.name
CSR로 댓글 기능 구현하기
▶ CSR을 구현하는 방법
1) 페이지 로드 시 필요한 리소스를 클라이언트에 선언
2) 클라이언트에서 필요한 데이터를 비동기 호출
3) 클라이언트가 전달받은 데이터를 가공, 리소스를 사용하여 화면에 표시
클라이언트에 리소스를 선언하기 위한 다양한 방법이 존재 (React.js, Vue.js 등)
여기서는 간단하게 HTML Template 기능을 사용
→ HTML Template은 브라우저에 표시되지 않는 HTML Element를 작성해 두고
JavaScript로 이를 화면에 반복적으로 그릴 수 있게 하는 기술
▶ 댓글 기능 구현하기
1) 댓글 화면 작성하기
- 게시글 상세 화면 하단에 댓글작성, 목록 화면 추가
- HTML Template 사용하여 한 개의 댓글이 표시될 모양을 선언
- JavaScript 로 조작하기 위해 id, class를 선언하는 것이 유용함
...
table
thead
tr
td(colspan="2")
input#content(type="text")
td: button(onclick="writeComment()") 댓글 작성
tbody#comments
template#comment-template
tr
td.content
td.author
td.createdAt
2) 게시글에 댓글 추가하기
- mongoose의 sub-schema를 이용하여 Post 스키마에 Comment를 배열로 추가
- populate를 사용할 때 ObjectID만 저장하는 것과는 다르게 sub-schema를 이용하면 Comment의 내용을 게시글이 포함
※ sub-schema 내부에서도 populate 가능
const CommentSchema = new Schema({
content: String,
author: {
type: Schema.Types.ObjectId,
ref: 'User',
},
}, {
timestamps: true,
});
const PostSchema = new Schema({
...
comments: [CommehtSchema],
...
3) API 작성하기 - 댓글 작성
* 지금까지의 구현들은 HTTP 응답으로 HTML을 전송하는 SSR 방식
→ CSR을 구현하기 위해서는 HTML을 보내는 것이 아니라 데이터만 주고받을 수 있는 API를 구성해야함 (JSON 사용)
- 댓글 작성 API와 댓글 목록 API만 구현
- 댓글 작성 시 댓글목록을 다시 불러와 그리는 형식으로 구현
(아예 새로 불러오는게 아니라 기존 댓글의 마지막 부분을 확인해서 마지막 부분부터 새로 작성한 댓글만 불러오는게 좋은 방법이지만 여기서는 우선 간단한 방법인 댓글목록 전체를 다시 불러와 렌더하는 방식으로 진행)
- api 라우터를 추가하고, RESTful하게 "api/posts/{postId}/comment" 경로로 댓글 작성 기능 구현
- 게시글 업데이트 시 $push를 사용하여 comments 배열에 새로 작성된 댓글 추가 (동시에 들어오는 요청에 대해 정확하게 처리)
- api는 render 하지 않고 json으로 응답
...
router.post('/posts/:shortId/comments', ... {
const { shortId } = req.params;
const { content } = req.body;
const author = await User.findOne({ shortId: req.user.shortId });
await Post.updateOne({ shortId }, {
$push: { comments: {
content,
author,
}},
});
res.json({ result: 'success' });
});
...
4) API 작성하기 - 댓글 목록
- "/api/posts/{postId}/comments"로 RESTful 경로 설정
- find에 populate하지 않고 User모델에 populate를 사용하는 방법도 가능
...
router.get('/posts/:shortId/comments', ... {
const { shortId } = req.params;
const post = await Post.findOne({ shortId });
await User.populate(post.comments, {
path: 'author'
});
res.json(post.comments);
});
...
5) fetch로 API 호출하고 처리하기 - 댓글 작성하기
- 브라우저는 fetch 함수를 이용해 비동기 HTTP 요청을 제공
- fetch는 jQuery의 ajax와 유사한 기능 (jQuery같은 라이브러리를 사용하지 않고도 HTTP 요청 구현 가능)
- fetch를 이용하면 간단하게 JavaScript로 HTTP 요청을 비동기 처리할 수 있음
- 댓글 작성 버튼 클릭 시 writeComment() 실행
- input#content에서 내용을 읽어 fetch로 댓글 작성 api 호출
- 호출 결과의 성공 여부를 확인하여, 댓글 다시 불러오기 실행
...
script.
function writeComment() {
const input = document.querySelector('#content')
const content = input.value;
fetch('/api/posts/#{post.shortId}/comments', {
method: 'post',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content }),
})
.then(() => {
if (res.ok) {
input.value = '';
loadComments();
} else {
alert('오류가 발생했습니다.');
}
});
}
6) fetch로 API 호출하고 처리하기 - 댓글 목록 불러오기
// 댓글 목록 api 호출하기
script.
loadComments();
function loadComments() {
document.querySelector('#comments').innerHTML = ''; // 이전 목록 삭제
fetch('/api/posts/#{post.shortId}/comments')
.then((res) => {
if (res.ok) {
return res.json();
} else {
throw new Error('댓글을 불러오지 못했습니다');
}
})
.then((comments) => {
comments.forEach(addComment);
})
.catch((err) => alert(err.message));
}
// HTML Template 사용하여 댓글 화면에 표시하기
function addComment(comment) {
const template = document.querySelector('#comment-template');
const node = document.importNode(template.content, true);
node.querySelector('.content').textContent = comment.content;
node.querySelector('.author').textContent = comment.author.name;
node.querySelector('.createdAt').textContent = comment.createdAt;
document.querySelector('#comments').appendChild(node);
}
+ 추가) MongoDB Aggregation
▶ Aggregation 이란?
- MongoDB에서 Document들을 가공하고 연산하는 기능
- RDBMS에서 SQL로 수행할 수 있는 기능들을 유사하게 구현할 수 있음 (ex. SQL의 GROUP BY, DISTINCT, COUNT, JOIN 등)
Aggregation을 사용하는 이유
: MongoDB의 find는 검색 필터링과 정렬 이외의 기능을 제공하지 않음
→ 다른 Collection에서 데이터를 가져오거나 검색된 데이터를 그룹화하는 등의 작업이 필요한 경우 Aggregation을 통해 이를 수행
aggregation은 Stage들의 배열로 이루어지고 각 Stage는 순차적으로 수행
db.posts.aggregate([
{ $group: { _id: '$author', count: { $sum: 1 } } },
{ $match: { sum: { $gt: 10 } } },
{ $lookup: { from: 'users', localField: '_id', foreignField: '_id', as: 'users' } },
]);
// 1. 작성자별 게시글 수를 취합하고
// 2. 게시글 수가 10개보다 많은 작성자를 찾아서
// 3. 해당 작성자를 회원 collection에서 검색
Aggregation Reference
Aggregation의 종류는 너무 다양하고 복잡하기 때문에 전부 외워서 사용할 필요는 없음
MongoDB 홈페이지에 Stage들의 설명과 예제 코드까지 잘 정리되어 있음
참고) MongoDB Aggregation
+) MongoDB Compass를 이용하면 Aggregations탭에서 Aggregations을 간단하게 테스트해볼 수 있음
이 글은 엘리스 AI트랙 5기 강의를 들으며 정리한 내용입니다.
'개발공부 > 🟩 Node.js' 카테고리의 다른 글
[NodeJS] Express와 MongoDB로 웹서비스 만들기 - 3 (2) (0) | 2022.08.05 |
---|---|
[NodeJS] Express와 MongoDB로 웹서비스 만들기 - 3 (1) (0) | 2022.08.05 |
[NodeJS] Express와 MongoDB로 웹서비스 만들기 - 2 (1) (0) | 2022.08.04 |
[NodeJS] Express와 MongoDB로 웹서비스 만들기 - 1 (2) (0) | 2022.08.02 |
[NodeJS] Express와 MongoDB로 웹서비스 만들기 - 1 (1) (0) | 2022.08.02 |
프론트엔드 개발자 삐롱히의 개발 & 공부 기록 블로그