반응형

 

 

목차 돌아가기: 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 목록 조회를 데이터 존재 유무 상관없이 가능하도록 해놨기 때문에 서버를 실행시키지 않았거나 board.json에 데이터도 아무것도 없을 때 아마 에러가 발생할 것이다.

 

 

그래서 11편의 순서는

 

1. 조회할 내용이 없을 때 에러 안 뜨도록 처리 (임시)

 

2. 조회수 반영 기능 생성

 

3. 조회 성공일 때 실패했을 때 따라 다르게 보이도록 화면 처리

 

이렇게 진행하겠다.

 

 

 

 

 

 

 

 

1번은 어렵지 않다.

 

BoardPage 컴포넌트로 가서

 

 

// BoardPage


  const { board, isLoading, isSuccess, error } = useSelector((state) => ({
    board: state.boardReducers.board,
    isLoading: state.boardReducers.isLoading,
    isSuccess: state.boardReducers.isSuccess,
    error: state.boardReducers.error,
  }));

 

useSelector 변수를 추가하고

 

 

 

 

 

 

 

return 값에 조건을 추가하면 된다.

 

 

 

// BoardPage


return (
    <div style={{ maxWidth: "700px", margin: "2rem auto" }}>
      <div>
        <Link to="/register">
          <Button type="primary">New Post</Button>
        </Link>
      </div>
      <div style={{ textAlign: "center", marginBottom: "2rem" }}>
        <Title>Board Title</Title>
      </div>
      <div>
        {isSuccess && board.length > 0 ? (
          <BoardList board={board} />
        ) : (
          <p> 조회할 내용이 없습니다. </p>
        )}
      </div>
    </div>
  );

 

아마 지난번과 코드가 조금 다를텐데 스타일과 버튼 위치변경이 있었다.

 

일단 조회 성공 + 데이터 존재할 때의 조건만 추가해놓는다.

 

 

 

 

 

 

그러고 나서 서버를 띄우지 않고 npm start 를 하면

 

 

BoardPage

 

 

이렇게 나올 것이다.

 

 

 

 

 

 

그리고 board.json을 비운 채로 json-server를 실행해보자

 

 

board.json

 

 

 

 

 

board.json

 

 

서버에 get 으로  갔으나 조회할 내용이 없어 화면에는 '조회할 내용이 없습니다' 문구가 뜰 것이다.

 

 

 

 

 

 

 

이제 2번 조회수 반영기능을 만들어보자.

 

 

원래 조회수 반영 기능을 따로 설명하려고 했는데 json-server 특성상 update할 때 원래 정보도 같이 있지 않는 한 덮어쓰기가 되어 버려서 ArticleSaga에서 getArticleAsync 전에 조회수가 update 되는 방식으로 가야 했다.

따로 reducer를 만들지 않고 getArticle, getArticleAsync 액션 사이에서 이뤄지므로 조회처리할 때 같이 다루려 한다.

 

 

 

 

 

약 올리려는 의도는 아니고 조회수 반영기능도 나름 간단하게 구현해놨다.

 

articleSaga의 getArticleAsync 함수에서 세 줄 정도만 바꿔주면 된다.

 

 

// articleSaga

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

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

  const request = yield Axios.put(`http://localhost:4000/board/${id}`, {
    ...response.data,
    views: parseInt(response.data.views) + 1,
  });

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

 

 

일단 getArticle 액션에서 갖고 오는 payload는 id밖에 없어서 조회수인 views는 조회 내용으로부터 가져와야 한다.

 

그래서 response, request를 나눈 것이고

 

update는 Axios.put 으로 처리하는데 이 때 (내가 방법을 모르는 것일 수도 있지만) json-server에 수정할 값 외에 빈 값으로 넣어버리면 다 공백으로 저장되어 버린다.

 

그래서 ...resposne.data 를 하여 조회된 내용을 복사하고 views: parseInt(response.data.views) + 1 하여 조회수를 수정한다.

 

그런 다음에 수정된 값인 request.data 를 getArticleAsync의 payload로 보내는 것이다.

 

 

 

 

조회수 기능이 완료되었으니 새 글을 등록해보자

 

 

changed views

 

조회수에 1이 들어가있는 것이 보이는가?

 

 

board.json

 

 

화면에만 증가값이 보이는 것이 아니라 json server에도 잘 들어갔다

 

 

 

 

 

 

언급한 순서대로 이젠 조회시 생길 수 있는 예외를 예방할 예외 처리문을 만들어보자

 

 

먼저 boardSlice의 getBoardAsync 를 getBoardSuccessAsync 로 바꿔주고 그 아래에는 getBoardFailedAsync를 만들어준다.

 

// boardSlice


export const boardSlice = createSlice({
  name: "board",
  initialState: {
    board: [],
    isLoading: true,
    isSuccess: false,
    error: null,
  },
  reducers: {
    getBoard: (state, { payload }) => {
      console.log("게시글 목록 조회 액션 호출 -- getBoard");
    },
    getBoardSuccessAsync: (state, { payload: data }) => {
      console.log("saga에서 put 액션 호출 -- getBoardSuccessAsync");
      return {
        ...state,
        board: data,
        isSuccess: true,
        isLoading: false,
      };
    },
    getBoardFailedAsync: (state, { payload: error }) => {
      console.log("saga에서 put 액션 호출 -- getBoardFailedAsync");
      return {
        ...state,
        isLoading: false,
        error: error,
      };
    },
  },
});

 

 

 

 

조회 실패 시 payload로 boardSaga에서 발생할 error 메시지가 보내진다.

 

 

 

// boardSaga

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

export function* getBoardAsync() {
  try {
    const response = yield Axios.get(`http://localhost:4000/board/`);
    yield put(boardActions.getBoardSuccessAsync(response.data));
  } catch (e) {
    yield put(boardActions.getBoardFailedAsync(e.message));
  }
}

 

 

 

 

 

 

마지막으로  BoardPage 에서 return 부분에 조건을 추가해준다.

 

 

// BoardPage


  return (
    <div style={{ maxWidth: "700px", margin: "2rem auto" }}>
      <div>
        <Link to="/register">
          <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} />
        ) : isSuccess && board.length <= 0 ? (
          <p> 조회할 내용이 없습니다. </p>
        ) : (
          <p> 목록을 불러오는 중입니다. </p>
        )}
      </div>
    </div>
  );

 

에러가 발생하면 에러를 보여주고

 

에러 발생 없고 isSuccess가 true이고 조회된 내용 개수에 따라 조회할 내용/조회할 내용이 없다는 문구로 표시

 

그 외 사항은 로딩 중으로 표현했다.

 

 

 

 

 

 

 

json 서버를 닫고 앱만 실행(npm start) 해보자

 

 

 

처음에는 로딩 표시인 "목록을 불러오는 중입니다" 가 뜨고

 

isLoading

 

 

 

 

 

 

조금 지나면

 

 

 

error with message

 

 

 

에러가 발생할 때 뜨는 내용이 나타난다.

 

 

 

 

 

 

 

 

그럼 다시 json 서버를 켜보고 화면을 새로고침해보자

 

 

"목록을 불러오는 중입니다."가  찰나로 보이고 바로 목록 리스트가 조회된다.

 

isSuccess

 

 

 

 

다음은 article부분 NullPointerException 처리와 수정, 삭제 구현을 시도해보겠다!

 

 

 

 

 

수정 필요! -->

 

 

articleSlice 에서 초기화가 일어나지 않아 새 글을 등록했을 때 views(조회수)가 0부터 시작하지 않는 문제점이 발생했다.

 

articleSlice 의 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,
      };
    },

 

(9편에서 ...state 지워도 된다고 하였지만 부정확한 정보였다. 지우면 안 된다)

 

 

 

+ 이것만으로도 해결이 안 되어서 어디서부터 문제인 건지 따져본 결과 ArticleDetail 컴포넌트에 있는 Link 컴포넌트가 문제점인 것을 알아냈다.

 

 

 

아래 블로그를 읽고 문제점을 밝혀냈다.

 

velog.io/@bigbrothershin/React-Router

 

React Router: router, link

1. Route: 특정 주소에 컴포넌트 연결하기 사용자가 요청하는 주소에 따라 다른 컴포넌트를 보여줘보겠습니다. 이 작업을 할 때에는 Route 라는 컴포넌트를 사용합니다. >`` 예제 > exact 라는 props 를 t

velog.io

 

 

 

 

블로그에서는

 

 

Link 컴포넌트는 클릭하면 다른 주소로 이동시키는 컴포넌트입니다.
리액트 라우터를 사용할땐 일반 <a href="...">...</a> 태그를 사용하시면 안됩니다.
그 대신에 그 대신에 Link 라는 컴포넌트를 사용해야합니다

 

그 이유는 a 태그의 기본적인 속성은 페이지를 이동시키면서, 페이지를 아예 새로 불러오게됩니다. 그렇게 되면서 우리 리액트 앱이 지니고있는 상태들도 초기화되고, 렌더링된 컴포넌트도 모두 사라지고 새로 렌더링을 하게됩니다.
그렇기 때문에 Link 컴포넌트를 사용하는데요, 이 컴포넌트는 HTML5 History API 를 사용하여 브라우저의 주소만 바꿀뿐, 페이지를 새로 불러오지는 않습니다.

 

 

 

내 경우 첫 화면 들어갈 때마다 새로 렌더링하면서 상태들도 초기화되어야 하는데 ArticleDetail의 목록으로 가기 버튼을 를 Link to 로 하는 바람에 상태들이 계속 유지되었던 것이다.

 

그래서 이전에 보았던 게시글의 상태가 계속 유지되었기 때문에 새글을 등록할 때도 이전 state의 views를 계속 달고 와서 저장이 되었던 것이다.

 

 

 

다음과 같이 수정하면 이젠 정말 완전히! 해결 된다.

 

 

// ArticleDetail

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

const { Title } = Typography;

function ArticleDetail(props) {
  return (
    <div style={{ width: "80%", margin: "3rem auto" }}>
      <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;

 

 

 

 

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

 

 

 

 

 

9편에서는 게시글 한 개의 내용을 불러오는 것을 다뤘고 이번 편에서는 게시글 목록 전체 조회를 다룰 것이다.

 

이번 편도 꽤나 쉽다!

 

 

 

 

 

 

articleSlice 와는 다른 initialState를 쓸 것이기 때문에 slice를 새로 만들어줘야 한다.

 

 

그래서 /src/slice/ 에 boardSlice.js 파일을 만들어준다.

 

방식은 articleSlice 와 같다.

 

 

 

 

createSlice룰 import 하고 createSlice 함수를 받는 변수 boardSlice를 선언한다.

 

 

// /src/slice/boardSlice

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

export const boardSlice = createSlice({
  
});

 

 

 

 

 

그리고 createSlice 부분에 configure할 내용들을 적는다.

name, initialState, reducers 를 board 특성에 맞게 적으면 끝이다.

 

 

// boardSlice.js

export const boardSlice = createSlice({
  name: "board",
  initialState: {
    board: [],
    isLoading: true,
    isSuccess: false,
    error: null,
  },
  reducers: {
    getBoard: (state, { payload }) => {
      console.log("getBoard 액션 호출");
    },
    getBoardAsync: (state, { payload: data }) => {
      return {
        ...state,
        board: data,
        isSuccess: true,
        isLoading: false,
      };
    },
  },
});

 

 

name 은 board 이기 때문에 board 이고

 

 

initialState는 board 배열 리스트를 가져오는 것이어서 state를 하나하나 지정할 필요가 없다.

그래서 조회된 내용이 잘 가져와졌는지 확인 정도만 하는 loading, success, error 와 조회 내용 부분인 board 만 지정해주겠다.

 

 

 

그리고 뷰(BoardPage)에서 dispatch 할 액션 getBoard를 기본적으로만 만들어주고

saga에서 put할 액션 getBoardAsync를 만들어주었다.

 

saga에서 put할 때 변경될 state는 데이터가 성공적으로 조회된 상태만 내보내므로 getBoardAsync의 success는 true, loading은 false로 정해주었다.

 

 

 

 

 

 

 

saga 도 또한 /src/sagas 에 boardSaga.js 파일을 만들어준다.

 

boardSaga 에서는 board 전체 내용을 가져오는 것밖에 없기 때문에 일단 아래 코드처럼만 만들어준다.

 

 

 

// /src/sagas/boardSaga.js

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

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

  yield put(boardActions.getBoardAsync(response.data));
}

 

 

위의 코드는 데이터가 성공적으로 전송됐을 때만 실행되는 것이다.

 

그 외의 경우는 reducer를 포함하여 나중에 만들기로 한다.

 

 

 

 

 

 

이제 만든 reducer 와 saga를 묶어주는 것이 남았다.

 

boardSlice 하단에 reducer와 action을 export 해줄 코드를 적는다.

 

 

 

// boardSlice.js


export const boardReducers = boardSlice.reducer;
export const boardActions = boardSlice.actions;

 

 

 

 

 

 

그리고 rootReducer가 있는 rootSlice에서 boardReducer 를 import 하여 combineReducer로 묶어준다.

 

 

// rootSlice.js

import { combineReducers } from "redux";
import { articleReducers } from "./articleSlice";
import { boardReducers } from "./boardSlice";

const rootReducer = combineReducers({ articleReducers, boardReducers });

export default rootReducer;

 

 

 

 

 

rootSaga에 가서는 감시할 액션 타입과 호출할 함수를 import 해주고 rootWatcher 안에 적는다.

 

// rootSaga

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

const { registerArticle, getArticle } = articleActions;
const { getBoard } = boardActions;

export default function* rootWatcher() {
  yield takeLatest(registerArticle.type, registerArticleAsync);
  yield takeEvery(getArticle.type, getArticleAsync);
  yield takeEvery(getBoard.type, getBoardAsync);
}

 

 

 

 

 

 

필요한 기능은 다 구현해놨으니 이제 뷰인 BoardPage에서 액션을 호출하면 끝이다.

 

 

BoardPage 에 useDispatch와 useEffect, useSelector를  import 한다.

 

 

// BoardPage

  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(boardActions.getBoard());
  }, [dispatch]);

  const board = useSelector((state) => state.boardReducers.board);

 

 

 

화면이 로딩될 때 dispatch를 해주므로 액션 dispatch 문은 useEffect 문 안에 적어줬다.

[] 안은 현재 화면 로딩 외에 만들어놓은 것이 없으므로 사실 비워놔도 된다

(나는 ESLint 노란 줄 생기는 게 싫어서 dispatch 넣어둠).

 

그리고 useSelector로는 리듀서에서 변경된 state.boardReducers.board를 잡아온다.

 

 

 

 

 

 

프레젠테이셔널 컴포넌트로 BoardList를 만들어놓았으므로 가져온 board를 BoardList의 프로퍼티로 넣어준다.

 

 

// BoardPage

      <div>
        <BoardList board={board} />
      </div>

 

 

 

 

 

BoardPage 의 전체 코드는 다음과 같다.

 

 

// BoardPage.js

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

const { Title } = Typography;

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

  useEffect(() => {
    dispatch(boardActions.getBoard());
  }, [dispatch]);

  const board = useSelector((state) => state.boardReducers.board);

  return (
    <div style={{ maxWidth: "700px", margin: "2rem auto" }}>
      <div>
        <Title>Board Title</Title>
      </div>
      <div>
        <Link to="/register">
          <Button type="primary">New Post</Button>
        </Link>
      </div>
      <div>
        <BoardList board={board} />
      </div>
    </div>
  );
}

export default BoardPage;

 

 

 

 

 

 

BoardList 컴포넌트에서는 props로 board를 받아 map으로 안의 내용을 풀어준다.

 

테이블로 그나마 보기 좋게 만들어준다.

 

 

// BoardList.js

import React from "react";
import { Link } from "react-router-dom";

function BoardList(props) {
  console.log(props.board);

  return (
    <div>
      <table>
        <colgroup>
          <col width="10%" />
          <col width="10%" />
          <col width="40%" />
          <col width="40%" />
        </colgroup>
        <tr>
          <th>번호</th>
          <th>제목</th>
          <th>조회수</th>
        </tr>
        {props.board.map((article) => (
          <tr>
            <td>{article.id}</td>
            <Link to={`/article/${article.id}`}>
              <td>{article.title}</td>
            </Link>
            <td>{article.views}</td>
          </tr>
        ))}
      </table>
    </div>
  );
}

export default BoardList;

 

 

 

여기서 Link to 는 해당 글의 제목을 클릭하면 이동하도록 해놨다.

 

ArticlePage에서 파라미터로 articleId를 읽으므로 경로 마지막에 article.id 를 넣어준 것이다.

 

 

 

 

 

 

 

 

결과물을 보면 이것과 비슷하게 나올 것이다.

 

 

 

 

BoardPage

 

 

 

 

 

 

 

 

글을 클릭하면

 

 

이전 과정에서 ArticlePage까지 만들어놨기 때문에

 

 

ArticleDetail

 

 

해당 글 내용까지 슈루룩 들어간다.

 

 

 

 

 

 

 

 

목록으로 가는 버튼까지만 만들고 이번 편을 마무리하겠다.

 

 

ArticleDetail 컴포넌트에 필요한 라이브러리를 import 한다.

 

 

// ArticleDetail


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

const { Title } = Typography;

 

 

 

 

 

그리고 table 이 들어간 div 위에 코드를 추가한다

 

 

      <div style={{ margin: "2rem auto" }}>
        <Link to="/">
          <Button type="primary">목록으로 가기</Button>
        </Link>
      </div>
      <div style={{ textAlign: "center" }}>
        <Title>게시글</Title>
      </div>

 

 

 

 

 

ArticleDetail 의 전체 코드는 다음과 같다.

 

 

// ArticleDetail


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

const { Title } = Typography;

function ArticleDetail(props) {
  return (
    <div style={{ width: "80%", margin: "3rem auto" }}>
      <div style={{ margin: "2rem auto" }}>
        <Link to="/">
          <Button type="primary">목록으로 가기</Button>
        </Link>
      </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;

 

 

 

 

 

 

 

파일을 저장하면 화면이 이렇게 바뀌어 있을 것이다.

 

 

 

ArticleDetail

 

 

 

버튼과 목록을 클릭해보며 왔다 갔다 해보자

 

 

 

 

 

기본적인 틀은 다 만들어놓았다고 생각한다.

 

조회수 늘리기, 수정하기, 삭제, 댓글 등 세부적인 기능은 다음 편에서부터 다루겠다.

 

 

 

 

 

 

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

 

 

 

 

 

화면 이동이 해결되었으니 이제 내가 등록한 글의 내용이 화면에 나타나는 것을 구현해보도록 해보자

 

 

 

 

 

 

첫번째로 articleSlice에 서버에서 내용을 불러오는 부분과 불러온 내용을 알맞은 형식으로 바꿔주는 액션을 만들어준다.

(registerArticleAsync 는 쓸 일이 없을 것 같아 지워버렸다)

 

 

 

// articleSlice.js

export const articleSlice = createSlice({
  name: "article",
  initialState: {
    id: 0,
    title: "",
    content: "",
    views: 0,
    date: new Date(Date.now()),
    editDate: new Date(Date.now()),
  },
  reducers: {
    registerArticle: (state, { payload: article }) => {
      console.log(article); // saga에서 감시용
    },
    getArticle: (state, { payload: id }) => {
      console.log(id); // saga에서 감시용
    },
    getArticleAsync: (state, { payload: article }) => {
      console.log(article); // saga에서 호출용
      return {
        ...state,
        id: article.id,
        title: article.title,
        content: article.content,
        date: article.date,
        editDate: article.editDate,
        views: article.views,
      };
    },
  },
});

 

 

getArticle은 rootSaga에서 게시글 내용 조회 액션이 dispatch됐을 때 그 액션을 catch하기 위해 만든 것이고

실질적으로 불러온 내용을 뿌려주는 것은 아래 getArticleAsync이다.

 

 

 

 

 

 

 

 

그리고 articleSaga로 가서 getArticle 액션이 dispatch 됐을 때 호출시킬 함수를 만든다.

 

 

 

// articleSaga.js

import { put } from "redux-saga/effects";


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

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

  console.log(response.data);

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

 

 

 

위 코드를 보면 getArticle 액션을 가로채서 payload로 보낸 id로 게시글을 조회해온다.

 

이 때 Axios.get 을 이용한다.

 

json-server로 board 안의 id가 1인 데이터 객체을 가져올 것이다.

 

그리고 조회해온 내용(response.data)을 getArticleAsynce 액션 호출(put)의 payload로 같이 보내준다.

 

 

 

 

 

 

 

이제 rootSaga에 getArticle 감시 코드를 만들자

 

 

 

// rootSaga.js

import { takeEvery, takeLatest } from "redux-saga/effects";
import { articleActions } from "../slice/articleSlice";
import { registerArticleAsync, getArticleAsync } from "./articleSaga";

const { registerArticle, getArticle } = articleActions;

export default function* rootWatcher() {
  yield takeLatest(registerArticle.type, registerArticleAsync);
  yield takeEvery(getArticle.type, getArticleAsync);
}

 

 

 

 

 

 

 

여기까지 됐다면 이제  view인 ArticlePage에서 화면 조회 부분을 추가하자.

 

 

 

// ArticlePage.js

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

function ArticlePage({ match, location }) {

  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>
      ArticlePage
      <br />
      글번호 : {id}
      <br />
      제목 : {title}
      <br />
      내용: {content}
      <br />
      {/* {date} */}
      <br />
      조회수: {views}
    </div>
  );
}


export default ArticlePage;

 

 

 

useEffect로 게시글 id가 바뀔 때마다 dispatch로 id를 보내 내용을 조회하도록 만들었다.

(이는 나중에 조회수 증가에도 쓰일 예정이다.)

 

 

그리고 articleSlice reducer에서 바뀐 state를 들고와야 하므로 useSelector를 사용해줬다.

 

 

date 부분은 자꾸 오류나서 일단 제외했다.

 

 

 

 

 

 

id, title, content, views 라도 잘 보이는지 글을 등록해보고 화면을 확인해보자.

 

ArticlePage

 

 

 

 

 

 

 

redux-toolkit의 편한 점은 reducer 에서 상태관리 할 때 ...state 나 state.concat 과 같이 원본 값을 복제해서 새로운 내용을 반영하는 기존 방법과는 달리 바로 추가(push) 해도 된다는 점이다.

 

 

articleSlice.js 에서 getArticleAsync 액션함수의 return 값을 다음과 같이 수정하면

 

 

앞에 ...staate를 지웠기 때문에

아래처럼 로딩 직전에 initialState가 잠깐 뜨는 경우가 없을 것이라 생각되지만

 

 

여전히 있다!

 

바로 써줘도 redux-toolkit에서는 문제가 없다.   

 

=> 조회수 state 초기화가 되지 않아 조회수가 0부터 시작하지 않게 된다. ...state 써줘야 한다.

 

 

부정확한 정보는 취소선처리한다.

 

 

 

 

 

 

 

유지보수하기 쉽도록 ArticlePage를 컨테이너 컴포넌트로 사용하고 프레젠테이셔널 컴포넌트로 ArticleDetail.js 파일을 추가하겠다.

 

 

 

 

ArticlePage directory

 

 

 

 

 

프레젠테이셔널 컴포넌트인 ArticleDetail 컴포넌트에는 ArticlePage의 return 부분만 긁어와서 props만 추가해주면 된다.

 

// ArticleDetail.js

import React from "react";

function ArticleDetail(props) {
  return (
    <div>
      <div>
        ArticlePage
        <br />
        글번호 : {props.id}
        <br />
        제목 : {props.title}
        <br />
        내용: {props.content}
        <br />
        {/* {date} */}
        <br />
        조회수: {props.views}
      </div>
    </div>
  );
}

export default ArticleDetail;

 

 

 

ArticlePage 컴포넌트에서 달라진 부분은 다음과 같다.

 

// 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>
      <ArticleDetail
        id={id}
        title={title}
        content={content}
        views={views}
        date={date}
      />
    </div>
  );
}


export default ArticlePage;

 

 

 

 

이제 게시글 화면을 조금 게시판스럽게(?) 바꿔줄 차례다.

 

 

// import React from "react";

function ArticleDetail(props) {
  return (
    <div style={{ width: "80%", margin: "3rem auto" }}>
      <div style={{ textAlign: "center" }}>
        <h1>게시글</h1>
      </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;

 

 

 

 

아주 기본이긴 하지만 그래도 알아볼만큼 형태가 잡혔다.

 

 

 

ArticleDetail

 

 

 

 

 

그리고 이번주 내로 github에 올려 소스를 공유할 계획이다.

 

 

이번주까지 기획한 대로 잘 진행되면 좋겠다.

 

 

 

 

 

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

 

 

 

 

saga 로 이용 중인 파일에서는 react-router-dom이 바로 import 되지 않는 것으로 알고 있다

.

import 를 쓰려해도 아마 terminal에서 module not found가 드거나 import가 impossible 하다고 뜰 것이다.

 

 

우회적인 방법을 써야 하는데 나는 여기를 참고하였다.

 

react.vlpt.us/redux-middleware/12-redux-saga-with-router.html

 

12. saga에서 라우터 연동하기 · GitBook

12. saga에서 라우터 연동하기 우리가 이전에 redux-thunk를 배울 때 thunk함수에서 리액트 라우터의 history 를 사용하는 방법을 배워보았습니다. 예를 들어서 로그인 요청을 할 때 성공 할 시 특정 주소

react.vlpt.us

react-router.vlpt.us/1/05.html

 

1-5. withRouter · GitBook

05. withRouter 라우트가 아닌 컴포넌트에서 라우터에서 사용하는 객체 - location, match, history 를 사용하려면, withRouter 라는 HoC 를 사용해야합니다. src/components/ShowPageInfo.js import React from 'react'; import { wi

react-router.vlpt.us

 

 

john ahn을 만나기 전 나의 빛과 소금이었던 velopert..

 

사실 이 방법 외에도 다른 방법이 있을 것 같은데 나는 잘 모르겠다.

 

만약 내 블로그를 방문한 분들 중 더 좋은 방법을 아시는 분이 있다면 꼭 댓글을 달아주셨으면 좋겠다. (제발요)

 

 

일단 오늘까지 처리한 것만 업로드하겠다.

화면 이동이 끝까지 안 되는데 그건 내일 출근해서 알아봐야 할 듯하다.  (해결! 해결된 내용은 아래에 있다!)

 

 

먼저 store.js 에 가서 history의 createBrowserHistory import한 후 customHistory 라는 변수에 받는다.

그리고 customHistory를 sagaMiddleware와 store 적용시킨다.

 

 

 

 

store.js의 full code는 다음과 같다.

 

// store.js

import { configureStore } from "@reduxjs/toolkit";
import createSagaMiddleware from "redux-saga";
import logger from "redux-logger";
import rootReducer from "./slice/rootSlice";
import rootSaga from "./sagas/rootSaga";
import { createBrowserHistory } from "history";

const customHistory = createBrowserHistory();
const sagaMiddleware = createSagaMiddleware({
  context: { history: customHistory },
});
const initialState = {};

const store = configureStore({
  reducer: rootReducer,
  middleware: [sagaMiddleware, logger],
  devTools: true,
  preloadedState: initialState,
});

sagaMiddleware.run(rootSaga);

export default store;

 

 

 

그리고 index.js 로 가서 똑같이 history의 createBrowserHistory import한 후 customHistory 라는 변수에 받는다.

BrowserRouter 컴포넌트에 history라는 property로 customHistory 를 넣어준다.

 

 

 

 

index.js의 full code는 다음과 같다.

 

// index.js

import React from "react";
import ReactDOM from "react-dom";
import App from "./components/App";
import { Provider } from "react-redux";
import { BrowserRouter } from "react-router-dom";
import { createBrowserHistory } from "history";
import reportWebVitals from "./reportWebVitals";
import store from "./store";

const customHistory = createBrowserHistory();

ReactDOM.render(
  <Provider store={store}>
    <BrowserRouter history={customHistory}>
      <App />
    </BrowserRouter>
  </Provider>,
  document.getElementById("root")
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

 

 

 

 

 

그리고 articleSaga 로 가서

 

const postedData = yield Axios.post(`http://localhost:4000/board/`, data);

 

이 코드를 아래처럼 바꿔주었다.

 

 

const response = yield Axios.post(`http://localhost:4000/board/`, data);

 

 

 

 

사실 안 바꿔도 상관없는데 나는 response.data 이런 형식으로 쓰는 게 더 익숙해서 바꿔주었다.

 

 

 

 

그리고 redux-saga/effects 에서 getContext를 import 한후 getContext("history") 값을 history 라는 변수에 담아준다.

 

import { getContext } from "redux-saga/effects";

  const history = yield getContext("history");

 

 

 

 

history 에 화면 이동 경로인 `/article/${response.data.id}`를 push해준다.

 

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

 

 

 

full code는 다음과 같다.

 

import { call, put, getContext } from "redux-saga/effects";
import Axios from "axios";
import { articleActions } from "../slice/articleSlice";

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

  const response = yield Axios.post(`http://localhost:4000/board/`, data);

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

  console.log(response.data.id);

  const history = yield getContext("history");

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

}

 

 

 

여기서 마주한 문제점은 주소 이동은 되는데 컴포넌트까지 불러오지 못한다는 점이다.

 

새로고침하면 컴포넌트가 그제서야 바뀌긴 한다.

 

지금 ArticlePage 컴포넌트는 다음과 같이 작성해놓은 상태이다.

 

 

 

// ArticlePage.js

import React from "react";

function ArticlePage({ match, location }) {
  return <div>ArticlePage {match.params.articleId}</div>;
}


export default ArticlePage;

 

새로고침 후 화면을 보면 match.params.articleId 로 생성된 글 id가 잘 들어간 게 보인다.

 

 

 

아마 Redirect 문제가 아닐까 싶은데

 

오늘 하루 안에 해결방법을 찾지 못해서 내일도 이어가려고 한다.

 

 

 

11/2 ------------->

 

 

출근하자 마자 아침부터 찾아봤다.

 

 

어제 온갖 키워드로 검색해도 마음에 드는 답변을 못 찾았는데

 

 

 

오늘 아침에 찾은 것도 솔직히 반신반의했으나 바로 해결되어서 속시원하였지만 허무하기도 하였다.

 

 

내가 참고한 답변은 아래 사이트이다.

 

역시 모든 해결책은 stackoverflow에 있나니..

 

 

stackoverflow.com/questions/42941708/react-history-push-is-updating-url-but-not-navigating-to-it-in-browser

 

React history.push() is updating url but not navigating to it in browser

I've read many things about react-router v4 and the npm history library to this point, but none seems to be helping me. My code is functioning as expected up to the point when it should navigate...

stackoverflow.com

 

 

페이지 이동 문제를 해결하면서 리팩토링도 같이 진행하려 한다.

 

 

앞서 store와 middleware에 createBrowserHistory를 매번 import하고 customHistory 변수로 값을 받으면 나중에 유지보수가 어려워질 것 같아 /src/utils/history.js 파일을 따로 만들었다.

 

// /src/utils/history.js

import { createBrowserHistory } from "history";

export default createBrowserHistory();

 

 

 

 

그리고 index.js와 store.js, articleSaga.js 에서 /src/utils/history.js 를 import 하였다.

이렇게 하면 createBrowerHistory를 두 번 이상 import + 선언할 필요가 없다.

 

// index.js

import React from "react";
import ReactDOM from "react-dom";
import App from "./components/App";
import { Provider } from "react-redux";
import { BrowserRouter } from "react-router-dom";
import reportWebVitals from "./reportWebVitals";
import store from "./store";
import history from "./utils/history"; // 수정된 부분

ReactDOM.render(
  <Provider store={store}>
    <BrowserRouter history={history}> {/* 수정된 부분 */}
      <App />
    </BrowserRouter>
  </Provider>,
  document.getElementById("root")
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

 

 

// store.js

import { configureStore } from "@reduxjs/toolkit";
import createSagaMiddleware from "redux-saga";
import logger from "redux-logger";
import rootReducer from "./slice/rootSlice";
import rootSaga from "./sagas/rootSaga";
import history from "./utils/history"; // 수정된 부분

const sagaMiddleware = createSagaMiddleware({
  context: { history: history }, // 수정된 부분
});
const initialState = {};

const store = configureStore({
  reducer: rootReducer,
  middleware: [sagaMiddleware, logger],
  devTools: true,
  preloadedState: initialState,
});

sagaMiddleware.run(rootSaga);

export default store;

 

 

index와 store는 import 부분만 바꿔주고 const customHistory = createBrowserHistory(); 이 코드 지운 뒤에 변수 이름을 history로 고쳐주면 될 것이다.

 

 

 

articleSaga.js에서는 getContext 를 빼고 history에서  바로 push하는 것으로 바꾸었다.

 

 

// articleSaga.js

import { call, put } from "redux-saga/effects";
import Axios from "axios";
import { articleActions } from "../slice/articleSlice";
import history from "../utils/history";

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

  const response = yield Axios.post(`http://localhost:4000/board/`, data);

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

  console.log(response.data.id);

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

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

 

history.push(url) 과 history.push(url, object) 이 두 방법이 있는데 url 페이지로 이동하는 것은 둘다 같으나

history.push(url, object) 일 경우 object는 state로 보내진다.

 

나는 둘 중 어느 방법으로 쓸지 몰라 일단 object도 보내는 것으로 하고 url만 push 하는 부분은 주석처리해놨다.

 

 

그리고 화면이 넘어갔을 때 내가 바로 전에 등록한 글인지 알 수 있도록 ArticlePage 컴포넌트에 생성된 Id가 바로 보일 수 있도록 match.params.articleId 로넣어놨다.

 

// ArticlePage.js

import React from "react";

function ArticlePage({ match, location }) {
  console.log(match.params.articleId);
  return <div>ArticlePage - id: {match.params.articleId}</div>;
}



export default ArticlePage;

 

 

 

 

 

이제 문제해결 부분인데

위 참조해놓은 stackoverflow를 보면 index.js 에서 App 컴포넌트를 react-router-dom의 BrowserRouter 가 아니라 react-router의 Router로 감싸줘야 redirect 가 올바르게 기능한다고 한다.

 

 

매우 간단한 해결 방법이었다.

 

 

// index.js

import React from "react";
import ReactDOM from "react-dom";
import App from "./components/App";
import { Provider } from "react-redux";
import { Router } from "react-router"; // 수정
import reportWebVitals from "./reportWebVitals";
import store from "./store";
import history from "./utils/history";

ReactDOM.render(
  <Provider store={store}>
    <Router history={history}> {/* 수정 */}
      <App />
    </Router>
  </Provider>,
  document.getElementById("root")
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

 

 

수정 후 json-server를 열고 바로 글을 등록해보면

 

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

 

 

RegisterPage
ArticlePage

 

 

성공한 것을 볼 수 있다!

 

 

 

 

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

 

 

 

 

 

들어가기 전에 이전까지 만든 소스에 문제가 없는지

 

(블로그 포스팅하면서 내가 소스 수정하고 빼먹은 부분이 있을 수도 있기에)

 

잠깐 확인하고 본격적으로 내용에 들어가겠다.

 

봐야 할 파일은 총 6개이며

해당 파일 소스 코드를 아래 코드블록에 작성해놓을테니 비교해서 빠진 부분이 있다면 수정하길 바란다.

앞서 작성한 포스팅에서 이미지와 코드 블록을 수정하긴 했지만 빠진 부분이 있다면 계속 실행이 되지 않을 것이므로 여기서 확인하고 가야 한다.

 

 

 

RegisterPage.js

 

// /src/components/views/RegisterPage.js

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

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

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

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

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

  const onSubmitArticle = (event) => {
    event.preventDefault();
    const article = { title: TitleValue, content: ContentValue };
    dispatch(articleActions.registerArticle(article));
  };

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

export default RegisterPage;

 

 

 

RegisterOrEdit.js

// /src/components/views/RegisterOrEdit.js

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

const { TextArea } = Input;

function RegisterOrEdit(props) {
  return (
    <div style={{ maxWidth: "700px", margin: "2rem auto" }}>
      <Link to="/">
        <Button>←</Button>
      </Link>
      <form onSubmit={props.handleSubmit}>
        <br />
        <div style={{ maxWidth: "700px", margin: "2rem" }}>
          <label>Title: </label>
          <Input
            onChange={props.handleTitleChange}
            value={props.titleValue}
            type="text"
            name="title"
          />
          <hr></hr>
          <TextArea
            onChange={props.handleContentChange}
            value={props.contentValue}
            name="content"
          />
        </div>
        <Button type="danger" onClick={props.handleSubmit}>
          {props.updateRequest ? "수정" : "등록"}
        </Button>
      </form>
    </div>
  );
}

export default RegisterOrEdit;

 

 

 

 

articleSlice.js

 

// /src/slice/articleSlice.js

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

export const articleSlice = createSlice({
  name: "article",
  initialState: { id: 0, title: "", content: "", views: 0 },
  reducers: {
    registerArticle: (state, { payload: article }) => {
      console.log(article);
    },
    registerArticleAsync: (state, { payload }) => {
      console.log(payload);
      debugger;
      return {
        ...state,
        id: payload.id,
      };
    },
  },
});

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

 

 

 

articleSaga.js

 

// /src/sagas/articleSaga.js

import { call, put } from "redux-saga/effects";
import Axios from "axios";
import { articleActions } from "../slice/articleSlice";

export function* registerArticleAsync(action) {
  const data = action.payload;
  debugger;
  // yield put(articleActions.registerArticleAsync(data));
  
}

 

 

 

 

rootSaga.js

 

// /src/sagas/rootSaga.js

import { takeEvery, takeLatest } from "redux-saga/effects";
import { articleActions } from "../slice/articleSlice";
import { registerArticleAsync } from "./articleSaga";

const { registerArticle } = articleActions;

export default function* rootWatcher() {
  yield takeLatest(registerArticle.type, registerArticleAsync);
}

 

 

 

 

board.json

 

( ./board.json )

{
  "board": [

  ]
}

 

 

 

 

확인이 끝났으면 이제 json-server를 띄우겠다.

6편에서 한 것처럼 rootfolder에서 terminal을 열어 아래를 입력한다.

 

 

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

 

json-server 가 install 된 상태라면

 

json-server --watch board.json --port 4000

 

 

나는 포트번호로 4000번을 사용했지만 이건 임의로 정한 거라 다른 번호로 바꿔도 된다.

다만 이따 post와 get 부분에서 포트번호를 사용할 경우가 있으니 그때도 참고해서 같이 변경해줘야 한다.

 

 

 

 

json server가 완료됐다면 http://localhost:4000/board 로 들어가보자

 

들어가면 json-server를 띄운 터미널에 이렇게 떠있을 것이다.

 

json-server

 

서버에 있는 정보를 GET으로 들고온다.

 

 

json-server를 이용하면서 Axios와 연동하여 데이터를 전송하고 전달받을 때 꼭 이 터미널을 같이 봐주길 바란다.

 

서버에 해당하는 오류들은 계속 이 터미널에서 뜰 것이다.

 

 

리액트 앱 컴파일 오류가 없는데 웹 devTool에서 원인 모를 오류가 나는 경우에도 꼭 이 서버를 띄운 터미널을 한번 체크해보길 바란다.

 

 

 

 

 

 

전송은 생각보다 매우 쉽다.

 

 

데이터 흐름을 위해 앞에서 reducer와 saga를 만들어놨으므로

 

기존에 만들어놓은 articleSaga의 registerArticleAsync 함수에 Axios 한 줄만 추가하면 끝이다.

 

 

 

// articleSaga.js

import { call, put } from "redux-saga/effects";
import Axios from "axios";
import { articleActions } from "../slice/articleSlice";

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

  const postedData = yield Axios.post(`http://localhost:4000/board/`, data);

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

  console.log(postedData);

}

 

Axios는 아마 saga 파일 처음 만들 때부터 import 되어 있을 것이다.

그러나 import가 없다면 추가하길 바란다.

 

 

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

 

이 부분은  Axios의 post method로 data 객체를 http://localhost:4000/board/ url로 요청을 보내주는 것이다.

 

axios 에서는 get 으로 객체를 보낼 수는 없게 되어 있다.

 

 

github.com/axios/axios#axios-api

 

axios/axios

Promise based HTTP client for the browser and node.js - axios/axios

github.com

 

api를 보면

axios.get(url[, config])

axios.post(url[, data[, config]])

 

이렇게 달리 되어있다. 그래서 사용에 주의가 필요하다.

 

 

 

 

Axios 를 더 알아보고 싶다면 위의 공식문서와 다음 사이트들을 참고하면 좋다.

tuhbm.github.io/2019/03/21/axios/

 

Axios를 사용하여 HTTP요청하기

Axios소개Axios는 HTTP통신을 하는데 매우 인기있는 Javascript라이브러리입니다. Axios는 브라우저와 Node.js 플랫폼에서 모두 사용할 수 있습니다. 또한 IE8이상을 포함한 모든 최신 브라우저를 지원합니

tuhbm.github.io

xn--xy1bk56a.run/axios/guide/

 

Axios | Axios 러닝 가이드

 

xn--xy1bk56a.run

 

 

 

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

  console.log(postedData);

저장하는 데 영향을 아예 주지 않지만

저장이 잘 되었는지 확인차 alert를 추가했고 Axios.post() 했을 때 response 로 어떤 데이터가 오는지도 확인하기 위해 postedData로 받아 콘솔창에서 살펴보자.

 

 

 

 

 

 

 

 

이렇게 아무말이나 쳐보고 등록을 누르면

저장해보기

 

 

alert 메시지가 뜨고

 

 

alert

 

확인을 눌러주면 (여기까진 콘솔창에 postedData 안 뜬 상태임)

 

 

콘솔창에 이렇게 뜬다!

 

console

data 부분에 내가 적은 내용과 적지 않았는데 json-server에서 자동으로 시퀀스 준 id까지 받아온 값을 볼 수 있다.

 

 

 

 

 

 

json-server 를 띄운 터미널로 가볼까

board.json

 

음 성공적인 POST

 

 

 

 

 

 

 

그렇다면 http://localhost:4000/board 에 들어가보자!

 

 

처음에 이랬던 board.json이

board.json

 

 

 

새창으로 들어가거나 새로고침을 하면

 

board.json

 

 

id 값이 다 들어가있는 것을 볼 수 있다!

 

 

 

 

근데 문제점이 생겼다.

 

articleSlice를 보면 initialState 로 조회수를 나타내는 views: 0 이 들어가있는데 서버에는 반영이 안 되어 있다.

 

아무래도 state의 views를 잡아오지 않아서 그런 것 같다.

 

 

그래서 redux의 hook인 useSelector를 사용하도록 하겠다.

 

 

 

RegisterPage 컴포넌트에 useSelector를 import 하고 state의 views를 가져온 뒤(articleReducers 에 있는 것을 주의) article 객체에 key:value를 추가해주자.

 

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

  const views = useSelector((state) => state.articleReducers.views);

  const onSubmitArticle = (event) => {
    event.preventDefault();
    const article = { title: TitleValue, content: ContentValue, views: views };
    dispatch(articleActions.registerArticle(article));
  };

 

 

그리고 저장을 한 뒤 등록을 새로 해보면??

 

새로 등록

 

 

 

board.json에도 아주 잘 들어간 것을 알 수 있다.

 

board.json

 

 

 

나중에 조회할 때 데이터가 제자리에 잘 들어가 있어야 오류가 발생하지 않으므로 validation check를 집어넣겠다.

 

null 체크만 해주면 된다.

 

 

 

RegisterPage 컴포넌트에 onSubmitArticle 함수를

 

  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 = { title: TitleValue, content: ContentValue, views: views };
    dispatch(articleActions.registerArticle(article));
  };

다음과 같이 수정해준다.

 

 

 

그러면 빈값이 생길 때마다 다음과 같은 알림이 뜨며 글 등록을 막는다.

title empty
content empty

 

 

 

앞서 저장을 테스트 해본 board.json에는 id, title,content, views 의 값이 모두 잘 들어가 있어야 뒤에 구현할 게시글 조회에서 오류가 생기지 않을 것이기 때문에

 

board: [] 안의 내용을 직접 다 지우고 다시 저장하도록 하자 (실무에서는 이러면 큰일난다!)

(혼자 연습용으로 만드는 간단한 페이지이기 때문에 가능한 일이다.)

 

 

 

실행되어 있던 서버를 끄고 지운 다음에 다시 켜야 텅 빈 board.json을 볼 수 있다.

 

board.json을 수정한 뒤 서버를 다시 실행했을 때 화면

 

 

 

참고로 저장할 때 VSCode에 board.json을 띄워놓고 저장하면 실시간으로 값이 들어가지는 걸 볼 수 있다.

 

vscode json

 

 

 

 

useSelector를 쓴 김에 필드를 두 개 더 추가하겠다. 서버에 timestamp 찍히는 것처럼 Slice에서 다음과 같이 추가 한다. 

 

 

// articleSlice.js

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

export const articleSlice = createSlice({
  name: "article",
  initialState: {
    id: 0,
    title: "",
    content: "",
    views: 0,
    date: new Date(Date.now()), // 추가된 부분
    editDate: new Date(Date.now()), // 추가된 부분
  },
  reducers: {
    registerArticle: (state, { payload: article }) => {
      console.log(article);
    },
    registerArticleAsync: (state, { payload }) => {
      console.log(payload);
      debugger;
      return {
        ...state,
        id: payload.id,
      };
    },
  },
});

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

 

 

그리고 RegisterPage 컴포넌트에 useSelector 변수 date 와 editDate를 추가하는데 어차피 RegisterPage에서 view와 date, editDate가 이 화면에서 수정되거나 수정된 값이 나타날 일은 거의 없기 때문에 렌더링 될 때마다 새로 객체가 생겨나도 상관이 없다. 그래서 하나의 useSelector에서 한꺼번에 써준다

 

 

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

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

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

 ...

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

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

  ...
}

export default RegisterPage;

 

 

파일을 저장한 뒤 글을 등록했을 때 json-server에 어떻게 들어가는지 확인해보자

 

 

board.json

 

두둥두둥

 

editDate는 일단 빈 string으로 들어가는 것으로 처리했다.

 

나중에 바꿔줄 때 어떤 일이 일어날지는 모르겠지만

 

그건 그때가서 생각하기로 한다 ^___________________^ (해본 적이 없어서 잘 모름)

 

 

글이 너무 길어져서 이번 편은 여기서 끝내고 다음 편은 response 로 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

 

반응형
반응형

 

 

 

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

 

 

 

 

 

게시글 저장 구현에 본격적으로 들어가기 전에 임시 서버를 띄워보도록 하겠다.

 

그나마 과정이 간단한 json-server를 이용해보려고 한다.

 

 

json server의 대략적인 내용은 이 블로그를 참조하면 좋다.

(나는 간단하게 쓸 거라 json-server가 어떻게 실행되는지 알아만 봤을 뿐 따라하진 않았다)

 

min9nim.github.io/2018/10/json-server/

 

[json-server] 30초 안에 RESTful API서버 만들기

프론트엔드 개발자에게 백엔드서버 구축은 여간 귀찮은 일이 아니다. 백엔드 구축이 귀찮은 분들에게 json-server 라는 신박한 녀석을 소개한다. json-server 는 내부적으로 lowdb 라는 단순한 데이터베

min9nim.github.io

 

 

 

 

 

 

 

root folder 바로 안에 (src 폴더 바깥을 의미함) board.json 파일을 만들어 준다.

 

 

{"board" : 
    [
        
    ]
}

 

 

 

그리고 rootfolder 위치에서 terminal을 연다.

 

npm start / yarn start 되어 있는 상태가 아니라면 바로 VSCode terminal에서 입력해도 된다.

 

입력할 내용은 다음과 같다.

 

 

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

 

4000번은 내가 임의로 부여한 포트번호이고 원하는 번호를 쓰면 된다.

 

 

 

 

 

완료되면 이렇게 뜬다

 

 

json-server

 

 

 

 

근데 terminal을 보면 npx: installed 가 써있다.

 

 

 

npx 가 무엇인지 구글링 해보면

 

npx is a npm package runner (x probably stands for eXecute). The typical use is to download and run a package temporarily or for trials. create-react-app is an npm package that is expected to be run only once in a project's lifecycle. Hence, it is preferred to use npx to install and run it in a single step.

 

출처: stackoverflow.com/questions/50605219/difference-between-npx-and-npm

 

 

 

 

npx는 임시적으로 패키지를 다운받아 실행시켜주는 경우 사용하는 명령어이다.

create-react-app 같이 단발성으로 끝나는 명령 같은 경우 npx를 사용하는 것을 권장한다.

 

 

 

 

만약 지금 페이지 만드는 과정을 하루만에 끝내버리거나 json-server를 여러번 실행시키지 않을 거라면 npx 명령어를 사용하는 것이 낫겠지만

하루에 json-server를 여러번 실행해야 한다면

 

 

npm install json-server --save

json-server --watch board.json --port 원하는 포트번호

 

 

이런식으로 json-server를 다운로드 받아 사용하는 것을 추천한다.

 

 

 

 

 

 

그리고 만들어놓은 json 파일이 웹상에서 어떻게 보이는지도 확인하고 싶다면

 

{"board" : 
    [
        
    ]
}

 

 

 

 

이런식으로 json-server에서 보여줬던 대로 웹에 주소를 복붙해서 들어가 보자

 

 

json-server

 

 

 

만들었던 내용대로 화면이 나타나는 것을 알 수 있다.

 

 

json-server /board

 

 

 

 

 

 

4편에서 만들어 놓은 RegisterPage 컴포넌트, rootSaga, articleSaga, articleSlice를 보며 지금 흐름이 어디까지 어떻게 진행되었는지 먼저 살펴보겠다.

 

 

게시물 등록화면에서 내용을 입력한 후 등록을 누르면

 

 

RegisterPage.js

 

 

이 onSubmitArticle 이벤트를 타고 입력한 내용이 article에 담긴 후에 registerArticle 액션생성함수를 호출한다. 

이때 action payload로서 article 객체를 가져간다.

 

 

 

호출된 액션생성함수는 articleSlice의 registerArticle이고 아무것도 바뀌지 않게 만들어 놨다.

 

 articleSlice

 

json-server 는 자동으로 id를 생성해주기 때문에 id 필드가 숫자로 꼭 있어야 한다.

 

 

안 그러면 id를 찾을 수 없다고 json-server 를 띄운 terminal 창에서 오류가 난다!

 

 

initialState에 어떤 숫자를 써놔도 insert할 때마다 json-server에서 자동으로 시퀀스를 매겨 들어간다.

 

 

 

그리고 rootSaga를 보면 registerArticle 액션생성함수가 호출(타입이나 함수 자체도 됨)된 것을 캐치하여 articleSaga의 registerArticleAsync를 호출하도록 만들어놨다.

 

 

rootSaga.js

 

 

 

 

 

그렇게 해서 호출된 articleSaga의 registerArticleAsync 함수를 보면

 

articleSlice

 

console

 

action.payload 값에 아까 같이 보내준 article 객체 값이 들어가 있는 것을 알 수 있다.

 

 

 

 

 

 

articleSaga.js

 

이제 data 라는 객체에 action.payload를 보관하여 registerArticleAsync 액션함수를 호출하면서 해당 액션 registerArticleAsync 의 payload로 data를 다시 보내준다.

 

put 은 redux-saga 버전의 dispatch라고 생각하면 된다.

 

 

 

 

 

 

articleSlice

 

 

호출된 registerArticleAsync 액션생성함수의 구조를 articleSlice에서 보면

위의 코드 상에서(console.log(payload)) payload 값으로 아까 articleSaga에서 보냈던 data가 들어와있음을 알 수 있다.

 

 

console

 

 

 

 

 

 

redux-toolkit을 중간에 사용하게 되면 기존에 쓰던 reducer 와는 리턴되는 key-value 구조가 약간 다르니 꼭 값이 어떻게 들어오는지 확인하는 것을 권한다.

 

 

 

 

 

값이 들어오는 것을 확인 했으니 다음에는 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 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

 

 

 

 

 

 

 

시간이 부족한 관계로 서버 연동 + 저장 기능은 가능하면 저녁 이후 혹은 내일에 해야 할 것 같다.

 

남은 시간 동안 뭘할까 하다가 antd 라이브러리 (Ant design)를 이용하여 만든 컴포넌트들을 예쁘게 바꿔주려 한다.

 

 

ant.design/

 

Ant Design - The world's second most popular React UI framework

Stories about Ant Design 4.0: VirtualList 🏃 In React, usually you don't need to focus on performance problems. However, as a component library, we have to think about it.

ant.design

 

사실  antd 말고 material UI 나 다른 어떤 것이든 상관 없다. 다만 ui마다 사용 방법이 다르니  API를 참고하여 적용해야 한다.

 

antd 적용에 들어가기 전에 RegisterOrEdit 컴포넌트에 '뒤로가기' 버튼을 하나 더 추가했다.

 

history 기능은 아직 사용하지 않았고 react-router의 Link로 첫페이지 연결만 해놓았다.

 

 

 

// RegisterOrEdit.js

import React from "react";
import { Link } from "react-router-dom";

function RegisterOrEdit(props) {
  return (
    <div>
      <Link to="/">
        <button>←</button>
      </Link>
      <form onSubmit>
      ...
      </form>
      
      ...
    </div>
  );
}

export default RegisterOrEdit;

 

위나 아래 아무곳에나 위치해도 되지만 이왕이면 form 태그 바깥에 위치하는 것이 좋을 것 같다.

 

 

antd는 1편에서 이미 yarn add 로 설치해놨기 때문에 바로 쓰면 된다.

 

 

 

 

우선 /src/App.css  .App 보다 윗줄에 아래 문장을 추가해 준다.

 

 

// /src/App.css

@import '~antd/dist/antd.css';

 

css

 

 

그리고 /src/App.js 에 App.css를 import 해준다.

 

 

// /src/App.js

import "../App.css";

function App() {

...

}

 

 

 

 

 

 

 

바꾸고 싶은 컴포넌트를 고른다.

 

나는 RegisterOrEdit 컴포넌트에 있는 button을 바꿔줄 것이다.

 

 

 

atnd 의 Button 을 import 해주고 button 태그를 Button으로 바꿔준다.

모양은 아직 설정 안했지만 반영이 됐는지 보기 위해 수정/등록버튼 부분에만 type="danger"를 추가해본다.

 

 

// RegisterOrEdit.js

import { Button } from "antd";

<Link to="/">
    <Button>←</Button>
</Link>

<Button type="danger" onClick={props.handleSubmit}>
    {props.updateRequest ? "수정" : "등록"}
</Button>

 

 

 

 

바뀐 화면을 볼까

 

antd 적용

 

 

 

 

 

 

title input 과 content textarea 부분도 못생겼으니 바꿔보자.

 

 

같은 방법으로 import 부분에만 추가하면 되는데 textarea는 조금 다르다.

 

textarea는 Input의 하위 컴포넌트 이기 때문에 따로 빼줘야 한다.

 

 

import { Button, Input } from "antd";

const { TextArea } = Input;

<Input
  onChange={props.handleTitleChange}
  value={props.titleValue}
  type="text"
  name="title"
/>

<TextArea
  onChange={props.handleContentChange}
  value={props.contentValue}
  name="content"
/>

 

 

 

 

 

화면을 보면!

 

 

antd 적용

 

 

 

 

width 를 설정하지 않아 여전히 못생겼다.

 

임시 방편으로  style을 설정해주자.

 

원래는 css에서 클래스로 사용하는 것이 낫지만 나는 큰 프로젝트도 아니고 페이지 하나 만드는 것이기 때문에 그냥 해당 파일에서 style을 설정해주는 것으로 하겠다.

 

 

 

바꾼 코드 모양이다.

 

// RegisterOrEdit.js

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

const { TextArea } = Input;

function RegisterOrEdit(props) {
  return (
    <div style={{ maxWidth: "700px", margin: "2rem auto" }}>
      <Link to="/">
        <Button>←</Button>
      </Link>
      <form onSubmit>
        <br />
        <div style={{ maxWidth: "700px", margin: "2rem" }}>
          <label>Title: </label>
          <Input
            onChange={props.handleTitleChange}
            value={props.titleValue}
            type="text"
            name="title"
          />
          <hr></hr>
          <TextArea
            onChange={props.handleContentChange}
            value={props.contentValue}
            name="content"
          />
        </div>
        <Button type="danger" onClick={props.handleSubmit}>
          {props.updateRequest ? "수정" : "등록"}
        </Button>
      </form>
    </div>
  );
}

export default RegisterOrEdit;

 

 

 

 

 

화면에 반영 되면 이런 모습이다.

 

훨씬 예뻐졌다.

 

 

 

antd Button, Input, TextArea

 

 

 

 

 

 

 

BoardPage 컴포넌트도 예쁘게 바꿔주자

 

 

간단하게 Button과 Title만 가져와보겠다.

 

 

 

// BoardPage.js

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

const { Title } = Typography;

function BoardPage() {
  return (
    <div style={{ maxWidth: "700px", margin: "2rem auto" }}>
      <div>
        <Title>Board Title</Title>
      </div>
      <div>
        <Link to="/register">
          <Button type="primary">New Post</Button>
        </Link>
      </div>
      <div>
        <BoardList />
      </div>
    </div>
  );
}

export default BoardPage;

 

 

 

 

 

많이는 아니어도 처음보다는 예뻐졌다

 

 

결과

 

 

 

 

 

 

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

 

 

 

 

 

3편에서 RegisterPage.js 를 글 수정할 때도 재사용할 것이라고 언급한 적이 있었는데 RegisterPage 컴포넌트는 프레젠테이셔널 컴포넌트와 컨테이너 컴포넌트가 합쳐져 있는 상태라 재사용할 수 없는 구조여서 파일 구조 수정이 필요하게 되었다.

 

 

RegisterPage.js 가 들어있는 디렉토리만 바꾸면 되고 바뀐 구조는 이렇다

 

changed directory

 

RegisterPage 를 컨테이너 컴포넌트로 사용하고 RegisterOrEdit을 프레젠테이셔널 컴포넌트로 사용할 예정이다.

 

 

containter component 와 presentational component에 대해선 이  redux 공식홈페이지에 잘 나와있다.

 

redux.js.org/basics/usage-with-react#presentational-and-container-components

 

Usage with React | Redux

Basic Tutorial > Usage with React: How to use Redux with React components

redux.js.org

 

 

 

 

우선 RegisterPage 컴포넌트에선 RegisterOrEdit 이라는 컴포넌트를 return 하는 방식으 대체했다.

(console.log 는 compile success 확인 여부이니 신경쓰지 않아도 된다)

 

container component 로 쓸 RegisterPage

 

 

 

RegisterOrEdit 컴포넌트의 모습은 DOM형태만 만들어주고 값과 이벤트는 RegisterPage의 props로 받아쓰므로 단순하게 생겼다. 사실 return 값은 이전 RegisterPage 컴포넌트와 거의 다를 게 없다.

 

presentational component 로 사용핧 RegisterOrEdit

 

 

console 창에 값이 잘 나오는지 확인해보자

 

성공!

 

 

 

 

 

 

 

이젠 form submit 부분을 만들 것이다.

 

우선  RegisterPage 컴포넌트에 이벤트를 간단하게 생성하고 RegisterOrEdit 컴포넌트에 props로 전달한다.

 

 

// RegisterPage.js

  const onSubmitArticle = (event) => {
    event.preventDefault();
    const article = { title: TitleValue, content: ContentValue };
    console.log(article);
  };
  
    return (
    <>
      <RegisterOrEdit
        titleValue={TitleValue}
        contentValue={ContentValue}
        handleTitleChange={onTitleChange}
        handleContentChange={onContentChange}
        handleSubmit={onSubmitArticle}
      />
    </>
  );

 

 

 

값이 많을 때는 프레젠테이셔널 컴포넌트의  form 태그 내 value를 모두 긁어와서 setStateValue 하는 방법도 있지만 보낼 값이 적으니 객체 만들어 보내는 쉬운 방법으로 하기로 한다.

 

 

const article = { title: TitleValue, content: ContentValue };

 

 

 

 

 

 

RegisterOrEdit 컴포넌트에서 전달받은 handleSubmit 이벤트는 form 태그의 onSubmit 에 아직 적용시키지 않고 aritcle 값을 먼저 확인하기 위해 button의 onClick에만 적용한다.

 

 

// RegisterOrEdit.js

<button onClick={props.handleSubmit}>Submit</button>

 

 

 

 

 

 

파일을 저장하고 버튼을 클릭해보자

 

 

RegisterPage 컴포넌트에 article 객체에 값이 잘 들어간 것을 볼 수 있다!

 

 

 

 

 

 

RegisterOrEdit 컴포넌트를 게시글 수정 때도 다시 사용하려면 새글 등록인지 글 수정인지 구별할 flag가 필요하다.

 

 

따라서 useState로 IsForUpdate 를 만들어주고 RegisterOrEdit 컴포넌트에 props로 넘긴다.

 

아직 인자 값이 없기 때문에 setIsForUpdate은 나중에 수정 기능을 만들 때 보기로 한다.

 

 

// RegisterPage.js

  const [IsForUpdate, setIsForUpdate] = useState(false);
  
    return (
    <>
      <RegisterOrEdit
        titleValue={TitleValue}
        contentValue={ContentValue}
        handleTitleChange={onTitleChange}
        handleContentChange={onContentChange}
        handleSubmit={onSubmitArticle}
        updateRequest={IsForUpdate}
      />
    </>
  );

 

 

 

 

 

 

RegisterOrEdit 컴포넌트에서는 RegisterPage 컴포넌트에서 보내준 IsForUpdate 값을 updateRequest로 받아 이 값이 true 이면 수정, false이면 등록으로 버튼이 바뀌어 나타날 수 있도록 코드를 수정해준다.

 

 

<button onClick={props.handleSubmit}>
  {props.updateRequest ? "수정" : "등록"}
</button>

 

 

 

웹 화면에서는 이렇게 나타나야 한다.

등록 버튼

 

 

 

 

 

 

수정이 완료된 RegisterPage 컴포넌트와 RegisterOrEdit 컴포넌트는 다음과 같은 모습이다.

 

 

 

RegisterPage

 

 

RegisterOrEdit

 

 

 

 

 

 

이젠 저장할 값을 서버에 반영해야 할 차례다.

 

 

긴 여정이 될 것 같은데 필요한 모듈을 설치하고 reducer와 middleware인 saga를 만들어줘야 한다.

 

액션 -> 스토어  전달 -> 리듀서(reducer)에서 액션타입에 따라 다른 payload 전달 -> 상태 변경

이 흐름은 모두 동기적으로 일어나므로 API 같은 외부 리소스를 가져오는 경우 동기적인 리덕스 흐름만으로 해결할 수없다.

 

그래서 리듀서를 타기 전 혹은 액션에서 스토어 상태 변경 전에 비동기 로직을 끼워넣을 수 있는데 이를 미들웨어(middleware)라고 한다.

 

 

참고:

jeonghwan-kim.github.io/dev/2019/07/22/react-saga-ts-1.html

 

리덕스 사가 사용하기 (타입스크립트 버전) - 1편

지난 글에서 정리한 것 처럼 리덕스는 다음 순서로 상태를 관리한다. 액션 객체 생성 스토어로 전달 리듀서가 액션 객체를 수신 액션 타입에 따라 전달받은 패이로드를 가지고 스토어 상태 변경

jeonghwan-kim.github.io

 

 

 

 

 

api 외부 리소스를 가져오는 데 사용하는 미들웨어로 대표적인 것은 redux-thunk와 redux-saga가 있으며 나는 redux-saga를 사용할 예정이다.

 

 

 

그리고 리듀서를 한 파일에서 쉽게 만들고 활용하기 위해서 redux-toolkit에서 제공하는 createAction, createSlice 등을 이용할 것이다.

(필요한 경우 다른 함수를 더 쓸 수도 있다.)

 

 

 

 

필요한 라이브러리를 설치하자.

root folder에서 terminal을 연다. (VSCode terminal을 kill 해도 되고 open native terminal 을 이용하여 열어도 된다)

 

 

 

terminal에 다음과 같이 입력한다.

 

 

yarn add redux-logger axios

 

 

 

 

설치가 끝나면 package.json에 반영되어 있을 것이다.

 

package.json

 

 

 

 

 

 

 

일단 새로 만들 파일과 그 파일들이 들어갈 디렉토리는 이렇게 생겼다.

saga와 slice(reducer)

 

 

 

 

 

sagas 디렉토리에는  saga 파일만 들어갈 것이고

slice 디렉토리에는 type, action, reducer 나 혹은 이 세 가지를 다 포함할 slice들이 만들어진 파일들이 들어갈 것이다.

 

 

 

saga와 slice가 종류별로 다양하지 않고 한 개만 있다면 rootSaga와 rootSlice는 굳이 안 만들어도 된다.

나는 혹시 몰라서 만들어놨다.

보통은 index.js로 만들어 놓는데 나중에 import할 때 헷갈릴까봐 이름을 달아놓았다.

 

 

 

reducer는 redux-toolkit의 createSlice로 만들어 사용할 건데 여기서부터는 redux를 이용한 상태 관리 흐름을 이미 알고 있다는 가정 하에 설명할 것이다.

 

모른다면 redux 공식홈페이지 설명을 먼저 읽고 간단한 tutorial을 한 뒤에 뒷 과정을 이어나가는 것을 추천한다.

 

#redux : actions

redux.js.org/basics/actions

 

Actions | Redux

Basic Tutorial > Actions: Core concept - actions are plain objects that describe events

redux.js.org

 

# redux : reducers

redux.js.org/basics/reducers

 

Reducers | Redux

Basic Tutorial > Reducers: Core concept - reducers are plain functions that return new state

redux.js.org

 

 

# redux: store

redux.js.org/basics/store

 

Store | Redux

Basic Tutorial > Store: Core concept - the Redux store

redux.js.org

 

# redux : data flow

redux.js.org/basics/data-flow

 

Data flow | Redux

Basic Tutorial > Data Flow: How data flows through a Redux app

redux.js.org

 

 

# redux : basic tutorial

redux.js.org/basics/example

 

Example: Todo List | Redux

Basic Tutorial > Todo List: Source code for the Todo List example

redux.js.org

 

 

 

 

그리고 이 사이트도 추천한다.

 

이곳도 마찬가지로 redux-saga와 redux-toolkit 를 동시에 사용했는데 특히 redux-toolkit 부분에서 엄청 도움이 되었다

 

mjn5027.tistory.com/39?category=1157737

 

[ React ] 리액트 Saga + Toolkit ( 미들웨어 사가, 리덕스 툴킷 )

지금껏 다룬 포스팅의 project 에 Middleware Saga 와 Redux Toolkit 을 적용시켜보자. 먼저 해당 포스팅에서 사용할 기능들을 설치하자. // Redux 와 React-Redux 설치 yarn add redux react-redux // Redux Tool..

mjn5027.tistory.com

 

 

 

 

 

우선 articleSlice.js 에 redux-toolkit 의 createSlice를 import 한다.

 

 

// /src/slice/articleSlice.js

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

 

 

 

redux-toolkit이 제공하는 API에서 createSlice 설명은 다음과 같다.

 

A function that accepts an initial state, an object full of reducer functions, and a "slice name", and automatically generates action creators and action types that correspond to the reducers and state.

 

출처: redux-toolkit.js.org/api/createSlice

 

 

 

 

그리고 추가할 내용은 

 

 

// articleSlice.js

export const articleSlice = createSlice({
  name: "article",
  initialState: { id: 0, title: "", content: "", views: 0 },
  reducers: {
    registerArticle: (state, article) => {
      console.log(article);
      return {
        ...article,
        id: state.id,
      };
    },
    registerArticleAsync: (state, { payload }) => {
      console.log(payload);
      debugger;
      return {
        ...state,
        id: payload.id, 
      };
    },
  },
});

 

 

 

initialState는 인용문에서 말한 initial State,

reducers: {registerArticle: ..., registerArticleAsync: ,,} 는  reducer 함수가 있는 객체,

그리고 name은 slice name 이다.

 

createSlice는 자동으로 액션 타입과 액션 생성함수를 만들어준다.

 

 

 

액션 생성함수 없는 reducer를 extraReducer 로 추가할 수 있다고 API문서에 나와있지만 나는 일반 리듀서에서 해결해볼 것이다. (일단은)

 

 

 

 

그리고 이후에 saga에서 편히 쓰기 위해 articleSlice 변수에서 action과 reducer를 빼와 export 변수로 따로 만들어 아랫줄에 추가하였다.

 

 

 

// articleSlice.js


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

 

 

 

 

 

 

 

앞의 내용을 정리하면 articleSlice.js 파일의 형태는 다음과 같다.

 

 

// articleSlice.js

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

export const articleSlice = createSlice({
  name: "article",
  initialState: { id: 0, title: "", content: "", views: 0 },
  reducers: {
    registerArticle: (state, article) => {
      console.log(article);
      return {
        ...article,
        id: state.id,
      };
    },
    registerArticleAsync: (state, { payload }) => {
      console.log(payload);
      debugger;
      return {
        ...state,
        id: payload.id,
      };
    },
  },
});

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

 

 

여기서 registerArtice 함수는 saga에서 감시할 액션으로 쓸 것이고

상태변경은 registerArticleAsync 함수는 서버 저장 후 게시물을 불러오는 데까지 이어 쓸 것이다.

 

 

 

 

 

 

articleSlice의 리듀서를 바로 store에 적용해도 되나 앞일은 모르니 rootSlice.js 파일을 만들어(앞에서 이미 만든 것과 동일한 파일임) 준다.

 

rootSlice는 사실 rootReducer라고 보면 되는데 /slice 디렉토리 안에 있기 때문에 이름을 rootSlice로 통일시킨 것뿐이다.

 

 

 

rootSlice.js

 

 

rootReducer라는 변수를 만들어 리듀서들을 묶어준다.

 

지금까지 사용한 리듀서는 일단 1개뿐이므로 articleReducer 하나만 import하여 combineReducer에 넣어준다.

 

 

 

 

 

 

 

그리고 /src 디렉토리에 바로 store.js 파일을 만들어준다.

 

 redux-saga를 미들웨어로 쓸 것이기 때문에 createStore가 아닌 redux-toolkit의 configureStore를 사용한다.

 

 

// store.js

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

 

 

 

 

 

 

그리고 사용할 미들웨어를 import 한다.

 

 

// store.js

import createSagaMiddleware from "redux-saga";
import logger from "redux-logger";

 

 

 

 

 

saga를 실행시켜줄 createSagaMiddleware을 sagaMiddleware라는 변수를 만들어 받아놓고

initialState 도 세팅해준다.

 

 

// store.js

const sagaMiddleware = createSagaMiddleware();
const initialState = {};

 

 

 

 

 

 

 

configureStore 함수 내 필요한 데이터를 집어놓고 store 라는 변수로 받아 둔다.

 

 

// store.js

const store = configureStore({
  reducer: rootReducer,
  devTools: true,
  preloadedState: initialState,
});

export default store;

 

 

여기서  devTools 는 웹의 devTool 과 연결할 것인지 여부를 적는 것이다.

preloadState는 reducer에서 정해둔 initialState보다 더 앞서 prefix될 state이다.

 

 

 

 

 

 

redux-toolkit의 default middleware는 redux-thunk이기 때문에 sagaMiddleware 로 덮어씌워줘야 한다.

그러니 이젠 saga를 만들 차례다!

 

 

 

 

 

 

글 초반에 만들어둔 articleSaga.js 파일에 generate 함수를 만든다.

saga 는 generate 함수를 바탕으로 만들어야 한다.

 

 

// articleSaga.js

export function* registerArticleAsync(action) {
  console.log(action);
  debugger;
  yield console.log("finish");
}

 

(값 확인을 위해 기본형태로만 만들었다)

 

 

 

 

 

generate 함수를 모른다면 아래 링크들을 참고하길 바란다.

 

wonism.github.io/javascript-generator/

 

WONISM | JavaScript Generator 이해하기

WONISM's Blog

wonism.github.io

developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators

 

Iterators and generators

Iterators and Generators bring the concept of iteration directly into the core language and provide a mechanism for customizing the behavior of for...of loops.

developer.mozilla.org

developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Iterators_and_Generators

 

반복기 및 생성기

컬렉션 내 각 항목 처리는 매우 흔한 연산입니다. JavaScript는 간단한 for 루프에서 map() 및 filter()에 이르기까지, 컬렉션을 반복하는 많은 방법을 제공합니다. 반복기(iterator) 및 생성기(generator)는

developer.mozilla.org

developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/function*

 

function*

function* 선언 (끝에 별표가 있는 function keyword) 은 generator function 을 정의하는데, 이 함수는 Generator 객체를 반환합니다.

developer.mozilla.org

exploringjs.com/es6/ch_generators.html

 

22. Generators

22. Generators 22.1. Overview 22.1.1. What are generators? 22.1.2. Kinds of generators 22.1.3. Use case: implementing iterables 22.1.4. Use case: simpler asynchronous code 22.1.5. Use case: receiving asynchronous data 22.2. What are generators? 22.2.1. Rol

exploringjs.com

 

 

 

 

 

 

saga 도 기능별로 다양하게 만들 수 있으므로 rootSaga를 하나 두기로 한다.

rootSaga 로서 사용할 함수도 generate 함수여야 한다.

generate 함수는 function에 별(*)이 붙어있는 모양으로 작성해야 하고 함수내 꼭 yield를 써줘야 한다. (안 그러면 오류남)

 

 

// rootSaga.js

import { takeLatest } from "redux-saga/effects";
import { articleActions } from "../slice/articleSlice";
import { registerArticleAsync } from "./articleSaga";

const { registerArticle } = articleActions;

export default function* rootWatcher() {
  yield takeLatest(registerArticle.type, registerArticleAsync);
}

 

 

rootSaga.js 에서는 일단 뷰에서 registerArticle 액션생성함수를 dispatch 하게 되면 articleSaga의 registerArticleAsync 함수를 호출하도록 하였다.

 

 

 

 

 

 

 

그리고 다시 store.js로 돌아와서 rootSaga 를 store에 탑재해준다.

 

 

// store.js

import { configureStore } from "@reduxjs/toolkit";
import createSagaMiddleware from "redux-saga";
import logger from "redux-logger";
import rootReducer from "./slice/rootSlice";
import rootSaga from "./sagas/rootSaga";

const sagaMiddleware = createSagaMiddleware();
const initialState = {};

const store = configureStore({
  reducer: rootReducer,
  middleware: [sagaMiddleware, logger],
  devTools: true,
  preloadedState: initialState,
});

sagaMiddleware.run(rootSaga);

export default store;

 

 

여기서 logger도 다른 종류의 미들웨어인데 prevState (previous state), action, next State 를 console 창에 보여주는 기능을 한다.

 

 

 

saga가 제대로 돌아가려면  꼭 run()을 해줘야 한다.

 

 

// store.js

sagaMiddleware.run(rootSaga);

 

 

 

 

 

store가 준비되었으니 index.js로 가서 store를 import 하자!

 

 

index.js

 

 

 

 

 

 

 

마지막으로 saga를 타도록 할 액션함수를 RegisterPage에서 dispatch 해야 한다.

 

 

RegisterPage 컴포넌트에서 useDispatch 라는 redux 훅과 articleSlice에서 빼둔 액션함수를 import하고  onSubmitArticle 함수를 다음과 같이 수정해야 한다.

 

 

// RegisterPage.js

import { useDispatch } from "react-redux";
import { articleActions } from "../../../slice/articleSlice";

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

  const onSubmitArticle = (event) => {
    event.preventDefault();
    const article = { title: TitleValue, content: ContentValue };
    dispatch(articleActions.registerArticle(article));
  };
  
  return (
  ...
  );
}

 

 

 

 

 

RegisterPage 컴포넌트의 최종적 모습은 다음과 같다.

 

RegisterPage

 

 

 

그리고 RegisterOrEdit 컴포넌트의 form tag 에 onSubmit 이벤트도 마무리 해주자.

<form onSubmit={props.handleSubmit}>

...

</form>

 

 

 

 

 

 

등록 버튼을 눌렀을 때 articleSaga.js 에 적은 console.log까지 잘 가는지 확인해보자.

 

 

// articleSaga.js


import { articleActions } from "../slice/articleSlice";

export function* registerArticleAsync(action) {
  console.log(action);
  debugger;
  yield console.log("finish");
}

 

 

 

 

 debugger 걸린 articleSaga

 

 

 

 

 

console 창 articleSaga

 

 

 

Axios post 는 다음 편에서 다루겠다.

 

 

 

 

 

 

목차 돌아가기: 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/6) useState보다 useSelector 사용을 더 추천합니다.

 

 

 

 

이번에는 게시글 저장을 구현해보려고 한다.

 

RegisterPage.js 의 form 태그 안에 button을 만들어준다.

 

RegisterPage

 

 

나는  RegisterPage를 게시글 등록뿐만 아니라 수정할 때도 재사용 것이기 때문에 react hook인 useState와 onChange 이벤트를 사용할 것이다.

 

 

es6, es7 code snippet extension을 이용하게 되면 각종 라이브러리의 hook 들을 손쉽게 쓸 수 있다.

 

code snippet

 

 

위의 사진처럼  useState를 입력했을 때 첫번째 자동완성은

 

 

const [state, setState] = useState(initialState)

 

이 식을 만들어주고

 

 

 

 

두번째 자동완성은

 

 

import React, { useState } from react;

 

import 문에 useState를 자동으로 추가해준다.

 

 

 

 

 

일단  useState로 TitleValue, ContentValue를 만들어주고 해당 name을 갖고 있는 태그에 value로 넣어준다.

 

 

// RegisterPage.js

import React, { useState } from "react";

function RegisterPage() {

  const [TitleValue, setTitleValue] = useState("")
  const [ContentValue, setContentValue] = useState("")
  return (
    <div>
      <form onSubmit>
        <br />
        <label>Title: </label>
        <input onChange value={TitleValue} type="text" name="title" />
        <hr></hr>
        <div>
          <textarea onChange value={ContentValue} name="content"/>
        </div>
        <button onClick>Submit</button>
      </form>
    </div>
  );
}

export default RegisterPage;

 

 

 

이렇게 작성하고 저장하게 되면 input과 textArea 의 초깃값이 '""로 지정되어있기 때문에 아무리 입력하려 해도 값이 들어가지 않는다.

 

 

 

 

 

value property를 defaultValue로 바꾸면 입력이 가능하지만 나는 이 대신 onChange 이벤트를 사용할 것이다.

 

 

<input onChange defaultValue={TitleValue} type="text" name="title" />

<textarea onChange defaultValue={ContentValue} name="content" />

 

 

 

 

onTitleChange와 onContentChange를 추가하고 console.log 로 값이 어떻게 들어가는지 확인해 보자

 

 

// RegisterPage.js

import React, { useState } from "react";

function RegisterPage() {
  const [TitleValue, setTitleValue] = useState("");
  const [ContentValue, setContentValue] = useState("");

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

  const onContentChange = (event) => {
    setContentValue(event.currentTarget.value);
  };
  console.log(ContentValue);
  return (
    <div>
      <form onSubmit>
        <br />
        <label>Title: </label>
        <input
          onChange={onTitleChange}
          value={TitleValue}
          type="text"
          name="title"
        />
        <hr></hr>
        <div>
          <textarea
            onChange={onContentChange}
            value={ContentValue}
            name="content"
          />
        </div>
        <button onClick>Submit</button>
      </form>
    </div>
  );
}

export default RegisterPage;

 

 

 

onTitleChange, onContentChange

 

 

 

 

보통은 input, textarea의 onChange 이벤트는

 

 

const onChange = (event) => {
  const { name, value } = event.target;
  if (name === "title") {
    setTitleValue(value);
  } else if (name === "content") {
    setContentValue(value);
  }
};
  
<input onChange={onChange} value={TitleValue} type="text" name="title" />
<textarea onChange={onChange} value={ContentValue} name="content" />

 

 

 

 

이런식으로 사용겠지만 나중에 효과를 다르게 넣을 수도 있고 그럴 경우 위와 같은 방식은 가독성이 떨어지기 때문에 onChange 함수를 title과 content 별로 따로 두는 것이 낫다고 판단하였다.

 

 

 

 

 

 

 

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

 

 

 

 

VSCode extension 중 open native terminal 을 설치하면

 

open in native terminal

 

root folder 와 현 파일이 들어있는 디렉토리 폴더(current folder) 중 어디서 터미널을 열 것인지 고를 수 있다.

 

 

 

 

extension은 이렇게 생겼다

open native terminal

 

 

 

VSCode 에서 사용하는 terminal은 기본적으로 rootfolder 기준이므로 yarn start / npm run 이 실행된 상태일 시 VSCode terminal을 일단 kill 하고 change directory를 사용해야 하기 때문에 번거롭다.

 

yarn 없이 npm만 사용하는 것이 아니라면 따로 terminal을 띄워서 사용해도 이제까진 괜찮았다.

 

 

 

 

 

 

 

필요한 모듈을 설치해보자

 

yarn add redux react-redux react-router-dom redux-saga @reduxjs/toolkit antd

 

상태 관리를 위한 redux와 react-redux, redux-toolkit

router 사용을 위한 react-router-dom

비동기 처리로서 사용할 middleware로 redux-saga

스타일링을 위한 antd

 

이 외 더 필요한 것이 있다면 중간에 추가하도록 하겠다.

 

 

 

 

 

 

설치가 끝나면 package.json 파일의 dependencies 에 다음과 같이 추가된다.

package.json

 

 

 

 

 

 

 

프로젝트의 기본 구조는 다음과 같이 설정하였다.

 

원래는 온라인 쇼핑몰을 만드려 했으나 상품과 주문을 연결할 정보가 마땅치 않았기 때문에 기본 게시판 구조로 가기로 하였다.

(디렉토리도 추가할 항목이 생기면 추가할 예정)

 

file tree

 

(서버는 json-server 를 이용할 예정이다)

 

 

 

 

 

 

 

첫 화면 컴포넌트를 만들어보자

 

/src/components/views/BoardPage 위치에

BoardPage.js 만든다.

 

꿀팁으로 ES6 code snippet과 ES7 code snippet 이라는 VSCode extension를 설치한다면 react 약어를 이용하여 능률을 높일 수 있다.

 

 

 

예로 rfce 를 입력하면

BoardPage.js

 

이렇게 자동완성 해준다.

컴포넌트 이름도 자동으로 만들어준다.

BoardPage.js

 

 

 

 

이제 router를 이용하여 화면 첫 페이지에 BoardPage 컴포넌트가 나오도록 만들어보자.

 

 

 

 

/src/index.js 에 react router dom의 BrowserRouter 를 import 하고 App 컴포넌트를 감싸준다.

index.js

 

 

 

 

App 컴포넌트에는 Route, Switch 를 import 하여

들어가자마자 나오는 화면에 BoardPage가 뜰 수 있도록 경로(path)를 '/' 로 지정해주고 component에 BoardPage를 넣는다.

App.js

 

 

 

 

 

파일들을 모두 저장하고 웹에서 compile 된 결과를 보면 다음과 같다. 

(왼쪽은 web화면 오른쪽은 VSCode 화면)

 

 

 

 

 

 

 

BoardPage 에는 게시판 타이틀, 게시글목록(BoardList)과 글 등록 버튼이 나오게 할 예정이다.

 

 

 

 

우선

/src/components/views/BoardPage 디렉토리에 Sections 라는 폴더를 추가하고

Sections 폴더에 BoardList.js 파일을 생성하여 BoardPage.js 에 import 한다.

 

화면은 아래와 같이 나와야 한다.

타이틀과 버튼은 아직 없는 상태

 

 

 

 

 

타이틀과 버튼까지 기본 사항으로만 추가하면 이렇다

타이틀과 버튼은 넣었으나 스타일 적용 없는 상태

 

 

 

 

 

다음은 글 화면 부분이다.

 

/src/components/views/ArticlePage 디렉토리에서 ArticlePage.js 파일을 만든다.

ArticlePage.js

 

 

 

 

 

App.js 파일로 와서 route를 생성하고 ArticlePage를 import하여 컴포넌트 프로퍼티에 넣어준다.

이때 route 경로(path)는 게시글 Id 를 프로퍼티로 받아 사용할 것이기 때문에 "/article/:articleId" 로 적는다.

 

App.js

 

 

 

 

목록에서 게시글을 클릭하면 url이 다음과 같이 '/article/게시글아이디' 형식으로 들어갈 것이다.

url result

 

 

 

 

 

 

글목록을 클릭해서 보려면 일단 글을 생성해야 한다.

 

/src/components/views 에 RegisterPage 라는 폴더를 추가하고 생성된 폴더에 RegisterPage.js 도 추가한다.

그리고 App.js 에도 route를 등록해준다. 경로는 "/register" 로 한다.

 

RegisterPage.js

 

 

 

 

 

주소에 /register 라고 매번 치기 어려우니 BoardPage의 버튼에 Link 태그를 추가하여 'New Post' 버튼을 클릭했을 때 /register 페이지로 바로 이동할 수 있도록 만든다.

 

BoardPage.js

 

 

 

 

기본적으로 틀만 만들어놓고 저장 후 App이 잘 돌아가나 테스트를 해보자

RegisterPage.js

 

 

 

 

 

 

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