댓글 대댓글 구현은 상세 게시글 페이지 중 가장 시간을 많이 썼던 부분이다.
특징은 다음과 같다.
- 최상단에 댓글 입력창이 있고 그 밑에 댓글과 대댓글이 보인다.
- 댓글은 작성된 순서대로 정렬된다.
- 댓글 창에는 본인이 쓴 글이라면 수정 및 삭제 버튼이 보인다.
- 수정 버튼을 누르면 해당 댓글란이 기존 댓글 내용을 담은 입력창으로 바뀌고 수정 및 삭제 버튼이 등록 및 취소 버튼으로 바뀐다.
- 삭제 버튼을 누르면 댓글이 삭제된다.
- '댓글 추가' 버튼을 누르면 해당 댓글 밑으로 새로운 입력창이 뜨고 대댓글을 입력할 수 있다.
- 대댓글 입력창이 뜨면 '댓글 추가' 버튼은 '취소' 버튼으로 바뀌고, 이를 클릭하면 입력창이 사라진다.
- 대댓글이 입력되면 해당 댓글 바로 아래에 작성 순서대로 정렬된다.
- 댓글과 마찬가지로 수정과 삭제가 가능하다.
- 대댓글에 대한 대대댓글(?)은 달 수 없다. (무한 댓글 아님)
이 중 댓글 및 대댓글 CRUD 상태 관리 코드를 짜는데 가장 애를 먹었다.
댓글 데이터 구조
백엔드에서 넘겨준 로직은 댓글일 경우에는 parentId가 0이고, 대댓글일 경우에는 parentId가 해당 모댓글의 commentId를 가리키는 형식이었다.
// 댓글일 경우
{
commentId: 1,
content: "저는 벤츠 전기차 타고 다니는데 드라이브 같이 한 번 가실래여?ㅋㅋㅋ"
createdAt: "2023-05-25T06:55:06.183+00:00"
nickname: "원빈",
parentId: 0,
userPhoto: "https://mainplestory.s3.ap-northeast-2.amazonaws.com/2",
},
...
// 대댓글일 경우
{
commentId: 117,
content: "으악!!"
createdAt: "2023-06-22T10:53:44.236+00:00"
nickname: "냥이",
parentId: 1,
userPhoto: "https://mainplestory.s3.ap-northeast-2.amazonaws.com/4",
},
대댓글은 해당 댓글의 바로 아래에 위치해야 한다. 그래서 댓글 안에 해당하는 대댓글을 넣어주면 댓글 하나 그릴 때 함께 묶여서 그려 나오지 않을까 하는 생각에 replies(대댓글)를 key로 새로 넣어 배열로 모댓글에 넣어주는 함수를 새로 짰다.
function groupCommentsAndReplies(commentData: CommentType[]): CommentType[] {
const commentDict: {
[key: number]: CommentType & { replies: CommentType[] };
} = commentData.reduce<{
[key: number]: CommentType & { replies: CommentType[] };
}>((dict, comment) => {
dict[comment.commentId] = { ...comment, replies: [] };
return dict;
}, {});
// 댓글 순회하며 대댓글을 모댓글에 넣어주기
commentData.forEach((comment) => {
if (comment.parentId !== 0) {
const parentComment = commentDict[comment.parentId];
if (parentComment) {
parentComment.replies.push(comment);
}
}
});
// 댓글만 필터링
const topLevelComments = Object.values(commentDict).filter(
(comment) => comment.parentId === 0
);
return topLevelComments;
}
export default groupCommentsAndReplies;
이 작업을 거치고 나면 댓글 데이터 구조는 아래와 같이 변한다.
{
commentId: 1,
content: "저는 벤츠 전기차 타고 다니는데 드라이브 같이 한 번 가실래여?ㅋㅋㅋ"
createdAt: "2023-05-25T06:55:06.183+00:00"
nickname: "원빈",
parentId: 0,
replies: {
commentId: 117,
content: "으악!!"
createdAt: "2023-06-22T10:53:44.236+00:00"
nickname: "냥이"
parentId: 1
userPhoto: "https://mainplestory.s3.ap-northeast-2.amazonaws.com/4"
}
userPhoto: "https://mainplestory.s3.ap-northeast-2.amazonaws.com/2",
},
댓글 CRUD
사전 제출 시간이 다가오는데 상태 관리 코드를 다 못 짜서 댓글 생성, 업데이트, 삭제가 페이지를 새로고침을 해야만 보였다.
처음에는 window.location.reload()
를 넣었는데 그러면 화면이 깜빡여서 UX가 좋지 않았다.
그래서 두번째로 생각한 건 useEffect
를 사용해서 숫자 1씩 올려줌으로써 페이지가 깜빡이지 않고 리렌더링하는 방법이었다. 하지만 이 경우는 만약 댓글 수가 많아지거나 컴포넌트가 복잡해지면 매번 리렌더링 하면서 퍼포먼스에 부정적인 영향을 줄 수 있다.
결론은 무조건 상태로 로컬에서도 관리해야 한다는 것이다.
상태 관리 전에는 게시글 상세 페이지에서 데이터를 받아서 댓글란 컴포넌트에 해당 게시글 아이디(boardId)와 댓글(comment)을 props로 내려주고 댓글란에서 댓글에, 댓글에서 대댓글에 내려주는 걸로 짰다.
이후에는 게시글 상세 페이지에서 데이터를 받아올 때 댓글을 상태에 넣어주고, 댓글란에서 바로 꺼내쓰도록 바꾸었다.
useEffect(() => {
const fetchPost = async () => {
const data = await getPostData(memberId, parseInt(boardId, 10));
if (data && data.comments) {
const commentsWithReplies = groupCommentsAndReplies(data.comments);
dispatch(setComments(commentsWithReplies));
} else if (!data) {
setPostDeleted(true);
}
setPost(data);
};
fetchPost();
}, [dispatch, memberId, boardId]);
리덕스 툴킷 슬라이스는 다음과 같다.
const commentsSlice = createSlice({
name: "comments",
initialState,
reducers: {
setComments: (state, action: PayloadAction<CommentType[]>) => {
return {
...state,
comments: action.payload,
};
},
},
});
댓글 세팅을 마치면 이제 댓글을 추가하는 리듀서도 적을 수 있다.
addComment: (state, action: PayloadAction<CommentType>) => {
return {
...state,
comments: [...state.comments, action.payload],
};
},
등록 버튼을 누르자마자 새로고침 없이 빠르게 로컬에서 댓글 추가가 잘 된다.
여기까지하면 댓글 수정 및 삭제는 다른 곳에서 CRUD 연습했던 것처럼 쉽게 짤 수 있다.
대댓글 CRUD
댓글과 큰 틀은 다르지 않지만, 대댓글은 parentId와 commentId가 같은 댓글을 찾아 replies 값에 추가해주는 방식이다.
addReply: (
state,
action: PayloadAction<{ commentId: number; reply: CommentType }>
) => {
const { commentId, reply } = action.payload;
return {
...state,
comments: state.comments.map((comment) =>
comment.commentId === commentId
? {
...comment,
replies: comment.replies
? [...comment.replies, reply]
: [reply],
}
: comment
),
};
},
나머지 코드는 아래 레포지토리에서 보실 수 있습니다.
회고
사실 아직도 댓글 대댓글 최선의 구조가 무엇인지 잘 모르겠다. 프로그래밍은 항상 이게 정답이다! 하는 건 없지만 그래도... 정석을 보고 싶은 마음...
또 다른 사람들이 한 것을 찾아보니 보통 백엔드에서 애초에 내가 바꾼 구조처럼 넘어오는 거 같았다. 스스로 코드를 더 쳐 볼 수 있어서 좋았지만 멘토님께서 말씀하신 것처럼 인터페이스를 처음에 백엔드와 함께 짰어야 한다는 말이 다시 한번 와닿은 경험이었다.
댓글 입력 컴포넌트는 따로 파일로 빼주는 게 나을까, 아니면 비교적 간단한 부분이니 그냥 나두는 게 나을까 고민이다.
boardId도 지금은 props로 내려주고 있는데, 상태 관리로 하면 좋을 거 같다. 그리고 지금은 댓글이나 대댓글 삭제 시 그냥 뿅 삭제 되는데, 게시글 삭제처럼 모달이 뜨면 더 그럴싸한 웹사이트가 될 거 같다.
'Development > Frontend' 카테고리의 다른 글
Elog: Event Bubbling (0) | 2024.05.02 |
---|---|
[회원 정보 수정] react-hook-form (0) | 2023.07.19 |
[게시글 상세] 게시글 상세 페이지 구조 (0) | 2023.06.22 |
[Navbar] Redux-toolkit 활용: 로그인/비로그인 구분, 프로필 관리 (1) | 2023.06.09 |
프로젝트 시작하기: Vite + React in TypeScript + ESLint + Prettier (2) | 2023.06.06 |