이전글:
목차 돌아가기:
binaryjourney.tistory.com/pages/ReactCRUD-create-board-tutorial
url 구조가 이렇게 되어 있다면
http://localhost:portNumber/resources/parameter?queryString=stringValue
json-server에서는 이렇게 받아들인다.
http://localhost:포트번호/Resources/id
그래서 (json-server 켜진 상태임)
http://localhost:4000/comment/1
이렇게 주소를 입력하면
댓글 id가 1번인 데이터를 가져오고
http://localhost:4000/comment/2
를 입력하면
댓글 id가 2번인 데이터를 가져온다.
댓글은 댓글의 아이디를 각각 조회해서 게시글 아이디와 맞춰보고 가져오는 게 아니라 그냥 조회한 게시글 id를 포함하고 있는 댓글 데이터만 뽑아 리스트로 가져오는 것이다.
기본 db구조만 있는 json-server에서 foreign key를 어떻게 사용하는지 궁금할테지만
거창한 방법은 아니고 쿼리를 이용해서 원하는 정보만 get 해올 수 있다.
http://localhost:4000/comment?articleId=1
주소창에 이렇게 입력하면
articleId는 1로 같으나 id가 unique한 댓글리스트를 가져온다.
바로 적용해보자!
commentSlice 를 다음과 같이 수정하였다.
// commentSlice
import { createSlice } from "@reduxjs/toolkit";
export const commentSlice = createSlice({
name: "comment",
initialState: {
id: 0,
content: "",
date: Date.now(),
articleId: 0,
comments: [],
},
reducers: {
registerComment: (state, { payload: comment }) => {
console.log("댓글 등록 액션 호출 -- registerComment"); // saga 애서 감시용
},
getComments: (state, { payload: articleId }) => {
console.log("댓글 불러오기 액션 호출 -- getComments"); // saga 에서 감시용
},
getCommentsAsync: (state, { payload: list }) => {
return {
...state,
comments: list,
};
},
},
});
export const commentReducers = commentSlice.reducer;
export const commentActions = commentSlice.actions;
saga에서 getComments 액션을 낚아채 데이터를 불러온 후 getCommentsAsync 액션에 받은 데이터를 태워 보낼 것이다.
말한 대로 commentSaga에 다음 함수를 추가하자
// commentSaga
export function* getCommentsAsync(action) {
const articleId = action.payload;
const response = yield Axios.get(
`http://localhost:4000/comment?articleId=${articleId}`
);
yield put(commentActions.getCommentsAsync(response.data));
}
그리고 rootSaga에 낚아챌 액션과 방금 만든 saga 함수를 적는다.
// rootSaga
import { take, takeEvery, takeLatest } from "redux-saga/effects";
import { articleActions } from "../slice/articleSlice";
import { boardActions } from "../slice/boardSlice";
import { commentActions } from "../slice/commentSlice";
import {
registerArticleAsync,
getArticleAsync,
fetchArticleAsync,
updateArticleAsync,
deleteArticleAsync,
} from "./articleSaga";
import { getBoardAsync } from "./boardSaga";
import { registerCommentAsync, getCommentsAsync } from "./commentSaga";
const {
registerArticle,
getArticle,
fetchArticle,
updateArticle,
deleteArticle,
} = articleActions;
const { getBoard } = boardActions;
const { registerComment, getComments } = commentActions;
export default function* rootWatcher() {
yield takeLatest(registerArticle.type, registerArticleAsync);
yield takeEvery(getArticle.type, getArticleAsync);
yield takeEvery(getBoard.type, getBoardAsync);
yield takeEvery(fetchArticle.type, fetchArticleAsync);
yield takeLatest(updateArticle.type, updateArticleAsync);
yield takeLatest(deleteArticle.type, deleteArticleAsync);
yield takeLatest(registerComment.type, registerCommentAsync);
yield takeEvery(getComments.type, getCommentsAsync);
}
이제 뷰인 ArticlePage 에 적용시킬 차례인데
화면이 로드될 때 댓글 목록도 같이 불러와야 하므로
원래 있던 useEffect 에 새로 만든 액션을 dispatch하는 코드만 넣어준다.
// ArticlePage
useEffect(() => {
dispatch(articleActions.getArticle(match.params.articleId));
dispatch(commentActions.getComments(match.params.articleId)); // 추가된 부분
}, [match.params.articleId]);
그리고 불러온 댓글로 변경될 state 를 잡아올 useSelector도 추가한다.
// ArticlePage
const comments = useSelector((state) => state.commentReducers.comments);
console.log(comments);
개발자 창에 데이터가 보이는지 알아보자.
불러온 데이터가 맞는지 json server와도 비교해보자!
데이터가 잘 불려왔으니 이젠 뷰에 뿌릴 차례다
ArticlePage의 return 부분에 loadComments란 이름으로 useSelector로 잡아온 comments를 ArticleDetail에 넘겨준다.
// ArticlePage
return (
<div style={{ width: "80%", margin: "3rem auto" }}>
<div>
<ArticleDetail
id={id}
title={title}
content={content}
views={views}
date={date}
handleDeleteClick={onDeleteClick}
handleComment={
<Comment
comment={CommentValue}
handleCommentChange={onCommentChange}
handleCommentSubmit={onCommentSubmit}
/>
}
loadComments={comments}
/>
</div>
</div>
);
ArticleDetail은 넘겨준 데이터를 props.loadComments 로 받아 map을 이용하여 댓글 id 별로 나눠준다.
// ArticleDetail
<div>
{props.loadComments.map((comment) => (
<div>
<span key={comment.id}>
<span>{comment.content}</span>
<span>{new Date(comment.date).toLocaleString()}</span>
</span>
</div>
))}
</div>
작성한 파일들을 저장하고 화면에 어떻게 나오는지 봐보자
style을 주지 않아 예쁘진 않지만 그래도 잘 나와서 만족한다.
댓글 저장과 게시가 한꺼번에 잘 되는지 이제 댓글을 마구마구 등록해보자
조금 느리긴 한데 아마 서버를 여러번 왔다갔다 해서 그런 것 같다.
사실 서버단에서 게시글과 댓글을 조인해서 한꺼번에 갖고 오는 것이 훨씬 좋다.
이건 json server라 조인까지는 할 수 없겠지만
나중에 saga에서 게시글 내용 조회와 동시에 댓글 가져오는 것까지 한 번에 할 수 있도록 리팩토링해보겠다.
부족한 실력이지만 스타일 조금만 바꿔봤다.
// ArticleDetail
<div>
{props.loadComments.map((comment) => (
<div
style={{
width: "100%",
backgroundColor: "lightsteelblue",
border: "1px dotted black",
}}
>
<span key={comment.id}>
<span>{comment.content}</span>
<span style={{ float: "right" }}>
{new Date(comment.date).toLocaleString()}
</span>
</span>
</div>
))}
</div>
삭제버튼 너무 눈에 띄어서 default 값으로 바꿔주겠다.
// ArticleDetail
<div style={{ margin: "auto" }}>
<Button onClick={props.handleDeleteClick}>삭제</Button>
</div>
게시글 스타일 바꾼 김에 다른 페이지 스타일도 바꿔보겠다.
일단 첫화면
// BoardPage
import React, { useEffect } from "react";
import { Link } from "react-router-dom";
import BoardList from "./Sections/BoardList";
import { Button, Typography } from "antd";
import { useDispatch, useSelector } from "react-redux";
import { boardActions } from "../../../slice/boardSlice";
import { articleActions } from "../../../slice/articleSlice";
const { Title } = Typography;
function BoardPage({ match }) {
const dispatch = useDispatch();
console.log(match);
useEffect(() => {
dispatch(boardActions.getBoard());
}, [dispatch]);
const { board, isLoading, isSuccess, error } = useSelector((state) => ({
board: state.boardReducers.board,
isLoading: state.boardReducers.isLoading,
isSuccess: state.boardReducers.isSuccess,
error: state.boardReducers.error,
}));
const onDeleteClick = (id) => {
if (!window.confirm("삭제하시겠습니까?")) return false;
dispatch(articleActions.deleteArticle(id));
};
return (
<div style={{ width: "80%", margin: "3rem auto" }}> {/* 수정된 부분 */}
<div>
<Link to="/register?isForEdit=false">
<Button type="primary">New Post</Button>
</Link>
</div>
<div style={{ textAlign: "center", marginBottom: "2rem" }}>
<Title>Board Title</Title>
</div>
<div>
{error ? (
<h2>에러 발생: {error}</h2>
) : isSuccess && board.length > 0 ? (
<BoardList board={board} handleDeleteClick={onDeleteClick} />
) : isSuccess && board.length <= 0 ? (
<p> 조회할 내용이 없습니다. </p>
) : (
<p> 목록을 불러오는 중입니다. </p>
)}
</div>
</div>
);
}
export default BoardPage;
// BoardList
import React from "react";
import { Link } from "react-router-dom";
import { Button } from "antd";
function BoardList(props) {
// console.log(props.board);
return (
<div>
<table style={{ width: "100%" }}> {/* 수정된 부분 */}
<colgroup>
<col width="10%" />
<col width="70%" />
<col width="10%" />
<col width="10%" />
</colgroup>
<tbody>
<tr>
<th>번호</th>
<th>제목</th>
<th>조회수</th>
<th></th>
</tr>
</tbody>
<tbody>
{props.board.map((article) => (
<tr key={article.id}>
<td>{article.id}</td>
<Link to={`/article/${article.id}`}>
<td>{article.title}</td>
</Link>
<td>{article.views}</td>
<td>
<Button onClick={() => props.handleDeleteClick(article.id)}>
X
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
// boardList.map => Link to = `/${id}`
export default BoardList;
등록 화면
// RegisterOrEdit
import React from "react";
import { Button, Input } from "antd";
const { TextArea } = Input;
function RegisterOrEdit(props) {
return (
<div style={{ width: "80%", margin: "3rem auto" }}> {/* 수정된 부분*/}
<a href="/">
<Button>←</Button>
</a>
<form onSubmit={props.handleSubmit}>
<br />
<div style={{ width: "80%", margin: "2rem auto" }}> {/* 수정된 부분*/}
<label>Title: </label>
<Input
onChange={props.handleTitleChange}
value={props.titleValue}
type="text"
name="title"
/>
<hr></hr>
<TextArea
rows="30"
onChange={props.handleContentChange}
value={props.contentValue}
name="content"
/>
</div>
<Button type="primary" onClick={props.handleSubmit}>
{props.updateRequest ? "수정" : "등록"}
</Button>
</form>
</div>
);
}
export default RegisterOrEdit;
아직 다 끝난 건 아니고 다음 편에서는 댓글 삭제와 게시글 목록에서 댓글 개수를 나타내는 것까지 해보겠다.
목차 돌아가기: binaryjourney.tistory.com/pages/ReactCRUD-create-board-tutorial