반응형

 

 

목차로 돌아가기: 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-..

binaryjourney.tistory.com

 

 

 

 

 

렌더링 최적화를 위해 useState로 사용했던 

 

 

 

const [TitleValue, setTitleValue] = useState("")
const [ContentValue, setContentValue] = useState("")

 

 

 

요 useState 구문을 useSelector를 이용하여 코드도 줄이고 reducer를 재활용하는 방법과 렌더링 최적화를 실천해보겠다.

 

 

 

 

 

 

useSelector를 이용한다는 것은 redux-toolkit의 createSlice로 만든 slice에 initialState 로 이미 객체변수가 존재함을 전제로 한다.

 

 

 

 

 

내가 리팩토링하려는 RegisterPage 를 보면 이미 useSelector로 initialState를 다 가져다 놓은 상태여서 정말 아주 조금의 편집만 하는 정도이다.

 

// RegisterPage.js

import React, { useEffect, useState } from "react";
import { shallowEqual, useDispatch, useSelector } from "react-redux";
import RegisterOrEdit from "./Sections/RegisterOrEdit";
import { articleActions } from "../../../slice/articleSlice";

function RegisterPage(props) {
  const dispatch = useDispatch();

  const { id, views, date, editDate, title, content } = useSelector(
    (state) => ({
      id: state.articleReducers.id,
      views: state.articleReducers.views,
      date: state.articleReducers.date,
      editDate: state.articleReducers.editDate,
      title: state.articleReducers.title,
      content: state.articleReducers.content,
    }),
    shallowEqual
  );
  
.
.
.
}

 

 

 

 

setTitleValue 와 setContentValue를 사용한 메소드를 찾아 지워주고

 

 

다음과 같이 바꿔준다.

 

 

일단 작성만 해놓고 저장은 하지 말고 잠시만 기다려주길 바란다.

 

 

 

// RegisterPage.js

  const onTitleChange = (event) => {
    const { name, value } = event.target;
    dispatch(articleActions.changeRegisterInput({ name: name, value: value }));
  };

  const onContentChange = (event) => {
    const { name, value } = event.target;
    dispatch(articleActions.changeRegisterInput({ name: name, value: value }));
  };

 

 

 

 

그리고 articleActions 를 export 하는 articleSlice로 가서 reducer에 다음 항목을 추가한다.

 

 

 

// articleSlice.js

    changeRegisterInput: (state, { payload }) => {
      switch (payload.name) {
        case "title":
          return {
            ...state,
            title: payload.value,
          };

        case "content":
          return {
            ...state,
            content: payload.value,
          };

        default:
          break;
      }
    },

 

 

 

두 코드블럭을 해석해보면 같은 action 함수에서 name이 title인지 content인지 가려서 onChange 이벤트가 발생했을 때의 value를 state에 반영해준다.

 

 

 

 

 

그리고 RegisterPage에서 TitleValue 와 ContentValue가 쓰인 곳을 useSelector로 잡아온 title 과 content로 바꿔준다.

 

 

 

// RegisterPage.js

function RegisterPage(props) {

...  
    if (title === "" || title === null || title === undefined) { // 수정
      alert("제목을 작성하십시오.");
      return false;
    }

    if (content === "" || content === null || content === undefined) { // 수정
      alert("내용을 작성하십시오.");
      return false;
    }

...

    if (IsForUpdate) {
      dispatch(articleActions.updateArticle(article));
    } else {
      dispatch(articleActions.registerArticle(article));
    }
  };

  return (
    <>
      <RegisterOrEdit
        titleValue={title} // 수정
        contentValue={content} // 수정
        handleTitleChange={onTitleChange}
        handleContentChange={onContentChange}
        handleSubmit={onSubmitArticle}
        updateRequest={IsForUpdate}
      />
    </>
  );
}

 

 

 

 

RegisterPage 의

 

 

const [IsForUpdate, setIsForUpdate] = useState(false);

 

 

부분은 initialState에 없기 때문에 지우면 안된다.

이건 필요하다.

 

 

 

 

더 클린코딩을 해보자면 RegisterPage의

 

  const onTitleChange = (event) => {
    const { name, value } = event.target;
    dispatch(articleActions.changeRegisterInput({ name: name, value: value }));
  };

  const onContentChange = (event) => {
    const { name, value } = event.target;
    dispatch(articleActions.changeRegisterInput({ name: name, value: value }));
  };

 

dispatch(articleActions.changeRegisterInput({ name: name, value: value }));

 

이 부분이 반복됨을 알 수 있다.

 

 

원래는 title과 content가 서로 다르게 사용될 수도 있을 때를 고려하여 onChange 메소드를 각각 만든 것인데

 

 

지금은 크게 상관이 없으므로

 

 

다음과 같이 합쳐주기로 한다.

 

 

  const onRegisterChange = (event) => {
    const { name, value } = event.target;
    dispatch(articleActions.changeRegisterInput({ name: name, value: value }));
  };

 

 

 

그리고 RegisterPage의 컴포넌트 return 부분 메소드도

 

  return (
    <>
      <RegisterOrEdit
        titleValue={title}
        contentValue={content}
        handleRegisterChange={onRegisterChange} // 수정
        handleSubmit={onSubmitArticle}
        updateRequest={IsForUpdate}
      />
    </>
  );

 

이렇게 한 줄로 합쳐주고

 

 

property 이름이 변경됐기 때문에

 

RegisterOrEdit 으로 가서 바꿔줘야 한다.

 

 

      <form onSubmit={props.handleSubmit}>
        <br />
        <div style={{ width: "80%", margin: "2rem auto" }}>
          <label>Title: </label>
          <Input
            onChange={props.handleRegisterChange} // 변경
            value={props.titleValue}
            type="text"
            name="title"
          />
          <hr></hr>
          <TextArea
            rows="30"
            onChange={props.handleRegisterChange} // 변경
            value={props.contentValue}
            name="content"
          />
        </div>
        <Button type="primary" onClick={props.handleSubmit}>
          {props.updateRequest ? "수정" : "등록"}
        </Button>
      </form>

 

 

이렇게 같은 액션 함수와 같은 메소드를 쓰는 onChange 이벤트로 바뀌게 되었다.

 

 

 

 

다음은 redux-saga의 select를 이용한 refactoring 을 해보겠다.

 

 

 

 

 

+) 추가

 

개발자창에 오류가 보여서 추가로 수정한다.

 

ArticleDetail 컴포넌트에 tbody 태그를 추가하겠다. 그리고 colspan 도 colSpan으로 수정한다. 이거는 JSX 변환 문제 때문인 것 같다.

 

 

// ArticleDetail.js

          <tbody> {/* 추가  */}
            <tr>
              <th>번호</th>
              <td>{props.id}</td>
              <th>조회수</th>
              <td>{props.views}</td>
            </tr>
            <tr>
              <th>날짜</th>
              <td>{new Date(props.date).toLocaleString()}</td>
            </tr>
            <tr>
              <th>제목</th>
              <td colSpan="3">{props.title}</td> {/* 수정 */}
            </tr>
            <tr>
              <th>내용</th>
              <td colSpan="3">{props.content}</td> {/* 수정 */}
            </tr>
          </tbody> {/* 추가  */}

 

 

 

목차로 돌아가기:  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-..

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-..

binaryjourney.tistory.com

 

 

 

 

Hi Everybody 안녕 Hola!

 

 

 

LongTimeNoSee

 

 

 

 

오랜만의 포스팅이다.

 

 

지난주부터 개발 들어가고 2주를 바삐 보냈다.

 

 

 

react는 공부할수록 새롭다. 새로워....!

 

 

기회가 된다면 library 중 하나인 redux-persist 도 추가하면 어떨까 생각 중이다.

 

 

UI는 Material UI로 바꾸거나 Ant design을 더 알아보거나 

 

 

요즘 든 생각인데 UI 도큐먼트는 공식 홈페이지에서 예제로 보는 것보다 그냥 import된 파일을 뜯어보는 게 더 나은 것 같다.

 

특히 검도...... 설명이 넘나리 불친절하다.

 

 

 

 

 

 

 

 

react warning

 

 

 

 

목록 부분에 들어갔을 때 보이는 오류인데

 

validateDOMNesting(...): <td> cannot appear as a child of <a>

 

 

 

 

 

구글링해보니 onClick 이벤트로 고쳐줘야 한단다

 

 

stackoverflow.com/questions/58093536/validatedomnesting-td-cannot-appear-as-a-child-of-a

 

 

 

 

난 지금 BoardList 테이블 부분에 reat-router의 Link 컴포넌트를 사용했었는데

옳지 않은 방법이라 하니 (돌아는 가지만 그래도 해결하는 게 좋다고 하니)

 

 

// BoardList.js


  <Link to={`/article/${article.id}`}>
    <td>
      {article.title}
      &nbsp;
      {props.commentLength[article.id] > 0 &&
        `[${props.commentLength[article.id]}]`}
    </td>
  </Link>

 

 

 

어디 한 번 고쳐보자

 

 

 

 

우선 BoardPage로 간다.

 

 

react-router의 hook인 useHistory를 import하여 BoardPage 컴포넌트 안에 history 로 받아둔다.

 

 

hook은 반드시 컴포넌트 안에서 변수를 선언하여 받아야 한다.

 

컴포넌트 바깥이나 안의 다른 함수에서 받으려 하면 실행되지 않는다.

 

 

import React, { useEffect } from "react";
import { Link, useHistory } 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";
import { createSelector } from "@reduxjs/toolkit";

const { Title } = Typography;

function BoardPage() {
  const dispatch = useDispatch();
  const history = useHistory(); // 추가된 부분
  
  .
  .
  .
  }

 

 

 

 

 

그리고 클릭했을 때 해당 아이디를 가진 게시글 화면으로 넘어갈 수 있는 메소드를 만든다.

 

 

  const onArticleTitleClick = (id) => {
    const path = `/article/${id}`;
    history.push(path);
  };

 

 

 

만든 메소드는 BoardList 컴포넌트의 property로 넣어준다.

 

 

          <BoardList
            board={board}
            commentLength={commentLength}
            handleDeleteClick={onDeleteClick}
            handleArticleTitleClick={onArticleTitleClick} // 추가
          />

 

 

프로퍼티로 넘겨준 onArticleTitleClick을 BoardList에서 props.handleArticleTitleClick으로 받아준다.

 

그리고 <td> 태그에 onClick 으로 넣어준다.

 

 

메소드에서 id를 input으로 받게 만들었으므로

 

handleArticleTitle 에서도 id를 넣어준다.

 

 

// BoardList.js

        <tbody>
          {props.board.map((article) => (
            <tr key={article.id}>
              <td>{article.id}</td>

              <td onClick={() => props.handleArticleTitleClick(article.id)} // 추가
              >
                {article.title}
                &nbsp;
                {props.commentLength[article.id] > 0 &&
                  `[${props.commentLength[article.id]}]`}
              </td>

              <td>{article.views}</td>
              <td>
                <Button onClick={() => props.handleDeleteClick(article.id)}>
                  X
                </Button>
              </td>
            </tr>
          ))}
        </tbody>

 

 

 

 

 

결과를 보면

 

 

 

 

onClick으로 변경한 결과

 

 

 

 

 

하이퍼링크 표시는 사라졌지만 글 제목을 누르면

 

 

 

mission success!

 

 

 

 

 

이슈 끝

 

오늘 useState를 없애는 게 해결되면 바로 뒤이어 포스팅하겠다.

 

 

 

목차 돌아가기: 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-..

binaryjourney.tistory.com

 

 

반응형
반응형

 

 

이전글:

2020/11/04 - [React] - [React][CRUD] 간단한 게시판 페이지 만들기 - 17.(1) 게시판 목록에 댓글 개수 띄우기, redux-saga, axios

 

[React][CRUD] 간단한 게시판 페이지 만들기 - 17.(1) 게시판 목록에 댓글 개수 띄우기, redux-saga, axios

목차 돌아가기: 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

 

 

 

 

 

redux-toolkit 에서 createSelector 메소드를 제공하는데 이는 reselect의 createSelector 메소드를 redux-toolkit 이 다시 export 해주는 것이다.

 

아래 API를 보면 알 수 있다.

 

redux-toolkit.js.org/api/createSelector

 

createSelector | Redux Toolkit

createSelector

redux-toolkit.js.org

 

 

 

createSelector는 state 내 지정된 값을 참조하여 새로운 값을 만든 후 변수에 넣어 저장하게 해주어 memoization 기능을 나름 제공한다.

 

kyounghwan01.github.io/blog/React/redux/redux-toolkit/#reselect

 

Redux Toolkit을 사용하여 간단하게 상태 관리하기

Redux Toolkit을 사용하여 간단하게 상태 관리하기, reselect, redux, react, immer, redux-action, FSA

kyounghwan01.github.io

 

velog.io/@ksh4820/react-redux-reselect-%ED%8C%A8%ED%82%A4%EC%A7%80

 

react-redux, reselect 패키지

npm install react-reduxProvider 컴포넌트Provider 컴포넌트 하위에 있는 컴포넌트는 리덕스의 상태 값이 변경되면 자동으로 컴포넌트 함수가 호출되도록 할 수 있다. store 객체를 Provider의 속성 값으로 전

velog.io

 

 

위 블로그는 createSelector를 사용하는 방법과 reducer에서 데이터를 가공하는 것보다 createSelector를 이용하여 데이터를 가공하는 것이 더 안전하다는 설명이다.

 

 

 

 

위 블로그 두 개가 createSelector에 대한 개념설명이 조금씩 차이가 있는데

 

 

첫째는 createSelector를 사용하지 않는다면 store가 업데이트 될 때마다 매번 댓글 개수를 불필요한 연샨을 하여 컴포넌트에 넘길텐데 createSelector 는 이전에 연산된 값을 캐시에 저장하여 비교해주어 값이 바뀌지 않았을 때는 값을 넘기지 않아서 낭비를 줄인다는 것이다. 

 

둘째는 createSelector가 연산에 사용되는 데이터가 변경되는 경우에만 연산을 수행하고, 변경되지 않은 경우에는 이전 결과 값을 그대로 사용한다는 것이다.

 

 

정확한 개념을 아는 사람이 있다면 꼭 댓글로 달아주길 바란다...

 

 

 

 

나 같은 경우 댓글 개수를 세는 createSelector를 만들기는 어려운 환경이었다. 테이블이 굉장히 한정되어 있고 서버쪽에서 필요한 데이터만 넘길 수가 없었기 때문이다. 그래서 일단은 아래같이 만들었다.

 

createSelector

위 코드는 게시글과 댓글을 참조하여 게시글 id와 같은 댓글 id의 개수를 세는 코드이다.

 

 

 

만약 첫번째 개념이라면 내 코드에서 board의 article에서 댓글을 달지 않는 한 수정하든 조회수가 변하든 불필요한 낭비가 없을 것이고

두번째 개념이라면 board의 state는 계속 바뀌고 있기 때문에 createSelector를 쓴 의미가 없게 된다.

 

 

 

 

stackoverflow.com/questions/52607573/how-does-redux-reselect-memoization-work

 

How does Redux Reselect memoization work?

I am trying to integrate reselect into my current app and as always , first i begin to read documentation and then if it needed , another recources.I couldn't understand one special part of documen...

stackoverflow.com

 

 

위 사이트의 글을 보면 두번째 개념에 더 가까운 것 같다.

 

 

const mapStateToProps = (state, props) => { return { todos: getVisibleTodos(state, props) } }


const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)



return ( <div> <VisibleTodoList listId="1" /> </div> )

 

 

If we render the component twice with the same props:

 

  1. the selector gets called and result gets memoized

  2. the selector gets called a second time, and it sees that { listId: 1 } is the same prop arguments as the first time, so it just returns the memoized value.

 

If we render the component twice with different props:

 

  1. the selector gets called and the result gets memoized

  2. the selector gets called a second time, and it sees that { listId: 2 } is not the same props args as the first time { listId: 1 }, so it recalculates and memoizes the new result (todo list 2 object) in memory (overwriting the previous memoization).

 

 

여기서 말하는 props는 결국 useSelector 로 잡아오는 state 내 항목들을 의미하므로..

 

 

 

 

 

 

 

createSelector는 바로 값을 주지 않는다. 함수를 만들어주는 메소드이다.

 

 

그래서 useSelector안에 쓰거나 꼭 만든 형식에 맞게 인자값을 넣어주어야 한다.

 

 

createSelector useSelector

 

 

기존 useSelector 안에 넣어주고 shallowEqual을 써도 상관은 없다.

 

createSelector useSelector

 

 

 

 

 

 

아래는 createSelector를 이용하기 위해 바꾼 코드들이다.

 

// boardSaga

import { put } from "redux-saga/effects";
import Axios from "axios";
import { boardActions } from "../slice/boardSlice";
import { commentActions } from "../slice/commentSlice";

export function* getBoardAsync() {
  try {
    const responseForBoard = yield Axios.get(`http://localhost:4000/board/`);
    const responseForComment = yield Axios.get(
      `http://localhost:4000/comment/`
    );

    const boardData = responseForBoard.data;

    yield put(boardActions.getBoardSuccessAsync(boardData));
    yield put(commentActions.getCommentsAsync(responseForComment.data));
  } catch (e) {
    yield put(boardActions.getBoardFailedAsync(e.message));
  }
}

 

 

// 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";
import { createSelector } from "@reduxjs/toolkit";

const { Title } = Typography;

function BoardPage() {
  const dispatch = useDispatch();

  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 createCommentLength = createSelector( // 추가
    (state) => state.boardReducers.board,
    (state) => state.commentReducers.comments,
    (articles, comments) => {
      const commentByArticle = {};
      for (var index in articles) {
        debugger;
        if (!comments) return commentByArticle;

        const filteredComments = comments.filter(
          (comment) => comment.articleId === articles[index].id
        );
        commentByArticle[articles[index].id] = filteredComments.length;
      }
      return commentByArticle;
    }
  );

  const commentLength = useSelector(createCommentLength); // 추가

  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>게시판</Title>
      </div>
      <div>
        {error ? (
          <h2>에러 발생: {error}</h2>
        ) : isSuccess && board.length > 0 ? (
          <BoardList
            board={board}
            commentLength={commentLength} {/* 추가된 부분 */}
            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}
                  &nbsp; {/* 수정된 부분 */}
                  {props.commentLength[article.id] > 0 &&
                    `[${props.commentLength[article.id]}]`}
                </td>
              </Link>
              <td>{article.views}</td>
              <td>
                <Button onClick={() => props.handleDeleteClick(article.id)}>
                  X
                </Button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

export default BoardList;

 

 

 

 

 

 

 

 

 

 

createSelector 의 구조를 뜯어보면 다음과 같이 생겼다.

 

 

/* homogeneous selector parameter types */
/* one selector */
export function createSelector<S, R1, T>(
  selector: Selector<S, R1>,
  combiner: (res: R1) => T,
): OutputSelector<S, T, (res: R1) => T>;
export function createSelector<S, P, R1, T>(
  selector: ParametricSelector<S, P, R1>,
  combiner: (res: R1) => T,
): OutputParametricSelector<S, P, T, (res: R1) => T>;

/* two selectors */
export function createSelector<S, R1, R2, T>(
  selector1: Selector<S, R1>,
  selector2: Selector<S, R2>,
  combiner: (res1: R1, res2: R2) => T,
): OutputSelector<S, T, (res1: R1, res2: R2) => T>;
export function createSelector<S, P, R1, R2, T>(
  selector1: ParametricSelector<S, P, R1>,
  selector2: ParametricSelector<S, P, R2>,
  combiner: (res1: R1, res2: R2) => T,
): OutputParametricSelector<S, P, T, (res1: R1, res2: R2) => T>;

/* three selectors */
export function createSelector<S, R1, R2, R3, T>(
  selector1: Selector<S, R1>,
  selector2: Selector<S, R2>,
  selector3: Selector<S, R3>,
  combiner: (res1: R1, res2: R2, res3: R3) => T,
): OutputSelector<S, T, (res1: R1, res2: R2, res3: R3) => T>;
export function createSelector<S, P, R1, R2, R3, T>(
  selector1: ParametricSelector<S, P, R1>,
  selector2: ParametricSelector<S, P, R2>,
  selector3: ParametricSelector<S, P, R3>,
  combiner: (res1: R1, res2: R2, res3: R3) => T,
): OutputParametricSelector<S, P, T, (res1: R1, res2: R2, res3: R3) => T>;

//...(생략)

 

 

(이건 같은 parameter를 이용할 때를 말하는 것 같은데)

 

selector로는 현재 12개까지 가능한 것으로 보인다

보면 selector1, selector2, .. 이것은 combiner에 들어갈 값을 지정해주는 것이다.

combiner의  res1, res2, ... 로 이름이 지어져서 들어간다.

 

 

createSelector

 

내 코드의 경우 첫째줄 state => , 이 부분이 articles 로 들어가고 둘째줄 state => , 이 부분은 comments 로 들어간다.

 

 

 

 

 

 

 

*(참고로 parameter 값이 다른 경우 output 부분이 조금 다르다)

 

 

/* heterogeneous selector parameter types */

/* one selector */
export function createSelector<S1, R1, T>(
  selector1: Selector<S1, R1>,
  combiner: (res1: R1) => T,
): OutputSelector<S1, T, (res1: R1) => T>;
export function createSelector<S1, P1, R1, T>(
  selector1: ParametricSelector<S1, P1, R1>,
  combiner: (res1: R1) => T,
): OutputParametricSelector<S1, P1, T, (res1: R1) => T>;

/* two selector */
export function createSelector<S1, S2, R1, R2, T>(
  selector1: Selector<S1, R1>,
  selector2: Selector<S2, R2>,
  combiner: (res1: R1, res2: R2) => T,
): OutputSelector<S1 & S2, T, (res1: R1, res2: R2) => T>;
export function createSelector<S1, S2, P1, P2, R1, R2, T>(
  selector1: ParametricSelector<S1, P1, R1>,
  selector2: ParametricSelector<S2, P2, R2>,
  combiner: (res1: R1, res2: R2) => T,
): OutputParametricSelector<S1 & S2, P1 & P2, T, (res1: R1, res2: R2) => T>;

/* three selector */
export function createSelector<S1, S2, S3, R1, R2, R3, T>(
  selector1: Selector<S1, R1>,
  selector2: Selector<S2, R2>,
  selector3: Selector<S3, R3>,
  combiner: (res1: R1, res2: R2, res3: R3) => T,
): OutputSelector<S1 & S2 & S3, T, (res1: R1, res2: R2, res3: R3) => T>;
export function createSelector<S1, S2, S3, P1, P2, P3, R1, R2, R3, T>(
  selector1: ParametricSelector<S1, P1, R1>,
  selector2: ParametricSelector<S2, P2, R2>,
  selector3: ParametricSelector<S3, P3, R3>,
  combiner: (res1: R1, res2: R2, res3: R3) => T,
): OutputParametricSelector<S1 & S2 & S3, P1 & P2 & P3, T, (res1: R1, res2: R2, res3: R3) => T>;

 

 

 

 

 

 

 

 

그럴 일이 있을까 싶지만 만약 selector 개수가 12개가 넘어갈 때는 배열로 selector를 묶을 수도 있다

 

/* one selector */
export function createSelector<S, R1, T>(
  selectors: [Selector<S, R1>],
  combiner: (res: R1) => T,
): OutputSelector<S, T, (res: R1) => T>;
export function createSelector<S, P, R1, T>(
  selectors: [ParametricSelector<S, P, R1>],
  combiner: (res: R1) => T,
): OutputParametricSelector<S, P, T, (res: R1) => T>;

/* two selectors */
export function createSelector<S, R1, R2, T>(
  selectors: [Selector<S, R1>,
              Selector<S, R2>],
  combiner: (res1: R1, res2: R2) => T,
): OutputSelector<S, T, (res1: R1, res2: R2) => T>;
export function createSelector<S, P, R1, R2, T>(
  selectors: [ParametricSelector<S, P, R1>,
              ParametricSelector<S, P, R2>],
  combiner: (res1: R1, res2: R2) => T,
): OutputParametricSelector<S, P, T, (res1: R1, res2: R2) => T>;

/* three selectors */
export function createSelector<S, R1, R2, R3, T>(
  selectors: [Selector<S, R1>,
              Selector<S, R2>,
              Selector<S, R3>],
  combiner: (res1: R1, res2: R2, res3: R3) => T,
): OutputSelector<S, T, (res1: R1, res2: R2, res3: R3) => T>;
export function createSelector<S, P, R1, R2, R3, T>(
  selectors: [ParametricSelector<S, P, R1>,
              ParametricSelector<S, P, R2>,
              ParametricSelector<S, P, R3>],
  combiner: (res1: R1, res2: R2, res3: R3) => T,
): OutputParametricSelector<S, P, T, (res1: R1, res2: R2, res3: R3) => T>;

// ... 생략

 

 

 

 

나도  createSelector는 생소한 개념이라 다음주부터 본격적 개발 들어가고 더 알게 되면 포스팅을 새로 하겠다.

 

 

댓글 개수 나타내기는 이것으로 마치겠다.

 

 

 

 

목차 돌아가기: 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

 

반응형
반응형

 

이전글:

2020/11/04 - [React] - [React][CRUD] 간단한 게시판 페이지 만들어보기 - 16. 댓글 삭제하기, useDispatch, axios, redux-saga, redux-toolkit

 

[React][CRUD] 리액트로 간단한 게시판 페이지 만들어보기 - 16. 댓글 삭제하기, useDispatch, axios, redux-sa

[React][CRUD] 간단한 게시판 페이지 만들어보기 - 15. json-server에서 foreign key처럼 게시글 ID로 댓글 리 목차 돌아가기: binaryjourney.tistory.com/pages/ReactCRUD-create-board-tutorial [React][CRUD]..

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

 

 

 

 

 

 

앞 두 편에서 계속 말한 대로 이번 편에서는 첫 페이지 전체 게시글을 조회하는 게시판 목록에서 각 게시글의 댓글을 보여주는 기능을 만들 것이다.

 

목표는 이렇다.

 

 

[] 안에 있는 게 댓글이다

 

 

 

 

생각하고 있는 방법은 두 가지인데 첫번째 방법은 표면적인 방법이고 두 번째 방법은 보통 권장되는 방법이나 댓글 개수를 보여주는 게 그 방법의 좋은 예시가 될지는 모르겠다. 어쨌든 둘 다 해보겠다.

 

 

 

 

첫 번째 방법은 boardSaga에서 데이터 가공까지 전부 처리하는 방법이다.

 

 

slice에서 reducer에 더 추가하지 않고 게시글 전체 조회할 때 댓글도 불러와 boardReducer안의 key로 댓글을 넣어주는 것이다.

 

 

 

 

작성한 코드를 보면

 

// boardSaga

import { put } from "redux-saga/effects";
import Axios from "axios";
import { boardActions } from "../slice/boardSlice";

export function* getBoardAsync() {
  try {
    const responseForBoard = yield Axios.get(`http://localhost:4000/board/`);
    const responseForComment = yield Axios.get(
      `http://localhost:4000/comment/`
    );

    const boardData = responseForBoard.data;

    if (responseForComment.data.length > 0) {
      for (var article in responseForBoard.data) {
        const comments = [];
        for (var comment in responseForComment.data) {
          if (
            responseForComment.data[comment].articleId ===
            responseForBoard.data[article].id
          ) {
            comments.push(responseForComment.data[comment].id);
          }
        }
        boardData[article]["comments"] = comments;
      }
    }

    yield put(boardActions.getBoardSuccessAsync(boardData));
  } catch (e) {
    yield put(boardActions.getBoardFailedAsync(e.message));
  }
}

 

 

 

처음에 board와 comment 를 모두 조회해온다. (Axios.get)

 

 

    const responseForBoard = yield Axios.get(`http://localhost:4000/board/`);
    const responseForComment = yield Axios.get(
      `http://localhost:4000/comment/`
    );

 

 

 

그리고 boardData에 board 조회 값을 복사해놓는다.

 

    const boardData = responseForBoard.data;

 

 

 

 

board 안의 게시물 Id와 comment 안의 댓글 게시글 Id 가 같은 것만 뽑아와서 배열을 만들어 준다. (commments.push)

 

    for (var article in responseForBoard.data) {
      const comments = [];
      for (var comment in responseForComment.data) {
        if (
          responseForComment.data[comment].articleId ===
          responseForBoard.data[article].id
        ) {
          comments.push(responseForComment.data[comment].id);
        }

 

 

 

comments 를 key로, value는 댓글 id를 담아놓은 배열로 하여 boardData에 넣어준 후 getBoardSuccessAsync 액션에 payload로 태운다

 

참고로 getBoardSuccessAsync 는 미들웨어가 적용되지 않은 리듀서에만 있는 액션이다.

 

      boardData[article]["comments"] = comments;
    }

    console.log(boardData);
    yield put(boardActions.getBoardSuccessAsync(boardData));

 

 

 

원래는 length만 바로 보내줘도 되는데 사람 앞일은 누구도 모르지 않는가

그래서 id 배열로 보내준다.

 

나중에 여유가 생겨서 댓글 부분을 클릭하면 댓글만 보는 글도 들어가게 할 수 있으므로..

(예를 들어 나의 경우 혐오 글이거나 무서운 내용의 글이라 댓글 반응을 먼저 살필 때 그런 기능을 자주 이용한다.)

 

 

 

 

boardData 값을 보면

 

boardData

 

 

이렇게 배열로 들어가 있다

 

 

 

 

 

 

게시판 조회 액션 흐름을 다시 따라가 보자면

 

getBoardSuccessAsync

 

payload로 태운 boardData는 board 키의 value로 들어간다.

 

그리고

 

 

변경된 state는 BoardPage에서 useSelector 가 잡아준다.

 

useSelector

 

 

board는 BoardList 컴포넌트에 board 프로퍼티로 넘겨진다.

 

BoardList props

 

넘겨진 board는 map을 이용하여 적절하게 정보를 뿌려진다.

props.bord

 

 

 

 

 

이 흐름을 따라 BoardList에서 key가 추가된 부분만 반영해주면 되는 것이다!

 

 

// BoardList

        <tbody>
          {props.board.map((article) => (
            <tr key={article.id}>
              <td>{article.id}</td>
              <Link to={`/article/${article.id}`}>
                <td>
                  {article.title}&nbsp;
                  {article.comments.length > 0 &&
                    `[${article.comments.length}]`}
                </td>
              </Link>
              <td>{article.views}</td>
              <td>
                <Button onClick={() => props.handleDeleteClick(article.id)}>
                  X
                </Button>
              </td>
            </tr>
          ))}
        </tbody>

 

 

댓글이 없을 경우 아예 표시가 안되도록 조건부 렌더링도 넣어두었다.

 

 

이게 첫번째 방법의 끝이다.

 

 

저장하고 화면을 보면 된다.

 

 

두번째 방법은 다음 편에서 다뤄보겠다.

 

 

 

 

 

목차 돌아가기: 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

 

반응형
반응형

 

 

 

[React][CRUD] 간단한 게시판 페이지 만들어보기 - 15. json-server에서 foreign key처럼 게시글 ID로 댓글 리

목차 돌아가기: 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

 

 

 

 

 

 

앞서 말했듯이 댓글 삭제 기능을 구현해보겠다

 

 

 

 

일단 ArticlePage에 빈 이벤트함수 onDeleteComment 를 만들어서 ArtDetail 에 deleteComment라는 프로퍼티에 넘겨준다.

 

 

// ArticlePage

  const onDeleteComment = (commentId) => {};

  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}
          deleteComment={onDeleteComment}
        />
      </div>
    </div>
  );

 

 

 

 

 

 

ArticleDetail 에서는 날짜시간 바로 오른쪽에 삭제버튼으로 [x]을 만들고 앞서 만든 이벤트함수를 props.deleteComment로 받아 onClick 으로 넣어준다.

 

 

// 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()}&nbsp;
                <span
                  style={{ cursor: "pointer" }}
                  onClick={props.deleteComment}
                >
                  [X]
                </span>
              </span>
            </span>
          </div>
        ))}

 

[x] 위로 마우스가 가면 모양이 바뀌도록 스타일도 넣어줬다.

 

 

 

원래 button 태그로 하려고 했는데 css 손을 댈 수가 없어서 결국 span 태그로 만들어줬다.

 

 

 

 

 

 

 

 

 

화면에선 이렇게 보인다.

 

만들어진 삭제버튼

 

 

 

 

 

 

 

 

16편 글을 모두 본 사람은 이제 만드는 순서가 익숙해져 있을 것이다

 

 

 

1. slice에서 액션만들고

 

 

// commentSlice

    deleteComment: (state, { payload: id }) => {
      console.log("댓글 삭제 액션 호출 -- deleteComments"); // saga 에서 감시용
    },

 

 

 

2. saga에서 액션과 같이 처리할 함수를 만들고

 

 

// commentSaga

export function* deleteCommentAsync(action) {
  const commentId = action.payload;

  yield Axios.delete(`http://localhost:4000/comment/${commentId}`);

  history.go(0);
}

 

 

 

 

3. rootSaga에 액션타입과 saga에서 만든 함수를 적고 (엄청나게 뚱뚱해진 rootSaga ㄷㄷㄷㄷㄷ)

 

 

// 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,
  deleteCommentAsync, // 추가
} from "./commentSaga";

const {
  registerArticle,
  getArticle,
  fetchArticle,
  updateArticle,
  deleteArticle,
} = articleActions;
const { getBoard } = boardActions;
const { registerComment, getComments, deleteComment } = 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);
  yield takeLatest(deleteComment.type, deleteCommentAsync); // 추가
}

 

 

 

 

4. 컨테이너 컴포넌트에 만들어놓은 빈 이벤트 함수에 해당 액션함수를 import 하여 dispatch 해주는 것

- 근데 프레젠테이셔널 컴포넌트에도 수정이 필요하다

 

 

// ArticlePage

  const onDeleteComment = (commentId) => {
    dispatch(commentActions.deleteComment(commentId));
  };

 

 

// ArticleDetail

      <div>
        {props.loadComments.length > 0 && {/* 수정 부분 */}
          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()}&nbsp;
                <span
                  style={{ cursor: "pointer" }}
                  onClick={() => props.deleteComment(comment.id)} {/* 수정부분 */}
                >
                  [X]
                </span>
              </span>
            </span>
          </div>
        ))}
      </div>

 

(조건문도 넣어줬다)

 

 

 

작성한 파일을 저장하고 이제 [x] 를 눌러보자

 

 

 

delete 주목

json server에 delete가 보이는가?

 

 

 

아래 이미지와 비교해보자

 

 

 

 

 

 

 

 

 

 

삭제 전

 

 

 

 

11:30:56 에 쓴 댓글이 삭제되었다.

 

 

 

 

 

 

 

 

 

json server 데이터를 봐도 삭제가 성공한 것을 알 수 있다

 

삭제 후

 

 

 

 

 

 

 

분량이 길어서 목록에 댓글 개수 나타내는 것은 다음 편에서 다루겠다

 

 

 

 

 

목차 돌아가기: 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

 

반응형
반응형

 

이전글:

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

 

반응형
반응형

 

 

 

 

목차 돌아가기: 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

 

 

 

 

 

 

퇴근 전까지 진도 나갈 수 있을지는 모르겠으나 일단 도전해보겠다.

 

 

나는 화면부터 만들었다.

 

 

 

 

 

 

/src/components/views/ArticlePage/Sections 에 Comment.js 파일을 생성했다.

 

 

 

파일 위치

 

 

 

 

 

 

Comment 컴포넌트를 생성하는데 나름 스타일에도 힘줘봤다.

 

그닥 예쁘진 않다. 직접 보면 알 것이다.

 

 

// /src/components/views/ArticlePage/Sections/Comment.js

import React from "react";

function Comment() {
  return (
    <>
      <form>
        <div style={{ border: "1px solid black" }}>
          <textarea
            style={{
              borderStyle: "none none dashed none",
              borderColor: "black",
              width: "100%",
              display: "block",
              boxSizing: "border-box",
              borderWidth: "1px",
              marginBottom: "1px",
            }}
          />
          <div
            style={{
              width: "100%",
              boxSizing: "border-box",
              height: "35px",
              padding: "5px",
            }}
          >
            <button
              style={{
                border: "none",
                width: "100%",
                float: "right",
              }}
            >
              댓글 등록
            </button>
          </div>
        </div>
      </form>
    </>
  );
}

export default Comment;

 

 

 

 

 

 

 

 

 

 

댓글란은 게시글 화면에서 이 위치에 들어가게 할 거다

 

 

 

comment 란 생성

 

 

 

 

 

 

 

 

그러면 ArticlePage-ArticleDetail 둘 중 하나의 컴포넌트에 생성된 댓글이 달릴 것 같아서

Comment 컴포넌트를 ArtilcePage에서 작업하여 ArticleDetail 컴포넌트의 props로 보내주는 방법으로 했다.

 

 

 

// ArticlePage

import Comment from "./Sections/Comment";

return (
    <div style={{ width: "80%", margin: "3rem auto" }}>
      <div>
        <ArticleDetail
          id={id}
          title={title}
          content={content}
          views={views}
          date={date}
          handleDeleteClick={onDeleteClick}
          handleComment={
            <Comment />
          }
        />
      </div>
    </div>
  );

 

 

// ArticleDetail

<div style={{ margin: "2rem auto" }}>{props.handleComment}</div>

 

 

 

 

 

 

 

ArticlePage가 Comment의 컨테이너 컴포넌트 역할을 대신하기 때문에

 

textArea 입력에 필요한 useState와 onChange 이벤트 함수를 만들어서 Comment의 props로 넣어줬다.

 

 

// ArticlePage


  const [CommentValue, setCommentValue] = useState("");

  const onCommentChange = (e) => {
    setCommentValue(e.currentTarget.value);
  };

  const onCommentSubmit = () => {}; // reducer 만들고 추가 예정

  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}
            />
          }
        />
      </div>
    </div>
  );
}

 

 

// Comment

import React from "react";

function Comment(props) {
  console.log(props.comment);
  return (

...
            value={props.comment}
            onChange={props.handleCommentChange}

...
  );
}

export default Comment;

 

 

 

 

 

 

 

 

개발자 창을 열어 값이 잘 들어가는지 확인해보자

 

 

 

 

devTools

 

 

잘 들어간다.

 

 

 

 

 

보면 댓글이 치는 순간마다 렌더가 되기 때문에 최적화를 위해 ArticlePage의 useSelector 로 id, title, content를 만들어주는 부분에 shallowEqual을 달아주자.

 

 

 

// ArticlePage

import { shallowEqual, useDispatch, useSelector } from "react-redux";

  const { id, title, content, date } = useSelector(
    (state) => ({
      id: state.articleReducers.id,
      title: state.articleReducers.title,
      content: state.articleReducers.content,
      date: state.articleReducers.date // date도 합쳐버리기!
    }),
    shallowEqual
  );
  const views = useSelector((state) => state.articleReducers.views);

 

 

 

 

 

 

 

 

댓글 저장을 구현하려면 잠시 json-server 를 꺼야 한다.

 

 

 

 

board.json 구조를 수정해야 하기 때문이다.

 

 

{
  "board": [

  ],
  "comment": [

  ]
}

 

 

이렇게 바꿔주고 서버를 다시 킨다.

 

 

 

npx json-server ./board.json --port 4000

 

 

 

 

 

json server terminal을 보면 Resources 부분에 /comment가 새로 생긴 것을 알 수 있다.

 

 

json-server

 

 

 

http://localhost:4000/comment

 

 

로 들어가보면 이렇게 떠 있다

 

 

 

board.json /comment

 

 

 

 

 

 

 

 

이제 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 애서 감시용
    },
    getCommentsAsync: (state, { payload: list }) => {
      return {
        ...state,
        comments: list,
      };
    },
  },
});

export const commentReducers = commentSlice.reducer;
export const commentActions = commentSlice.actions;

 

 

 

 

 

 

 

 

그리고 commentSaga를 만들어 액션함수를 import 하여 함수를 만들어준다.

 

 

// commentSaga

import Axios from "axios";
import history from "../utils/history";

export function* registerCommentAsync(action) {
  const data = action.payload;

  yield Axios.post(`http://localhost:4000/comment/`, data);

  history.go(0); // refresh
}

 

 

 

 

 

 

 

 

 

rootSlice에서 만들어진 commentReducer 를 묶고

 

 

// rootSlice

import { combineReducers } from "redux";
import { articleReducers } from "./articleSlice";
import { boardReducers } from "./boardSlice";
import { commentReducers } from "./commentSlice"; // 추가

const rootReducer = combineReducers({
  articleReducers,
  boardReducers,
  commentReducers, // 추가
});

export default rootReducer;

 

 

 

 

 

 

rootSaga 에서는 감시할 액션과 함께 호출할 saga 함수를 적고

 

 

// rootSaga

import { take, takeEvery, takeLatest } from "redux-saga/effects";
...
import { commentActions } from "../slice/commentSlice"; // 추가
...
import { registerCommentAsync } from "./commentSaga"; //추가

...
const { registerComment } = commentActions; // 추가

export default function* rootWatcher() {
  ...
  yield takeLatest(registerComment.type, registerCommentAsync); // 추가
}

 

 

 

 

 

 

 

 

마지막으로 ArticlePage 컴포넌트에서 onCommentSubmit 이벤트 함수를 마무리지어주면 된다.

나는 validation 체크도 넣어줬다.

 

 

// ArticlePage

import { commentActions } from "../../../slice/commentSlice";

  const onCommentSubmit = () => {
    if (
      CommentValue === "" ||
      CommentValue === null ||
      CommentValue === undefined
    ) {
      alert("댓글을 입력하십시오.");
      return false;
    }
    const comment = {
      id: 0,
      content: CommentValue,
      date: Date.now(),
      articleId: id,
    };

    dispatch(commentActions.registerComment(comment));
  };

 

 

 

 

 

full code는 이렇다.

 

 

// ArticlePage

import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { articleActions } from "../../../slice/articleSlice";
import { commentActions } from "../../../slice/commentSlice"; // 추가
import ArticleDetail from "./Sections/ArticleDetail";
import Comment from "./Sections/Comment";

function ArticlePage({ match, location }) {
  // console.log(match.params.articleId);

  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(articleActions.getArticle(match.params.articleId));
  }, [match.params.articleId]);

  const { id, title, content, date } = useSelector(
    (state) => ({
      id: state.articleReducers.id,
      title: state.articleReducers.title,
      content: state.articleReducers.content,
      date: state.articleReducers.date
    }),
    shallowEqual
  );
  const views = useSelector((state) => state.articleReducers.views);

  const onDeleteClick = () => {
    if (!window.confirm("삭제하시겠습니까?")) return false;
    dispatch(articleActions.deleteArticle(id));
  };

  const [CommentValue, setCommentValue] = useState("");

  const onCommentChange = (e) => {
    setCommentValue(e.currentTarget.value);
  };

  const onCommentSubmit = () => {
    if (
      CommentValue === "" ||
      CommentValue === null ||
      CommentValue === undefined
    ) {
      alert("댓글을 입력하십시오.");
      return false;
    }
    const comment = {
      id: 0,
      content: CommentValue,
      date: Date.now(),
      articleId: id,
    };

    dispatch(commentActions.registerComment(comment));
  };

  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}
            />
          }
        />
      </div>
    </div>
  );
}

export default ArticlePage;

 

 

Comment.js ------------------------ 2021-04-08 댓글 등록 부분 코드가 블로그에 없다 하여 수정합니다.

 

// Comment.js

import React from "react";

function Comment(props) {
  return (
    <>
      <form>
        <div style={{ border: "1px solid black" }}>
          <textarea
            style={{
              borderStyle: "none none dashed none",
              borderColor: "black",
              width: "100%",
              display: "block",
              boxSizing: "border-box",
              borderWidth: "1px",
              marginBottom: "1px",
            }}
            value={props.comment}
            onChange={props.handleCommentChange}
          />
          <div
            style={{
              width: "100%",
              boxSizing: "border-box",
              height: "35px",
              padding: "5px",
            }}
          >
            <button
              style={{
                border: "none",
                width: "100%",
                float: "right",
              }}
              onClick={props.handleCommentSubmit} {/* 추가됨 */}
            >
              댓글 등록
            </button>
          </div>
        </div>
      </form>
    </>
  );
}

export default Comment;

 

 

 

저장해보면!

 

 

success!

 

 

comment 에 데이터가 잘 들어간 것이 보인다!

 

(json-server의 put은 조회수 반영때문에 일어난 것이다.)

 

 

 

다음편에서는 게시글 화면에 댓글 목록을 가져오는 것을 다루겠다!

 

 

 

 

 

목차 돌아가기: 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

 

반응형
반응형

 

 

 

목차 돌아가기: 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

 

 

 

 

 

date를 화면에 나타내 보려다 계속해서 만난 오류

 

Objects are not valid as a React child

 

 

찾아보니 new Date 이거나 Date의 값인 경우 Object라서 리액트에서 렌더링을 못한다는 것이다.

 

그래서 toString()을 해줘야 한다고 한다.

 

 

참고한 사이트:

 

stackoverflow.com/questions/41604539/objects-are-not-valid-as-a-react-child

 

Objects are not valid as a React child

I am getting an below error. I can see that I have to return array instead of object. But I am really not sure how to fix it. Thanks in advance Objects are not valid as a React child. If you mea...

stackoverflow.com

 

 

 

 

 

 

 

현재 서버에는 날자가 이런형식으로 들어가 있고

 

 

board json

 

 

 

 

 

 

이를 ArticleDetail 에서 props로 받아 이렇게 toString 해줘야 한다.

 

// ArticleDetail

          <tr>
            <th>날짜</th>
            <td>{new Date(props.date).toString()}</td>
          </tr>

toString

 

 

 

 

 

 

화면에서 날짜를 드디어 볼 수 있다.

 

 

date toString

 

 

 

 

 

 

 

 

 

format를 바꾸고 싶다면 toString() 말고 다른 걸 써도 된다.

 

예를 들어 toLocaleString() 을 쓰면

 

 

 

          <tr>
            <th>날짜</th>
            <td>{new Date(props.date).toLocaleString()}</td>
          </tr>

toLocaleString

 

 

 

 

 

 

이렇게 나타난다.

 

 

toLocaleString

 

 

(화면에 나타난 Comment 는 무시하길 바란다)

 

 

 

 

 

 

 

목차 돌아가기: 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

 

 

반응형
반응형

 

 

 

 

목차 돌아가기: 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

 

 

 

 

 

 

 

이제 board.json을 비우는 등의 서버를 직접 건드리는 일은 더 이상 발생하지 않도록 게시글 삭제 기능을 만들겠다.

 

 

 

시작하기 전에 양해를 구할 것은

 

 

ArticleDetail과 ArticlePage를 약간 수정했다.

 

컨테이너 컴포넌트와 프레젠테이셔널 컴포넌트에 맞게 버튼 자리를 바꿨을 뿐이고 오늘 만들 삭제버튼도 추가되었다.

 

 

 

코드를 통째로 보여주면

 

 

// ArticleDetail

import React from "react";
import { Button, Typography } from "antd";
import { Link } from "react-router-dom";

const { Title } = Typography;

function ArticleDetail(props) {
  return (
    <div>
      <div style={{ margin: "2rem auto" }}>
        <a href="/">
          <Button type="primary">목록으로 가기</Button>
        </a>
      </div>
      <div style={{ textAlign: "center" }}>
        <Title>게시글</Title>
      </div>
      <div>
        <table>
          <colgroup>
            <col width="10%" />
            <col width="40%" />
            <col width="10%" />
            <col width="40%" />
          </colgroup>
          <tr>
            <th>번호</th>
            <td>{props.id}</td>
            <th>조회수</th>
            <td>{props.views}</td>
          </tr>
          <tr>
            <th>제목</th>
            <td colspan="3">{props.title}</td>
          </tr>
          <tr>
            <th>내용</th>
            <td colspan="3">{props.content}</td>
          </tr>
        </table>
      </div>
      <div style={{ margin: "2rem auto" }}>
        <Link to={`/edit/${props.id}?isForEdit=true`}>
          <Button type="primary">수정</Button>
        </Link>
      </div>
      <div style={{ margin: "auto" }}>
        <Button type="danger">삭제</Button> {/* 오늘 할 부분 */}
      </div>
    </div>
  );
}

export default ArticleDetail;

 

 

 

 

// ArticlePage


import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { articleActions } from "../../../slice/articleSlice";
import ArticleDetail from "./Sections/ArticleDetail";

function ArticlePage({ match, location }) {
  // console.log(match.params.articleId);

  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(articleActions.getArticle(match.params.articleId));
  }, [match.params.articleId]);

  const { id, title, content } = useSelector((state) => ({
    id: state.articleReducers.id,
    title: state.articleReducers.title,
    content: state.articleReducers.content,
  }));
  const date = useSelector((state) => state.articleReducers.date);
  const views = useSelector((state) => state.articleReducers.views);

  return (
    <div style={{ width: "80%", margin: "3rem auto" }}>
      <div>
        <ArticleDetail
          id={id}
          title={title}
          content={content}
          views={views}
          date={date}
        />
      </div>
    </div>
  );
}

export default ArticlePage;

 

 

 

실무에서 삭제 기능은 서버에서 데이터를 영구삭제하는 것보다는 사용 여부 항목을 N 으로 update 한다.

 

하지만 json-server를 이용한 간단한 페이지를 만드는 과정이므로 나는 delete 를 쓰겠다 ^_________________^

 

update 방식으로 가도 괜찮다. 하지만 지금으로썬 필드도 새로 만들어야 하고,,,고칠 게 많을 것이다.

 

 

이번 편은 게시글 화면에 들어가서 삭제버튼 눌러 삭제 처리하는 것까지 목표로 하였고 목록에서 직접 삭제 구현은 사실 계획에 없었다.

 

articleReducer에서 삭제 후 boardReducer로 조회하면 될 것 같긴 한데

어차피 게시글 화면에서 삭제해도 목록으로 돌아가니까

 

 

일단 만들고 동일 로직으로 목록에 버튼만 추가해서 그게 가능한지까지 알아보겠다.

 

 

 

 

게시글 수정과 방식이 너무 비슷하여 이젠  다들 익숙하지 않을까 싶다.

 

 

 

 

 

 

 

우선 articleSlice에 삭제 액션을 만든다.

 

 

// articleSlice


    deleteArticle: (state, { payload: id }) => {
      console.log("게시글 삭제 액션 호출 -- deleteArticle"); // saga 에서 감시용
    },

 

 

 

 

 

 

 

 

그리고 articleSaga에서 deleteArticle 이 dispatch 됐을 때 서버와 연결할 함수를 만든다.

 

 

 

// articleSaga


export function* deleteArticleAsync(action) {
  const id = action.payload;

  yield Axios.delete(`http://localhost:4000/board/${id}`);

  alert("삭제되었습니다.");

  history.push(`/`);
}

 

첫화면으로 돌아가면 자동으로 목록이 조회되므로 추가 액션은 넣지 않았다.

 

 

 

 

 

rootSaga에서 감시할 액션 타입과 saga 함수를 연결해준다.

 

 

// rootSaga

import { take, takeEvery, takeLatest } from "redux-saga/effects";
import { articleActions } from "../slice/articleSlice";
import { boardActions } from "../slice/boardSlice";
import {
  registerArticleAsync,
  getArticleAsync,
  fetchArticleAsync,
  updateArticleAsync,
  deleteArticleAsync,
} from "./articleSaga";
import { getBoardAsync } from "./boardSaga";

const {
  registerArticle,
  getArticle,
  fetchArticle,
  updateArticle,
  deleteArticle,
} = articleActions;
const { getBoard } = boardActions;

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

 

 

 

 

 

 

 

그리고 ArticlePage 컴포넌트로 돌아와서

 

button onClick 값으로 들어갈 함수를 만들어주고 ArticleDetail의 프로퍼티로 보낸다.

 

삭제하기 전에 삭제 동의를 묻는 것도 필요할 것 같아 만들어주었다.

 

 

// ArticlePage


  const onDeleteClick = () => {
    if (!window.confirm("삭제하시겠습니까?")) return false;
    dispatch(articleActions.deleteArticle(id));
  };

  return (
    <div style={{ width: "80%", margin: "3rem auto" }}>
      <div>
        <ArticleDetail
          id={id}
          title={title}
          content={content}
          views={views}
          date={date}
          handleDeleteClick={onDeleteClick}
        />
      </div>
    </div>
  );

 

 

 

 

 

 

 

ArticleDetail 컴포넌트에서는 props.handleDeleteClick로 onDeleteClick 이벤트 함수를 받아 button onClick에 넣는다.

 

 

// ArticleDetail


        <Button onClick={props.handleDeleteClick} type="danger">
          삭제
        </Button>

 

 

 

 

 

 

 

 

 

편집한 파일들을 저장하고 화면에서 삭제버튼을 눌러보자.

 

 

 

 

 

번호가 13번인 글에서 삭제를 시도한다

번호 주목

 

 

 

삭제됐다는 알림

 

 

 

 

 

 

 

 

 

saga에서 쓴 알림 과정을 지나면..........!

 

 

 

 

첫 화면으로 가면서 목록을 불러오는데 12번까지 밖에 없다!

 

 

 

 

 

결과

 

 

 

 

화면 뿐만 아니라 서버에도 잘 들어갔는지 알아보고 싶다면

 

board.json
board.json

 

 

board.json을 확인해보면 된다.

 

 

 

처음부터 새로 시작하는 마음으로 나는 board.json 에 있는 데이터를 모두 지울 것이다.

 

 

게시글 클릭해서 지우기 너무 귀찮지 않은가? 같은 기능을 BoardPage에 놨을 때 어떻게 되는지 알아보자.

 

 

(근데 사실 이게 맞는지 잘 모르겠다. history 잘 아시는 분 있으면 꼭 댓글을 남겨주세요....)

 

 

 

BoardPage 컴포넌트에 onDeleClick 이벤트 함수를 붙여넣는다.

여기는 id를 갖고 오는 곳이 없으니 인자값으로 id를 넣어놔야 한다.

 

 

 

// BoardPage

  const onDeleteClick = (id) => {
    if (!window.confirm("삭제하시겠습니까?")) return false;
    dispatch(articleActions.deleteArticle(id));
  };

 

 

 

그리고 BoardList 컴포넌트의 프로퍼티로 넘겨준다.

 

 

// BoardPage



  return (
    <div style={{ maxWidth: "700px", margin: "2rem 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>
  );

 

 

 

 

 

 

BoardList 에서는 버튼 onClick 부분에 props 로 받아주면 된다

 

// BoardList

  return (
    <div>
      <table>
        <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>
  );

 

 

 

 

 

 

 

boardpage에서는 새로고침하지 않는 이상 목록 조회가 안 되기 때문에

 

임시방책으로 saga에서 새로고침을 해주자 (BoardPage onDeleteClick 내에서 새로고침을 할 경우 서버에 반영되기 전에 새로고침 해버림)

 

 

// articleSaga

export function* deleteArticleAsync(action) {
  const id = action.payload;

  yield Axios.delete(`http://localhost:4000/board/${id}`);

  alert("삭제되었습니다.");

  history.push(`/`);

  history.go(0); // 추가부분, 새로고침
}

 

 

 

 

logger에도 getBoard 액션이 여러 개 찍히지 않는 걸 보니 괜찮은 것 같다.

 

 

 

 

 

여러 개를 삭제하고 난 후 모습이다.

 

 

 

 

 

 

 

 

 

 

다음 편에는 지난번에 해결하지 못한 date를 해결해보고(성공 가능성 불확실) date 해결 여부 상관없이 댓글 기능도 가능하면 구현해보겠다.

 

 

 

 

 

 

 

목차 돌아가기: 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

 

 

반응형
반응형

 

 

목차 돌아가기: 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

 

 

 

 

 

11편에서 a 태그와 router의 Link 컴포넌트의 차이점을 혹독히 경험하면서

 

응용으로 수정하기 버튼은 Link 컴포넌트로 만들 것이다.

 

 

 

그 전에 수정된 부분이 있는데  ArticleDetail에 있던 버튼을 ArticlePage 컴포넌트로 옮겼다.

 

변수를 좀 더 편하게 받기 위해서였다.

 

 

// ArticlePage


import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { articleActions } from "../../../slice/articleSlice";
import ArticleDetail from "./Sections/ArticleDetail";
import { Button, Typography } from "antd";
import { Link } from "react-router-dom";

function ArticlePage({ match, location }) {
  // console.log(match.params.articleId);

  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(articleActions.getArticle(match.params.articleId));
  }, [match.params.articleId]);

  const { id, title, content } = useSelector((state) => ({
    id: state.articleReducers.id,
    title: state.articleReducers.title,
    content: state.articleReducers.content,
  }));
  const date = useSelector((state) => state.articleReducers.date);
  const views = useSelector((state) => state.articleReducers.views);

  return (
    <div style={{ width: "80%", margin: "3rem auto" }}>
      <div>
        <ArticleDetail
          id={id}
          title={title}
          content={content}
          views={views}
          date={date}
        />
      </div>
      <div style={{ margin: "2rem auto" }}>
        <Link to={`/edit/${id}`}>
          <Button type="primary">수정</Button>
        </Link>
      </div>
    </div>
  );
}


export default ArticlePage;

 

 

 

 

// ArticleDetail

import React from "react";
import { Button, Typography } from "antd";

const { Title } = Typography;

function ArticleDetail(props) {
  return (
    <div>
      <div style={{ margin: "2rem auto" }}>
        <a href="/">
          <Button type="primary">목록으로 가기</Button>
        </a>
      </div>
      <div style={{ textAlign: "center" }}>
        <Title>게시글</Title>
      </div>
      <div>
        <table>
          <colgroup>
            <col width="10%" />
            <col width="40%" />
            <col width="10%" />
            <col width="40%" />
          </colgroup>
          <tr>
            <th>번호</th>
            <td>{props.id}</td>
            <th>조회수</th>
            <td>{props.views}</td>
          </tr>
          <tr>
            <th>제목</th>
            <td colspan="3">{props.title}</td>
          </tr>
          <tr>
            <th>내용</th>
            <td colspan="3">{props.content}</td>
          </tr>
        </table>
      </div>
    </div>
  );
}

export default ArticleDetail;

 

 

 

 

 

 

 

react router API에서 Link 컴포넌트를 찾아보면 수정버튼을 누를 때 해당 글의 state를 Link 컴포넌트의 프로퍼티로 바로 집어넣어줄 수 있다.

 

 

 

reactrouter.com/web/api/Link

 

React Router: Declarative Routing for React

Learn once, Route Anywhere

reactrouter.com

 

 

 

 

 

예를 들어 

 

 

 

일반적으로 이렇게 간단히 쓰는데

 

 

        <Link to={`/edit/${id}?isForEdit=true`}>
          <Button type="primary">수정</Button>
        </Link>

 

 

 

 

만약 링크로 보내줄 state가 적고 한정되어 추가할 게 없는 경우

 

 

이렇게 바로 Link 컴포넌트를 통해 보내주면 편하다.

 

 

        <Link
          to={{
            pathname: `/edit/${id}`,
            search: "?isForEdit=true",
            state: {
              id: id,
              title: title,
              content: content,
              views: views,
            },
          }}
        >
          <Button type="primary">수정</Button>
        </Link>

 

 

 

 

BoardPage

 

 

 

Link 컴포넌트에서 state를 넘겨준 후 위 글에서 수정을 눌렀을 때

 

 

 

RegisterPage에서 console.log(props) 의 값을 보면

 

 

 

RegisterPage 에 props를 써야 한다!

 

 

console.log(props)  -- location 주목

 

 

 

props.location.state 부분에 앞서 Link 컴포넌트에서 넘겨주었던 state 값이 들어가 있는 걸 알 수 있다.

 

 

 

 

Link 컴포넌트에서 필요한 state 값을 넘겨주는 방식으로 게시글 수정을 구현하면 RegisterPage에서 props로 state 값을 받기만 하고 내용을 조회하는 액션을 따로 dispatch하지 않아도 된다.

 

 

그러나 코드가 지저분해진다는 단점이 있고 새글등록 부분도 프로퍼티를 넘겨주는 방식으로 바꿔줘야 한다.

 

 

 

 

Link 컴포넌트에서 필요한 state를 넘겨줬을 때 어떤 문제점을 겪게 되는지 알아보겠다.

 

 

 

 

 

 

일단 ArticlePage 컴포넌트의 useSelector 를 이용하여 stateForProps를 추가한다.

 

 

const stateForProps = useSelector((state) => state.articleReducers)

 

 

 

 

ArticlePage를 보면 useSelector를 네 개나 썼는데 사실 useMemo 를 쓰면 이렇게 나눠 쓰지 않아도 된다.

근데 내가 useMemo를 잘 안 쓰기도 하고 어려워 해서 일단은 useSelector만으로 작성했다.

 

나중에 기회가 된다면 useMemo를 이용한 방법도 써서 포스팅해보겠다.

 

 

 

 

 

만들어진 stateForProps 를 Link 컴포넌트의 state 키의 값으로 넣는다.

 

 

// ArticlePage


        <Link
          to={{
            pathname: `/edit/${id}`,
            search: "?isEdit=true",
            state: stateForProps,
          }}
        >
          <Button type="primary">수정</Button>
        </Link>

 

 

 

 

 

 

 

그러고 나서 수정버튼을 누른 후 state 값이 어떻게 나오는지 확인해보자

 

 

 

state 값에 다 들어가있다.

 

 

 

 

 

위의 값을 바탕으로 RegisterPage 컴포넌트에서 필요한 값들을 세팅해보자

 

 

 

 

 

// RegisterPage

import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import RegisterOrEdit from "./Sections/RegisterOrEdit";
import { articleActions } from "../../../slice/articleSlice";

function RegisterPage(props) {
  console.log(props);
  const dispatch = useDispatch();

  ...

  useEffect(() => {
    const searchParams = new URLSearchParams(props.location.search);
    if (searchParams.get("isForEdit")) {
      setIsForUpdate(true);
    }
    setTitleValue(props.location.state.title);
    setContentValue(props.location.state.content);
  }, []);

  ...

  const onSubmitArticle = (event) => {
    event.preventDefault();

    ...

    const article = {
      title: TitleValue,
      content: ContentValue,
      views: views,
      date: date,
      editDate: editDate,
    };
    dispatch(articleActions.registerArticle(article));
  };

  ...
}

export default RegisterPage;

 

 

useEffect 문을 보면 URLSearchParam을 이용하여 url에서 isForEdit이 true일 경우 setIsForUpdate true 값으로 바꿔주고 제목과 내용을 set 해주는 방식으로 했다.

 

 

 

URLSearchParam 을 처음 보는 사람들은 다음 사이트를 참고 바란다.

 

 

 

 

developer.mozilla.org/ko/docs/Web/API/URLSearchParams

 

URLSearchParams

URLSearchParams 인터페이스는 URL의 쿼리 문자열에 대해 작업할 수 있는 유틸리티 메서드를 정의합니다.

developer.mozilla.org

 

 

 

 

 

여기서 단점은 새글 등록의 경우 props.location.state 값이 undefined 로 되어 있다.

 

물론 조건문 여러 개를 쓰거나 새글 등록 Link에서 같은 방법으로 state를 보내도 되지만

 

useSelector 를 이용할 경우 새글 등록 때는 초기값을 사용하고 글 수정 땐 Link를 통해 게시글 화면의 state 값을 가져와서 localStorage로 담거나 dispatch(조회 액션)을 쓰면 된다.

 

 

 

결국 같은 메소드로 두 방식을 한꺼번에 처리할 수 있는데 번거롭고 찾아보기도 힘들게 "굳이???" 이 방법을 써야 하나 싶은 것이다.

 

 

 

보기 편한 방법으로 코드를 다시 수정해보겠다.

 

 

 

 

 

 

 

 

ArtcilePage 컴포넌트에서 Link 컴포넌트를 다시 아래처럼 돌려놓는다. (stateForProps 만들어놓은 것은 삭제해버린다.)

 

 

// ArticlePage

        <Link to={`/edit/${id}?isForEdit=true`}>
          <Button type="primary">수정</Button>
        </Link>

 

 

// ArticlePage

import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { articleActions } from "../../../slice/articleSlice";
import ArticleDetail from "./Sections/ArticleDetail";
import { Button } from "antd";
import { Link } from "react-router-dom";

function ArticlePage({ match, location }) {
  // console.log(match.params.articleId);

  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(articleActions.getArticle(match.params.articleId));
  }, [match.params.articleId]);

  const { id, title, content } = useSelector((state) => ({
    id: state.articleReducers.id,
    title: state.articleReducers.title,
    content: state.articleReducers.content,
  }));
  const date = useSelector((state) => state.articleReducers.date);
  const views = useSelector((state) => state.articleReducers.views);

  return (
    <div style={{ width: "80%", margin: "3rem auto" }}>
      <div>
        <ArticleDetail
          id={id}
          title={title}
          content={content}
          views={views}
          date={date}
        />
      </div>
      <div style={{ margin: "2rem auto" }}>
        <Link to={`/edit/${id}?isForEdit=true`}>
          <Button type="primary">수정</Button>
        </Link>
      </div>
    </div>
  );
}

export default ArticlePage;

 

 

 

 

 

 

 

 

그리고 RegisterPage 컴포넌트의 useSelector 에 id, title, content 를 추가하고 바뀐 값에 맞게 useEffect 문도 수정한다.

 

 

// RegisterPage


  const { id, title, content, views, date, editDate } = useSelector(
    (state) => ({
      id: state.articleReducers.id,
      title: state.articleReducers.title,
      content: state.articleReducers.content,
      views: state.articleReducers.views,
      date: state.articleReducers.date,
      editDate: state.articleReducers.editDate,
    })
  );

  useEffect(() => {
    const searchParams = new URLSearchParams(props.location.search);
    if (searchParams.get("isForEdit")) {
      setIsForUpdate(true);
    }
    setTitleValue(title);
    setContentValue(content);
  }, []);

 

 

 

 

위 코드를 저장 후 게시글 화면에서 수정버튼을 누르면 변수 id, title, content, views, date, editDate 에 바로 값이 들어가지긴 한다.

 

 

 

그러나 새로고침하면 값이 다 날아간다.

 

 

 

값이 남아있던 게

 

 

 

새로고침하면 사라짐

 

 

 

이게 단순히 useState에서 다 초기값으로 설정되었기 때문에 그런 게 아니라 useState로 설정되어 있지 않은  views, date, editDate도 모두 날아간다.

 

 

개발자 창을 보면 알 수 있는데 게시글 화면을 조회할 때 getArticleAsync 액션으로  state 값은 변경되었지만 이것을 RegisterPage 에 props로 전달하지 않았기 때문에 새로고침할 시 state 값이 다 날아가는 것이다.

 

 

 

redux-logger

 

 

 

해결하는 방법은

 

1. props 로 전달

2. local storage 에 저장

3. 서버에서 해당 내용 조회

 

 

앞서 props로 전달하는 것은 하지 않기로 했으므로 나는 dispatch를 통해 서버에서 내용을 조회해오겠다.

 

 

 

 

 

 

 

우선 articleSlice 의 reducer 부분에 호출할 액션을 추가한다.

 

// articleSlice


    fetchArticle: (state, {payload: id}) => {
      console.log("게시글 조회 액션 호출 -- fetchArticle"); // saga에서 감시용
    },

 

 

 

 

 

그리고 articleSaga 에 액션을 인터셉트하여 호출할 함수를 만든다.

 

 

export function* fetchArticleAsync(action) {
  console.log(action);
  const id = action.payload;

  const response = yield Axios.get(`http://localhost:4000/board/${id}`);

  yield put(articleActions.getArticleAsync(response.data));
}

 

위 코드를 보면 알 수 있듯 getArticleAsync를 재활용한다.

 

 

 

 

 

 

getArticleAsync의 내부는 이렇게 생겼다

 

 

// articleSlice

    getArticleAsync: (state, { payload: article }) => {
      console.log("saga에서 put 액션 호출 -- getArticleAsync");
      return {
        ...state,
        id: article.id,
        title: article.title,
        content: article.content,
        date: article.date,
        editDate: article.editDate,
        views: article.views,
      };
    },

 

 

 

 

 

그리고  rootSaga에 감시할 액션과 호출할 함수를 추가해준다.

 

 

 

 

// rootSaga


export default function* rootWatcher() {
  yield takeLatest(registerArticle.type, registerArticleAsync);
  yield takeEvery(getArticle.type, getArticleAsync);
  yield takeEvery(getBoard.type, getBoardAsync);
  yield takeEvery(fetchArticle.type, fetchArticleAsync); // 추가된 부분
}

 

 

 

 

이제 뷰에서 dispatch 할 차례다.

 

 

// RegisterPage


  const { id, views, date, editDate } = useSelector((state) => ({
    id: state.articleReducers.id,
    views: state.articleReducers.views,
    date: state.articleReducers.date,
    editDate: state.articleReducers.editDate,
  }));

  const { title } = useSelector((state) => ({ // 수정할 부분
    title: state.articleReducers.title,
  }));

  const { content } = useSelector((state) => ({ // 수정할 부분
    content: state.articleReducers.content,
  }));

  const [TitleValue, setTitleValue] = useState(title);
  const [ContentValue, setContentValue] = useState(content);
  const [IsForUpdate, setIsForUpdate] = useState(false);

  useEffect(() => {
    const searchParams = new URLSearchParams(props.location.search);
    if (searchParams.get("isForEdit")) {
      dispatch(articleActions.fetchArticle(props.match.params.articleId)); // 추가된 부분
      setIsForUpdate(true);
    }
    setTitleValue(title);
    setContentValue(content);
  }, [id]); // 수정된 부분

 

 

 

useState 에 초깃값으로 state에서 select 해온 변수들을 넣어주고

(그 때문에 순서 위치를 바꿈)

 

 

useEffect문에 액션을 호출 부분을 추가해준다.

 

 

useSelector를 많이 쓴 이유는 렌더링할 때마다 불필요한 객체가 생성되는 게 싫어서, useMemo도 쓰기 싫어서인데

생긴 게 안 예쁘므로 shallowEqual로 바꿔준다.

 

 

// RegisterPage

  const { id, views, date, editDate, title, content } = useSelector(
    (state) => ({
      id: state.articleReducers.id,
      views: state.articleReducers.views,
      date: state.articleReducers.date,
      editDate: state.articleReducers.editDate,
      title: state.articleReducers.title,
      content: state.articleReducers.content,
    }),
    shallowEqual
  );

 

 

 

 

 

shallowEqual 이 생소한 사람은 아래 사이트를 참조하길 바란다.

 

 

muang-kim.tistory.com/66

 

React-Redux - 9. useSelector 최적화 (컨테이너 컴포넌트 최적화)

최적화 프리젠테이션 컴포넌트에서는 TodoList 작성 시 React.memo 를 통해 컴포넌트의 리렌더링 최적화를 해준 바가 있다. 그렇다면, 컨테이너 컴포넌트에서는 어떤 것들을 신경써야 할까? 참고로,

muang-kim.tistory.com

react.vlpt.us/redux/08-optimize-useSelector.html

 

8. useSelector 최적화 · GitBook

8. useSelector 최적화 리액트 컴포넌트에서 리덕스 상태를 조회해서 사용 할 때 최적화를 하기 위해서 어떤 사항을 고려해야 하는지 알아보도록 하겠습니다. 지난 섹션에서 할 일 목록을 만들 때에

react.vlpt.us

 

 

 

(사실 둘의 내용은 같다)

 

 

 

 

코드가 아래처럼 수정되면

 

 

// RegisterPage

import React, { useEffect, useState } from "react";
import { shallowEqual, useDispatch, useSelector } from "react-redux";
import RegisterOrEdit from "./Sections/RegisterOrEdit";
import { articleActions } from "../../../slice/articleSlice";

function RegisterPage(props) {
  console.log(props);
  const dispatch = useDispatch();

  const { id, views, date, editDate, title, content } = useSelector(
    (state) => ({
      id: state.articleReducers.id,
      views: state.articleReducers.views,
      date: state.articleReducers.date,
      editDate: state.articleReducers.editDate,
      title: state.articleReducers.title,
      content: state.articleReducers.content,
    }),
    shallowEqual
  );

  const [TitleValue, setTitleValue] = useState(title);
  const [ContentValue, setContentValue] = useState(content);
  const [IsForUpdate, setIsForUpdate] = useState(false);

  useEffect(() => {
    const searchParams = new URLSearchParams(props.location.search);
    if (searchParams.get("isForEdit")) {
      dispatch(articleActions.fetchArticle(props.match.params.articleId));
      setIsForUpdate(true);
    }
    setTitleValue(title);
    setContentValue(content);
  }, [id]);

  const onTitleChange = (event) => {
    setTitleValue(event.currentTarget.value);
  };

  const onContentChange = (event) => {
    setContentValue(event.currentTarget.value);
  };

  const onSubmitArticle = (event) => {
    event.preventDefault();

    if (TitleValue === "" || TitleValue === null || TitleValue === undefined) {
      alert("제목을 작성하십시오.");
      return false;
    }

    if (
      ContentValue === "" ||
      ContentValue === null ||
      ContentValue === undefined
    ) {
      alert("내용을 작성하십시오.");
      return false;
    }

    const article = {
      id: id, ///
      title: TitleValue,
      content: ContentValue,
      views: views,
      date: date,
      editDate: editDate,
    };
    console.log(article);
    dispatch(articleActions.registerArticle(article));
  };

  return (
    <>
      <RegisterOrEdit
        titleValue={TitleValue}
        contentValue={ContentValue}
        handleTitleChange={onTitleChange}
        handleContentChange={onContentChange}
        handleSubmit={onSubmitArticle}
        updateRequest={IsForUpdate}
      />
    </>
  );
}

export default RegisterPage;

 

 

 

 

 

 

게시글 화면에서 수정을 눌렀을 때

example

 

 

 

내용이 잘 들어가져있고

 

 

막 아무글이나 쓴 다음에 새로고침을 하면

 

example

 

 

 

example

 

 

처음에 조회한 대로 값도 잘 들어가져 있다.

 

 

그리고 언급을 안 했었는데 dispatch 후 setIsForUpdate(true)를 해놨기 때문에 등록 버튼이 수정버튼으로 바뀌어 보이는 것이다.

 

 

 

 

 

 

 

이제 IsForUpdate 값을 이용하여 등록/수정버튼을 마무리지어보자.

 

 

 

 

 

먼저 articleSlice 에서 initialState 를 수정해주고 updateArticle 이란 액션을 만든다.

 

// articleSlice

import { createSlice } from "@reduxjs/toolkit";

export const articleSlice = createSlice({
  name: "article",
  initialState: {
    id: 0,
    title: "",
    content: "",
    views: 0,
    date: Date.now(), // 수정
    editDate: "", // 수정
  },
  reducers: {
    registerArticle: (state, { payload: article }) => {
      console.log("게시글 등록 액션 호출 -- registerArticle"); // saga에서 감시용
    },
    getArticle: (state, { payload: id }) => {
      console.log("게시글 조회 액션 호출 -- getArticle"); // saga에서 감시용
    },
    getArticleAsync: (state, { payload: article }) => {
      console.log("saga에서 put 액션 호출 -- getArticleAsync");
      return {
        ...state,
        id: article.id,
        title: article.title,
        content: article.content,
        date: article.date,
        editDate: article.editDate,
        views: article.views,
      };
    },
    fetchArticle: (state, { payload: id }) => {
      console.log("게시글 조회 액션 호출 -- fetchArticle"); // saga에서 감시용
    },
    updateArticle: (state, { payload: article }) => {
      console.log("게시글 수정 액션 호출 -- updateArticle"); // saga에서 감시용
    },
  },
});

export const articleReducers = articleSlice.reducer;
export const articleActions = articleSlice.actions;

 

 

 

 

그리고 articleSaga로 가서 updateArticleAsync를 만들어준다.

 

 

방식은 registerArticleAsync 와 거의 같고 Axios.get에서 Axios.put(id) 으로 바꿔준 것밖에 없다.

 

 

// articleSaga

export function* updateArticleAsync(action) {
  const article = action.payload;

  const response = yield Axios.put(
    `http://localhost:4000/board/${article.id}`,
    article
  );

  alert("저장되었습니다.");

  console.log(response.data.id);

  // history.push(`/article/${response.data.id}`);

  history.push(`/article/${response.data.id}`, response.data.id);
}

 

 

그리고 rootSaga에 가서 액션과 saga를 연결해준다.

 

 

// rootSaga

import { take, takeEvery, takeLatest } from "redux-saga/effects";
import { articleActions } from "../slice/articleSlice";
import { boardActions } from "../slice/boardSlice";
import {
  registerArticleAsync,
  getArticleAsync,
  fetchArticleAsync,
  updateArticleAsync, // 이 부분
} from "./articleSaga";
import { getBoardAsync } from "./boardSaga";

const {
  registerArticle,
  getArticle,
  fetchArticle,
  updateArticle, // 이 부분
} = articleActions;
const { getBoard } = boardActions;

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); // 이 부분
}

 

 

 

그리고 RegisterPage 의 onSubmitArticle에 조건문을 추가한다.

 

 

// RegisterPage


  const onSubmitArticle = (event) => {
    event.preventDefault();

    if (TitleValue === "" || TitleValue === null || TitleValue === undefined) {
      alert("제목을 작성하십시오.");
      return false;
    }

    if (
      ContentValue === "" ||
      ContentValue === null ||
      ContentValue === undefined
    ) {
      alert("내용을 작성하십시오.");
      return false;
    }

    const article = {
      id: id, ///
      title: TitleValue,
      content: ContentValue,
      views: views,
      date: date,
      editDate: IsForUpdate ? Date.now() : editDate, // 수정
    };
    // console.log(article);

    if (IsForUpdate) {
      dispatch(articleActions.updateArticle(article)); // 추가
    } else {
      dispatch(articleActions.registerArticle(article));
    }
  };

 

 

 

 

 

작성한 파일들을 다 저장하고

 

 

수정버튼을 눌러보자

 

 

 

수정 시도

 

 

수정 성공!

 

 

 

 

 

 

+

 

빠트린 부분과 수정할 부분 추가

 

 

BoardPage 컴포넌트에 New Post 버튼 링크에 쿼리가 추가되었다.

 

// BoardPage


        <Link to="/register?isForEdit=false">
          <Button type="primary">New Post</Button>
        </Link>

 

 

 

 

 

 

그리고 RegisterPage 에  오타가 있어 바로잡는다. (고치지 않으면 새글 등록이 안된다. 등록버튼도 안 보임!)

 

// RegisterPage


  useEffect(() => {
    const searchParams = new URLSearchParams(props.location.search);
    if (searchParams.get("isForEdit") === "true") { // 수정 부분
      dispatch(articleActions.fetchArticle(props.match.params.articleId));
      setIsForUpdate(true);
    }
    setTitleValue(title);
    setContentValue(content);
  }, [id]);

 

 

 

 

 

목차 돌아가기: 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