반응형

 

이전글:

2020/11/03 - [React] - [React][CRUD] 간단한 게시판 페이지 만들어보기 - 14. 댓글 달기(우선 댓글 저장부터), redux-toolkit, redux-saga, redux, reducer, useState, useDispatch, json-server, axios

 

[React][CRUD] 간단한 게시판 페이지 만들어보기 - 14. 댓글 달기(우선 댓글 저장부터), redux-toolkit, redu

목차 돌아가기: binaryjourney.tistory.com/pages/ReactCRUD-create-board-tutorial [React][CRUD] create-board-tutorial code: github.com/jwlee-lnd/react-create-board jwlee-lnd/react-create-board Descrip..

binaryjourney.tistory.com

 

 

목차 돌아가기: 

binaryjourney.tistory.com/pages/ReactCRUD-create-board-tutorial

 

[React][CRUD] create-board-tutorial

code: github.com/jwlee-lnd/react-create-board jwlee-lnd/react-create-board Description(korean) : https://binaryjourney.tistory.com/pages/ReactCRUD-create-board-tutorial - jwlee-lnd/react-create-boar..

binaryjourney.tistory.com

 

 

 

 

 

 

url 구조가 이렇게 되어 있다면

 

http://localhost:portNumber/resources/parameter?queryString=stringValue

 

 

 

json-server에서는 이렇게 받아들인다.

 

http://localhost:포트번호/Resources/id

 

 

 

 

 

 

그래서 (json-server 켜진 상태임)

 

http://localhost:4000/comment/1

 

이렇게 주소를 입력하면

 

 

 

comment id:1

 

댓글 id가 1번인 데이터를 가져오고

 

 

 

 

 

 

 

http://localhost:4000/comment/2

를 입력하면

 

comment id:2

 

댓글 id가 2번인 데이터를 가져온다.

 

 

 

 

댓글은 댓글의 아이디를 각각 조회해서 게시글 아이디와 맞춰보고 가져오는 게 아니라 그냥 조회한 게시글 id를 포함하고 있는 댓글 데이터만 뽑아 리스트로 가져오는 것이다.

 

 

 

 

기본 db구조만 있는 json-server에서 foreign key를 어떻게 사용하는지 궁금할테지만

 

거창한 방법은 아니고 쿼리를 이용해서 원하는 정보만 get 해올 수 있다.

 

 

 

 

 

 

 

 

http://localhost:4000/comment?articleId=1

 

주소창에 이렇게 입력하면

 

 

comment list

 

 

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);

 

 

 

 

 

 

 

개발자 창에 데이터가 보이는지 알아보자.

 

 

devTools

 

 

 

불러온 데이터가 맞는지 json server와도 비교해보자!

 

devTools

 

 

 

 

 

 

 

데이터가 잘 불려왔으니 이젠 뷰에 뿌릴 차례다

 

 

 

 

 

 

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>

 

 

 

 

 

 

작성한 파일들을 저장하고 화면에 어떻게 나오는지 봐보자

 

comments

 

 

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>

 

 

article page

 

 

 

 

삭제버튼 너무 눈에 띄어서 default 값으로 바꿔주겠다.

 

// ArticleDetail

      <div style={{ margin: "auto" }}>
        <Button onClick={props.handleDeleteClick}>삭제</Button>
      </div>

 

 

pretty pretty

 

 

 

 

 

 

 

 

게시글 스타일 바꾼 김에 다른 페이지 스타일도 바꿔보겠다.

 

 

 

 

일단 첫화면

 

 

// 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;

 

 

 

 

prettier prettier

 

 

 

 

 

등록 화면

 

 

// 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;

 

 

 

satisfied satisfied

 

 

 

 

 

 

 

 

 

 

아직 다 끝난 건 아니고 다음 편에서는 댓글 삭제와 게시글 목록에서 댓글 개수를 나타내는 것까지 해보겠다.

 

 

 

 

 

목차 돌아가기: binaryjourney.tistory.com/pages/ReactCRUD-create-board-tutorial

 

[React][CRUD] create-board-tutorial

code: github.com/jwlee-lnd/react-create-board jwlee-lnd/react-create-board Description(korean) : https://binaryjourney.tistory.com/pages/ReactCRUD-create-board-tutorial - jwlee-lnd/react-create-boar..

binaryjourney.tistory.com

 

반응형

+ Recent posts