반응형

 

 

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