Next.js 13+에서 무한 대댓글 구현 (with mongoDB)
Next.js 13+에서 무한 대댓글 구현 (with mongoDB)
📋 [ next.js ] 시리즈 몰아보기 (4)
요구사항
next.js
환경에서 무한 대댓글을 구현하려 합니다. 클라이언트에서 사용자가 댓글/대댓글을 작성하면 해당 댓글을 mongoDB
의 특정 collection에 저장, 변경사항이 게시글(클라이언트)에 즉시 반영되도록 할 것입니다.
무한 대댓글(혹은 대댓글) 이라고 하면 기본적으로 이런 구조를 생각해볼 수 있습니다.
대댓글을 달고자 하는 대상(부모댓글)의 하단에 대댓글(자식댓글)이 작성되고 그 대댓글을 대상으로 하는 또다른 대댓글을 작성... 이를 반복하는 것이죠.
댓글의 삽입 위치도 중요합니다. 중간에 대댓글이 끼어들더라도 기존의 순서가 무너지거나 하는 일은 없어야 할것입니다.
핵심 로직
위 요구사항을 충족하기 위해 계층형 트리구조
를 활용하여 대댓글 로직을 구현하려 합니다.
1 | export interface 댓글타입 { |
2 | _id: string; |
3 | REF: number; |
4 | RE_STEP: number; |
5 | RE_LEVEL: number; |
6 | date: string; |
7 | writter: string; |
8 | content: string; |
9 | } |
데이터 필드입니다. 대댓글 로직에 사용되는 주요 필드값에 대해 간단히 알아봅시다.
- REF : 댓글과 대댓글을 묶어 관리하기 위한
그룹
입니다. - RE_STEP : 같은 그룹 내에서의 댓글/대댓글의 절대적인
순서
를 의미합니다. 값이 작을수록 상단에 위치합니다. - RE_LEVEL : 들여쓰기 레벨. 즉 댓글의
깊이(depth)
를 의미합니다. - RE_PARENT :
부모댓글
의 식별 id입니다. 없어도 기능구현에는 문제 없지만 있으면 로직을 더 간단하게 구성할 수 있습니다.
다음 필드값들은 대댓글 로직에는 적용되지 않지만 댓글의 구분에 사용되는 값들입니다.
- _id : mongoDB에서 document(댓글)을 구분하는데 사용되는 고유 id입니다. 기본키로 이해하시면 되겠습니다.
- date : 댓글의 작성일자를 뜻합니다.
- writter : 댓글의 작성자를 뜻합니다.
- content : 댓글의 내용을 뜻합니다.
이렇게만 봐선 잘 모르겠습니다. step by step으로 자세히 알아보도록 합시다.
계층형 트리구조 활용
초코햄
유저의 첫번째 댓글에 대댓글 2개가 달려있습니다. 첫번째 댓글의 REF
값을 기준으로 하나의 댓글 그룹이 형성되어 있으며 RE_LEVEL
값을 기준으로 들여쓰기가 되어 있습니다.
1번 대댓글
이 2번 대댓글
보다 먼저 작성되었기 때문에 보다 상단에 위치해있고 이를 RE_STEP
으로 표현이 가능합니다.
여기서 1번 대댓글에 3번 대댓글
을 연이어 작성해보았습니다. RE_LEVEL의 값이 1
인 대댓글에 또다시 대댓글을 작성하였기 때문에 3번 대댓글의 RE_LEVEL의 값은 2
가 되어 들여쓰기가 2번 진행되었습니다.
또한 1번 대댓글에 대댓글을 작성하였기 때문에 1번과 2번 대댓글 사이에 3번 대댓글이 위치해야합니다. 때문에 2번 대댓글의 RE_STEP의 값이 1 증가하여 3
이 되었고 3번 대댓글의 RE_STEP 값이 2
가 되었습니다.
비슷한 상황입니다. 1번 대댓글에 또다시 새로운 대댓글 4번 대댓글
을 작성하였습니다. 이미 1번 대댓글을 부모로 둔 자식 대댓글(3번)이 존재하기 때문에 3번 대댓글의 뒤에 위치하는게 자연스럽습니다.
따라서 4번 대댓글의 RE_STEP 값은 3
이, 2번 대댓글의 RE_STEP의 값은 4
가 됩니다.
이 로직의 핵심은 자동으로 뒤에 위치할 댓글의 RE_STEP 값을 증가시키는 것입니다.
기능구현 - 클라이언트
우선 댓글을 볼 수 있는 view
와 사용자가 댓글을 작성할 form
을 작성해봅시다.
기능구현만을 위해 새로 작성하는 컴포넌트이기 때문에 css 코드가 제공되지 않으며 코드를 그대로 사용하시면 기대에 못미치는 결과화면이 나올 수 있기 때문에 최대한 이해한다는 것에 초점을 맞춰주시면 감사하겠습니다.
댓글 조회(view)
댓글을 조회하는 컴포넌트 CommentBox
는 기본적으로 다음과 같은 구조를 가집니다.
1 | // app/posts/[postId]/components/commentBox.tsx |
2 | |
3 | "use client" |
4 | |
5 | import { useState, useEffect } from 'react'; |
6 | import { commentHandler } from "../utils"; |
7 | import { 댓글타입 } from '../componentType'; |
8 | |
9 | export default function CommentBox(props: { postId: number }) { |
10 | const [comments, setComments] = useState<댓글타입[]>([]); |
11 | |
12 | useEffect(() => { |
13 | const getComment = async () => { |
14 | const res = await commentHandler(props.postId, "GET"); |
15 | setComments(res); |
16 | }; |
17 | |
18 | getComment(); |
19 | }, [props.postId]); |
20 | |
21 | return ( |
22 | {comments && comments.map((item: 댓글타입, i:number) => { |
23 | return ( |
24 | // ~~~ |
25 | ) |
26 | })} |
27 | ); |
28 | } |
CommentBox 컴포넌트는 기본적으로 게시글 번호 postId
를 파라미터로 받습니다.
해당 게시글의 모든 댓글을 담을 배열 comments
와 이를 제어하는 setComments
를 useState
변수로 선언, useEffect
내에서 댓글을 fetch하고 그 결과값을 comments에 입히고 map 메소드를 이용해 화면에 뿌려줍니다.
여기서 next.js에서 굳이 CSR
방식으로 댓글을 불러오는 이유는 다음과 같습니다.
댓글은 항상 신선한 데이터를 유지해야 합니다. 게시글은 정적배포(SSG) 하더라도 댓글은 상태가 바뀌는 즉시 클라이언트 및 DB에 반영되야 하기 때문에
SSR
,ISR
,CSR
방식 중 하나를 택해야 했습니다.클라이언트에서 댓글의 변경사항(작성/삭제)을 새로고침 없이 즉각 반영하기 위해서 입니다. 이를 위해선
React
의 hooks가 필요합니다.댓글은 언제나 게시글의
최하단
에 위치하기 때문입니다. 게시글을 모두 읽고 댓글을 확인할때 쯤에는 아무리 느리더라도 댓글의 로딩이 완료되어 있을 것이기 때문에 CSR의 단점은 보완하고 React의 interactive한 기능은 사용할 수 있습니다.
1 | // app/posts/[postId]/utils/commentHandler.ts |
2 | |
3 | import { 댓글타입 } from '../componentType'; |
4 | |
5 | export default function commentHandler(data: 댓글타입, type: string) { |
6 | switch (type) { |
7 | case "GET": |
8 | return getComment(data); |
9 | case "POST": |
10 | return addComment(data); |
11 | case "DELETE": |
12 | return deleteComment(data); |
13 | } |
14 | } |
15 | |
16 | /* ~~~getComment, addComment, deleteComment~~~ */ |
댓글의 각 액션을 제어하는 함수 commentHandler
입니다. 전달받은 methodType에 따라 각 상황에 맞는 함수를 매핑해줍니다. 여기서는 getPost
에 해당되겠습니다.
1 | async function getComment(postid: string) { |
2 | try { |
3 | const res = await fetch(`/api/comments?postid=${postid}`, { |
4 | method: "GET", |
5 | headers: { "Content-Type": "application/json" }, |
6 | cache: "no-store", |
7 | }); |
8 | |
9 | if (!res.ok) { |
10 | const failed = await res.json(); |
11 | throw new Error(failed.error as string); |
12 | } |
13 | const { comment } = await res.json(); |
14 | |
15 | return comment; |
16 | } catch (e: unknown) { |
17 | if (e instanceof Error) { |
18 | throw new Error(e.message); |
19 | } else { |
20 | throw new Error("Unknown error"); |
21 | } |
22 | } |
23 | } |
댓글을 불러오는 getComment
입니다. 포스트를 구분하는 postid를 쿼리에 담아 GET 요청을 보냅니다. 성공할 경우 결과값을, 그렇지 않을 경우 res의 각 status code에 따라 예외처리를 수행합니다.
신선한 데이터를 유지하기 위해 cache 옵션을 no-store
로 설정하였습니다. next.js에서 cache 옵션을 no-store로 설정할 경우 캐시데이터를 무시하고 매 요청마다 새로운 데이터를 불러옵니다.
1 | // ~~~ |
2 | import CommentForm from './CommentForm'; |
3 | |
4 | export default function CommentBox(props: { postId: number }) { |
5 | const [comments, setComments] = useState<댓글타입[]>([]); |
6 | const [replyClick, setReplyClick] = useState(false); |
7 | |
8 | // ~~~ |
9 | |
10 | return ( |
11 | <div> |
12 | {comments && comments.map((item: 댓글타입, i:number) => { |
13 | return ( |
14 | <div className="컨테이너" key={i}> |
15 | <div |
16 | className="댓글" |
17 | style={{ width: `${100 - item.RE_LEVEL * 6}%` }} |
18 | key={i} |
19 | > |
20 | <div className="댓글내용"> |
21 | <div className="정보"> |
22 | <span className="작성자">{item.writter}</span> |
23 | <span className="작성일자">{item.date}</span> |
24 | </div> |
25 | {item.content} |
26 | </div> |
27 | <div |
28 | className="대댓글달기_버튼" |
29 | onClick={() => { setTarget(item._id, "REPLY")}} |
30 | > |
31 | <p>{replyClick ? "닫기" : "대댓글달기"}</p> |
32 | </div> |
33 | {replyClick && commentId === item._id && ( |
34 | <CommentForm |
35 | data={item} |
36 | postId={props.postId} |
37 | type="REPLY" |
38 | setComments={setComments} |
39 | setClose={setReplyClick} |
40 | /> |
41 | )} |
42 | </div> |
43 | </div> |
44 | ) |
45 | })} |
46 | <CommentForm |
47 | data={comments ? comments.slice(-1)[0] : undefined} |
48 | postId={props.postId} |
49 | type="DEFAULT" |
50 | setComments={setComments} |
51 | /> |
52 | </div> |
53 | ); |
54 | } |
다소 복잡해보이지만 실은 간단합니다.
댓글은 각 item의 들여쓰기 레벨 RE_LEVEL
값에 따라 width값이 변환됩니다. default는 100%이며 RE_LEVEL이 1씩 증가할때 마다 6%씩 감소한 width값을 가지게 됩니다.
replyClick
은 대댓글 버튼의 클릭 여부를 뜻하며 해당 값에 따라 대댓글작성 폼이 노출될지 되지 않을지 결정됩니다. replyClick이 true
일 경우 대댓글작성 폼이 노출되며 필요한 데이터들이 전달됩니다. 필요한 데이터는 다음과 같습니다.
- data : 대댓글의 부모 댓글인
item
을 전달합니다. - postId : 해당 포스트의 postId를 전달합니다.
- type : 댓글의 타입입니다. 대댓글이므로
"REPLY"
를 전달합니다. - setComments : 대댓글작성 폼 내부에서 comments를 제어하기 위해 set 함수
setComments
를 전달합니다. - setClose : 마찬가지로 폼 내부에서 대댓글 작성 완료 후 자동으로 폼을 닫기 위해 set 함수
setReplyClick
을 전달합니다.
마지막으로 댓글을 모두 출력한 후 최하단에 별도의 댓글작성 폼을 추가합니다. 대댓글이 아닌 일반적인 댓글을 작성할 때 사용되기 때문에 전달할 데이터들은 다음과 같습니다.
- data : 해당 포스트에 존재하는 가장 마지막 댓글을 전달합니다. 댓글이 존재하지 않을 경우
undefined
를 전달합니다. - postId : 해당 포스트의 postId를 전달합니다.
- type : 댓글의 타입입니다. 일반댓글이므로
"DEFAULT"
를 전달합니다. - setComments : 대댓글작성 폼에서 comments를 제어하기 위해 set 함수
setComments
를 전달합니다. - setClose : 대댓글이 아니므로 전달하지 않습니다.
댓글 작성(write)
실질적으로 댓글을 작성하는 CommentForm
는 기본적으로 다음과 같은 구조를 가집니다.
1 | // app/posts/[postId]/components/CommentForm.tsx |
2 | |
3 | "use client" |
4 | |
5 | import { useState } from 'react'; |
6 | import { 댓글폼타입 } from '../componentType'; |
7 | |
8 | export default function CommentForm(props: 댓글폼타입) { |
9 | const [userComment, setUserComment] = useState<string>(""); |
10 | const [password, setPassword] = useState<string>(""); |
11 | const [userName, setUserName] = useState<string>(""); |
12 | |
13 | // 작성한 댓글 제출 |
14 | const submitComment = async (e: any) => { |
15 | /* ~~ */ |
16 | } |
17 | |
18 | return ( |
19 | <div className="댓글폼"> |
20 | <div> |
21 | <input |
22 | className="유저명" |
23 | value={setUserName} |
24 | onChange={(e: ChangeEvent<HTMLInputElement>) => |
25 | setUserName(e.target.value) |
26 | } |
27 | /> |
28 | <input |
29 | className="비밀번호" |
30 | value={password} |
31 | type="password" |
32 | onChange={(e: ChangeEvent<HTMLInputElement>) => |
33 | setPassword(e.target.value) |
34 | } |
35 | /> |
36 | </div> |
37 | <textarea |
38 | className="댓글" |
39 | value={userComment} |
40 | onChange={(e: ChangeEvent<HTMLInputElement>) => |
41 | setUserComment(e.target.value) |
42 | } |
43 | /> |
44 | <button onClick={submitComment}>Submit</button> |
45 | </div> |
46 | ); |
47 | } |
작성자의 이름, 비밀번호, 댓글 내용을 입력받아 각각 userName
, password
, userComment
에 저장합니다.
Submit 버튼을 클릭하면 submitComment
함수가 호출됩니다. 여기서 submitComment는 작성한 댓글을 POST하는 역할을 수행합니다.
1 | import { commentHandler } from "../../utils"; |
2 | |
3 | // ~~~ |
4 | |
5 | const submitComment = async (e: any) => { |
6 | e.preventDefault(); |
7 | if (!userComment || !userName || !password) { |
8 | alert("입력값 오류"); |
9 | return; |
10 | } |
11 | |
12 | const comment: 댓글입력타입 = { |
13 | REF: treeHandler.REF(props.data, props.type), |
14 | RE_STEP: treeHandler.RE_STEP(props.data, props.type), |
15 | RE_LEVEL: treeHandler.RE_LEVEL(props.data, props.type), |
16 | RE_PARENT: treeHandler.RE_PARENT(props.data, props.type), |
17 | postId: props.postId, |
18 | date: new Date(), |
19 | writter: userName, |
20 | password: password, |
21 | content: userComment, |
22 | }; |
23 | |
24 | // ~~~ |
25 | }; |
댓글을 전송하는 함수 submitComment
의 코드입니다. 유효하지 않은 입력값(댓글내용, 유저명, 비밀번호중 하나라도 입력하지 않았을 경우 예외처리를 수행해줍니다.
이후 댓글에 필요한 정보를 담은 객체 comment
를 타입에 맞게 선언합니다. 이때 REF, RE_STEP과 같은 댓글의 배치와 관련된 필드값들은 treeHandler
객체의 각 메소드의 return값으로 결정됩니다.
1 | const treeHandler: TreeHandlerType = { |
2 | REF(data, type) { |
3 | if (type === "DEFAULT") { |
4 | return data ? data.REF + 1 : 1; |
5 | } else { |
6 | return data.REF; |
7 | } |
8 | }, |
9 | |
10 | // ~~~ |
11 | } |
먼저 REF 입니다. 댓글의 타입이 DEFAULT
. 즉 대댓글이 아닌 일반적인 댓글일 경우 data 존재 여부에 따라 data의 REF값에 1을 증가시킨 값 혹은 1을 return합니다. data가 존재하지 않는다는 것은 작성한 댓글이 해당 포스트의 최초작성 댓글이라는 뜻입니다.
댓글의 타입이 대댓글일 경우 자신의 부모 댓글과 같은 그룹에 속해야 하기 때문에 data와 같은 REF값을 return합니다.
1 | const treeHandler: TreeHandlerType = { |
2 | // ~~~ |
3 | |
4 | RE_STEP(data, type) { |
5 | if (data && type === "REPLY") { |
6 | return data.RE_STEP; |
7 | } else { |
8 | return 0; |
9 | } |
10 | }, |
11 | } |
RE_STEP 입니다. data가 존재하며 댓글의 타입이 REPLY
일 경우 우선 data와 같은 RE_STEP값을 return합니다.
그렇지 않을 경우 0을 return합니다.
RE_STEP의 경우 추후에 값이 변경될 가능성이 높고 DB상에 존재하는 같은 그룹의 대댓글 여부에 따라 또 달라지기 때문에 댓글작성 단계에서 결정된 값이 곧바로 최종값으로 연결되지 않는다는 점을 기억해두시길 바랍니다.
1 | const treeHandler: TreeHandlerType = { |
2 | // ~~~ |
3 | |
4 | RE_LEVEL(data, type) { |
5 | if (data && type === "REPLY") { |
6 | return data.RE_LEVEL + 1; |
7 | } else { |
8 | return 0; |
9 | } |
10 | }, |
11 | } |
RE_LEVEL 입니다. 마찬가지로 data가 존재하며 댓글의 타입이 REPLY
일 경우 data의 RE_LEVEL 값에 1을 증가시킨 값을 return 합니다.
그렇지 않은 경우 0을 return합니다.
1 | const treeHandler: TreeHandlerType = { |
2 | // ~~~ |
3 | |
4 | RE_PARENT(data, type) { |
5 | if (data && type === "REPLY") { |
6 | return data._id; |
7 | } else { |
8 | return undefined; |
9 | } |
10 | }, |
11 | } |
RE_PARENT 입니다. 댓글의 타입이 REPLY
일 경우 data의 고유 id
를 return합니다.
그렇지 않을 경우 undefined
를 return합니다.
1 | const submitComment = async (e: any) => { |
2 | // ~~~ |
3 | |
4 | // 댓글 전송 |
5 | await commentHandler({ comment, commentType: props.type }, "POST"); |
6 | |
7 | // 입력값 초기화 및 데이터 리프레시 |
8 | initData(); |
9 | } |
10 | |
11 | const initData = async () => { |
12 | const newComment = await commentHandler(props.postId.toString(), "GET"); |
13 | props.setComments(newComment); |
14 | |
15 | setUserComment(""); |
16 | setPassword(""); |
17 | setUserName(""); |
18 | |
19 | if (props.type === "REPLY" && props.setClose) { |
20 | props.setClose(false); |
21 | } |
22 | }; |
이후 comment 객체를 commentHandler
에 댓글 타입과 함께 전달, 함수가 종료된 후 입력값을 초기화하고 데이터를 리프레시하는 initData
함수를 호출합니다.
initData에서는 입력값을 모두 빈 문자열로 초기화하고 최신 댓글 데이터를 불러와 comments에 적용, 대댓글을 전송했을 경우 props로 전달받은 setClose
의 값을 false
로 초기화하여 대댓글 입력 폼을 닫습니다.
이렇게하면 댓글작성 직후 새로고침 없이 댓글의 최신 내용이 클라이언트에 적용됩니다.
1 | async function addComment(data: { |
2 | comment: 댓글입력타입; |
3 | commentType: string; |
4 | }) { |
5 | const { comment, commentType } = data; |
6 | |
7 | try { |
8 | const myHeaders = new Headers({ "Content-Type": "application/json" }); |
9 | myHeaders.append("commenttype", commentType); |
10 | |
11 | const res = await fetch("/api/comments", { |
12 | method: "POST", |
13 | headers: myHeaders, |
14 | body: JSON.stringify(comment), |
15 | }); |
16 | |
17 | if (!res.ok) { |
18 | const failed = await res.json(); |
19 | throw new Error(failed.error as string); |
20 | } |
21 | |
22 | return; |
23 | } catch (e: unknown) { |
24 | if (e instanceof Error) { |
25 | throw new Error(e.message); |
26 | } else { |
27 | throw new Error("Unknown error"); |
28 | } |
29 | } |
30 | } |
commentHandler의 addComment
입니다.
댓글 타입을 헤더에 심어서 api에 POST 요청을 보냅니다. 성공했을 경우 그대로 return하고 실패했을 경우 res의 각 status code에 따라 예외처리를 수행해줍니다.
기능구현 - 서버
클라이언트에서 댓글의 내용을 body에 담아 서버에 POST 요청을 보낸 후 서버에서 취해야할 액션을 알아봅시다.
1 | myHeaders.append("commenttype", commentType); |
저희는 POST 요청 단계에서 헤더에 commenttype
을 담아서 요청을 보냈었습니다. 이 commenttype 속성을 기준으로 일반 댓글인지 대댓글인지 구분하여 각각 다른 메소드가 호출되도록 하겠습니다.
1 | // app/api/comments/route.ts |
2 | |
3 | import { NextRequest, NextResponse } from "next/server"; |
4 | import { addComment, addReply } from "./POST"; |
5 | |
6 | export async function POST(req: NextRequest) { |
7 | const commentType = req.headers.get("commenttype"); |
8 | |
9 | switch (commentType) { |
10 | case "DEFAULT": |
11 | return addComment(req); |
12 | case "REPLY": |
13 | return addReply(req); |
14 | default: |
15 | return NextResponse.json( |
16 | { error: "invalid comment type" }, |
17 | { status: 404, headers: { "Content-Type": "application/json" } } |
18 | ); |
19 | } |
20 | } |
api 폴더에 route.ts
를 생성하고 댓글타입이 DEFAULT
이면 addComment
를, REPLY
면 addReply
를, 둘다 아니라면 에러를 반환하도록 합니다.
일반댓글(addComment)의 경우 전달받은 body를 그대로 DB에 저장하면 되기 때문에 본문에서는 생략하고 곧바로 대댓글작성 api를 작성하도록 하겠습니다.
대댓글 필드값 재조정
우선 대댓글작성 api의 기본골조는 다음과 같습니다.
1 | // app/api/comments/POST/addReply.ts |
2 | |
3 | import { NextRequest, NextResponse } from "next/server"; |
4 | import { connectToDatabase } from "util/mongodb"; |
5 | |
6 | export default async function addReply(req: NextRequest) { |
7 | try { |
8 | const { db } = await connectToDatabase(); |
9 | let newBody = await req.json(); |
10 | |
11 | /* ~~~데이터 전처리~~~ */ |
12 | |
13 | await db.collection("comments").insertOne(newBody); |
14 | |
15 | return NextResponse.json( |
16 | { message: `comment added successfully at ${newBody.postId} : REPLY` }, |
17 | { status: 200, headers: { "Content-Type": "application/json" } }, |
18 | ); |
19 | } catch (e: unknown) { |
20 | const error = e + ""; |
21 | console.log(error); |
22 | |
23 | return NextResponse.json( |
24 | { error: `Error : ${error}` }, |
25 | { status: 500, headers: { "Content-Type": "application/json" } }, |
26 | ); |
27 | } |
db
객체를 통해 mongoDB에 접근하는 구조이며 request에 담긴 body를 json 형태로 파싱, 변수 newBody
에 담아 일련의 전처리 과정을 거친 후 comments
collection에 데이터를 삽입합니다.
성공적으로 작업이 완료되었다면 성공 메세지를, 실패했다면 터미널에 에러를 출력, 에러 메세지를 반환하도록 합니다.
데이터의 전처리란 다음과 같은 과정을 의미합니다.
핵심로직 설명 구간에서 대댓글 사이에 대댓글을 입력할 경우 입력한 대댓글에 의해 기존의 대댓글이 뒤로 밀려나는 경우가 발생할 수 있습니다. 댓글의 순서는 RE_STEP 값에 의해 결정되므로 입력한 대댓글 뿐 아니라 뒤로 밀려난 대댓글의 RE_STEP값 또한 조정이 필요합니다.
단순히 부모 댓글의 대댓글이 이미 존재하여 입력한 대댓글의 RE_STEP값을 조정하는 경우 또한 포함합니다.
1 | let lastComment = await db |
2 | .collection("comments") |
3 | .find({ |
4 | REF: newBody.REF, |
5 | RE_LEVEL: newBody.RE_LEVEL, |
6 | RE_PARENT: newBody.RE_PARENT, |
7 | }) |
8 | .sort({ RE_STEP: -1 }) |
9 | .limit(1) |
10 | .toArray(); |
입력한 대댓글과 비교할 대댓글 데이터를 조회합니다.
본인과 같은 그룹(REF)에 존재하며 같은 들여쓰기 레벨(RE_LEVEL) 값을 가지고 있고 같은 부모댓글(RE_PARENT)를 가지고 있는 대댓글들을 추려낸 뒤 가장 마지막에 위치한 댓글의 데이터를 변수 lastComment
에 대입합니다.
굳이 toArray()
를 이용해 배열의 형태로 데이터를 뽑은 이유는 단순히 데이터를 다루기 용이하기 때문입니다. mongoDB에서 데이터의 기본 형태는 json이기 때문에 json 데이터를 그대로 사용하셔도 됩니다.
1 | let targetStep = 0; |
2 | |
3 | if (lastComment.length > 0 && lastComment[0].RE_STEP > newBody.RE_STEP) { |
4 | targetStep = lastComment[0].RE_STEP + 1; |
5 | newBody.RE_STEP = targetStep; |
6 | } else { |
7 | targetStep = newBody.RE_STEP + 1; |
8 | newBody.RE_STEP = targetStep; |
9 | } |
우선 입력한 대댓글(대댓글 원문)의 RE_STEP값을 조정합니다. 변수 targetStep
을 선언한 후 다음 로직을 수행합니다.
lastComment가 존재하고 lastComment의 RE_STEP 값이 대댓글 원문의 RE_STEP보다 클 경우 targetStep의 값을 lastComment.RE_STEP + 1로 조정합니다.
비교대상이 존재하지 않을 경우 기존 RE_STEP의 값에 1을 더해줍니다. 대댓글 원문의 RE_STEP값은 부모 댓글의 RE_STEP값이기 때문에 lastComment가 존재하지만 대댓글 원문보다 RE_STEP의 값이 작은 경우는 없습니다.
1 | await db.collection("comments").updateMany( |
2 | { |
3 | REF: newBody.REF, |
4 | RE_STEP: { $gte: targetStep }, |
5 | }, |
6 | { $inc: { RE_STEP: 1 } }, |
7 | ); |
대댓글 원문의 RE_STEP값 조정이 끝났다면 원문에 의해 뒤로 밀려날 댓글들의 RE_STEP 값을 조정할 차례입니다.
원문과 같은 그룹에 속해있으며(REF) RE_STEP의 값이 원문의 RE_STEP 값보다 크거나 같은 모든 댓글들의 RE_STEP 값을 1 증가시킵니다.
이것으로 데이터 전처리 과정은 끝입니다. 이후에는 조정된 newBody를 DB에 전송, 문제가 없으면 성공 메세지를 반환합니다.
1 | await db.collection("comments").insertOne(newBody); |
2 | |
3 | return NextResponse.json( |
4 | { message: `comment added successfully at ${newBody.postId} : REPLY` }, |
5 | { status: 200, headers: { "Content-Type": "application/json" } }, |
6 | ); |
👨💻 관련 포스트
[next.js] CDN 서버를 활용한 Markdown 이미지 첨부(1)
[next.js] CDN 서버를 활용한 Markdown 이미지 첨부(1)
마크다운에서 이미지를 첨부하기 위해선 img태그의 src에 url을 전달하듯 url 형식의 문자열을 전달해야 합니다. 때문에 README를 작성하거나 저처럼 블로그 등지에서 마크다운을 활용하는 경우 첨부하고자 하는 이미지의 전처리 과정이 요구됩니다. 이미지 파일을 base64 형식으로 인코딩하여 안전한 문자열로 사용할 수 있습니다. HTTP 통신에서 JSON 형태의 통신을 위해 base64 형태로 인코딩할 필요가 있습니다. blob 객체 또한 이용 가능합니다. js의 내부 메소드로 간단하게 blob 객체로 인코딩, 브라우저에서 가상 url을 생성할 수 있습니다.
2023-11-12
[next.js] 네이버 CDN 서버를 활용한 Markdown 이미지 첨부(2)
[next.js] 네이버 CDN 서버를 활용한 Markdown 이미지 첨부(2)
안전한 HTTP 통신을 위해 이미지 파일을 base64 형태로 인코딩, next.js 서버단에서 CDN 서버에 업로드 하는것이 이상적입니다. github issue에 이미지를 등록하는것은 CDN 서버를 구성할 필요 없이 간편하게 이미지를 업로드 할 수 있지만 궁극적인 해결방안은 아닙니다. 네이버 클라우드 플랫폼의 CDN+와 Object Storage, AWS S3 bucket를 이용해 CDN 서버를 구성합니다. 네이버 클라우드 플랫폼의 API 인증키를 발급받고 서비스 사용 신청을 합니다.
2023-11-15
네이버 CDN 서버를 활용한 Markdown 이미지 첨부(3)
네이버 CDN 서버를 활용한 Markdown 이미지 첨부(3)
AWS S3에서 제공하는 javascript SDK를 이용해 node.js 환경에서 파일(이미지)를 api 요청을 통해 전송해보겠습니다. @aws-sdk/client-s3 모듈을 설치 후 next.js의 원하는 api 엔드포인트에서 route.ts를 작성합니다. S3 클라이언트에 연결하기 위한 객체를 선언하고 접근권한(ACL), 경로 등을 설정하고 Buffer 객체에 이미지를 담아 S3에 전송합니다. 바이너리 데이터로 이미지를 변환해 api의 요청사항을 만족시킵니다. 이미지를 별도의 DB에 분산저장(백업) 하여 데이터의 무결성을 유지합니다.
2023-12-17
💡 로그인 하지 않아도 댓글을 등록할 수 있습니다!