React/게시판만들기 v2.

[React][CRUD] 게시판 만들기 All in One (5). 게시글 내용 조회해오기. 그리고 이제 조회수 view 증가를 곁들인. redux-saga, redux, axios, put

binaryJournalist 2021. 4. 14. 15:54
반응형

 

 

목차돌아가기:

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

 

[React][CRUD] create-board-tutorial-v2

* 인용시 출처 부탁드립니다. 완성 소스 code: github.com/cruellaDev/react-create-board-v2 cruellaDev/react-create-board-v2 updated version of react-create-board. Contribute to cruellaDev/react-create-..

binaryjourney.tistory.com

 

 

 

ArticleList 에서 나오는 게시글 목록 중 하나를 클릭하면

 

http://localhost:3000/article/2

 

이렇게 url이 바뀐다.

 

/article/:articleId 로 해놨으므로 param : { articleId: 2 } 이렇게 값을 받을 수 있을 것이다.

 

그래서 ArticleList 에서 boardId를 활용한 것 처럼 Article 에서도 같은 방법을 쓸 것이다.

 

 

 

그리고 앞서 만들어놓은 스토리보드/화면정의서에는 Comment 와 Article을 분리하여 그려져 있었는데 개발상 어려울 것 같아 Article 컴포넌트 안에 Comment 컴포넌트롤 넣기로 하였다.

 

 

그래서 Article.js 는 이런 구조로 간다.

 

// Article.js

import React from 'react';
import Comment from './Comments';

function Article() {
    return (
        <>
            <div>
                게시글 상세
            </div>
            <div>
                <Comment />
            </div>
        </>
    );
}

export default Article;

 

 

 

 

 

 

articleSlice 의 initialState 에 article: {} 를 추가하고 reducer에는  getArticle 과 upadateArticleViews 액션들을 추가한다.

 

 

// articleSlice.js

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

const name = "article";

const initialState = {
    article: {}, // 추가
    articleList: [],
    status: 0,
    statusText: "Loading",
};

const reducers = {
    getArticleList: (state, action) => {},
    getArticleListSuccess: (state, action) => {
        state.articleList = action.payload?.data ?? [];
        state.status = action.payload?.status;
        state.statusText = action.payload?.statusText ?? "Success";
    },
    getArticleListFail: (state, action) => {
        state.articleList = initialState.articleList;
        state.status = action.payload?.status ?? 500;
        state.statusText = action.payload?.statusText ?? "Network Error";
    },

    getArticle: (state, action) => {}, // 추가
    getArticleSuccess: (state, action) => {}, // 추가
    getArticleFail: (state, action) => { // 추가
        state.article = initialState.article;
        state.status = action.payload?.status ?? 500;
        state.statusText = action.payload?.statusText ?? "Network Error";
    },
    
    updateArticleViews: (state, action) => {}, // 추가
    updateArticleViewsSuccess: (state, action) => { // 추가
        state.article = action.payload?.data ?? {};
        state.status = action.payload?.status;
        state.statusText = action.payload?.statusText ?? "Success";
    },
    updateArticleViewsFail: (state, action) => { // 추가
        state.article = initialState.article;
        state.status = action.payload?.status ?? 500;
        state.statusText = action.payload?.statusText ?? "Network Error";
    }
};

const articleSlice = createSlice({
    name,
    initialState,
    reducers,
});

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

 

slice에 액션이 만들어졌으니 낚아챌 async함수를 articleSaga 에 만들자.

 

 

// articleSaga.js

import { all, call, retry, fork, put, take, select } from 'redux-saga/effects';
import { articleActions } from '../slices/articleSlice';
import axios from '../utils/axios';
import qs from "query-string";

const SECOND = 1000;

// api 서버 연결 주소
function apiGetArticle(articleId) {
    return axios.get(`articles/${articleId}`);
}

function apiGetArticleList(requestParams) {
    return axios.get(`articles?${qs.stringify(requestParams)}`);
}

function apiPutArticle(requestBody) {
    return axios.put(`articles/${requestBody?.id}`, requestBody);
}

const SECOND = 1000;

// api 서버 연결 주소
function apiGetArticle(articleId) {
    return axios.get(`articles/${articleId}`);
}

function apiGetArticleList(requestParams) {
    return axios.get(`articles?${qs.stringify(requestParams)}`);
}

function apiPutArticle(requestBody) {
    return axios.put(`articles/${requestBody?.id}`, requestBody);
}

// api 서버 연결 후 action 호출
function* asyncGetArticleList(action) {
    try {
        // const response = yield call(apiGetArticleList, { boardId: action.payload });
        const response = yield retry(3, 10 * SECOND, apiGetArticleList, { boardId: action.payload });
        if (response?.status === 200) {
            yield put(articleActions.getArticleListSuccess(response));
        } else {
            yield put(articleActions.getArticleListFail(response));
        }
    } catch(e) {
        yield put(articleActions.getArticleListFail(e.response));
    }
}

function* asyncGetArticle(action) {
    try {
        const response = yield call(apiGetArticle, action.payload);
        if (response?.status === 200) {
            yield put(articleActions.getArticleSuccess()); // 조회 성공확인만 판단하는 용도로 남김
            yield put(articleActions.updateArticleViews(response.data));
        } else {
            yield put(articleActions.getArticleFail(response));
        }
    } catch(e) {
        console.error(e);
        yield put(articleActions.getArticleFail(e.response));
    }
}

function* asyncUpdateArticleViews(action) {
    try {
        const response = yield call(apiPutArticle, {
            ...action.payload,
            views: parseInt(action.payload?.views ?? 0) + 1,
            updateDate: Date.now()
        });
        if (response?.status === 200) {
            yield put(articleActions.updateArticleViewsSuccess(response));
        } else {
            yield put(articleActions.updateArticleViewsFail(response));
        }
    } catch(e) {
        console.error(e);
        yield put(articleActions.updateArticleViewsFail(e?.response));
    }
}

// action 호출을 감시하는 watch 함수
function* watchGetArticleList() {
    while(true) {
        const action = yield take(articleActions.getArticleList);
        yield call(asyncGetArticleList, action);
    }
}

function* watchGetArticle() {
    while(true) {
        const action = yield take(articleActions.getArticle);
        yield call(asyncGetArticle, action);
    } 
}

function* watchUpdateArticleViews() {
    while(true) {
        const action = yield take(articleActions.updateArticleViews);
        yield call(asyncUpdateArticleViews, action);
    }
}

export default function* articleSaga()
{
    yield all([fork(watchGetArticleList), fork(watchGetArticle),
        fork(watchUpdateArticleViews)]);
}

 

 

java를 이용했을 경우 조회와 동시에 views 업데이트할 수 있는데 json-server는 어떻게 할지 방도를 모르겠다.

 

그래서 조회해옴과 동시에 조회 업데이트 치는 액션을 날린다.

 

근데 이 방법의 단점은 getArticle 액션을 dispatch할 때마다 조회수가 update가 되므로 다른 때에 재활용을 못한다는 것이다.

 

function* asyncGetArticle(action) {
    try {
        const response = yield call(apiGetArticle, action.payload);
        if (response?.status === 200) {
            yield put(articleActions.getArticleSuccess()); // 조회 성공확인만 판단하는 용도로 남김
            yield put(articleActions.updateArticleViews(response.data)); // 조회수 업데이트 액션 호출
        } else {
            yield put(articleActions.getArticleFail(response));
        }
    } catch(e) {
        console.error(e);
        yield put(articleActions.getArticleFail(e.response));
    }
}

 

 

apiPutArticle 함수는 게시글 수정시에도 재활용될 것이다. 그래서 이름을 아예 apiPutArticle 으로 하였다!

 

function apiPutArticle(requestBody) {
    return axios.put(`articles/${requestBody?.id}`, requestBody);
}

function* asyncUpdateArticleViews(action) {
    try {
        const response = yield call(apiPutArticle, {
            ...action.payload,
            views: parseInt(action.payload?.views ?? 0) + 1,
            updateDate: Date.now()
        });
        if (response?.status === 200) {
            yield put(articleActions.updateArticleViewsSuccess(response));
        } else {
            yield put(articleActions.updateArticleViewsFail(response));
        }
    } catch(e) {
        console.error(e);
        yield put(articleActions.updateArticleViewsFail(e?.response));
    }
}

 

조회수의 경우 다른 정보는 모두 같고 조회수만  +1이므로 { ...action.payload(게시글정보) } spread 배열을 이용하여 views 와 updateDate 만 덮어씌워줬다.

 

 

이제 /views/Article.js 를 보자.

 

// Article.js

import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Comment from './Comments';

function Article() {
    const params = useParams();
    const dispatch = useDispatch();
    useEffect(() => {
        dispatch(articleActions.getArticle(params?.articleId ?? 0));
    }, [dispatch, params?.articleId]);
    return (
        <>
            <div>
                게시글 상세
            </div>
            <div>
                <Comment />
            </div>
        </>
    );
}

export default Article;

 

useParam으로 articleId를 가져왔고 그걸 getArticle의  action.payload로 태워 dispatch 하였다.

 

 

 

 

 

redux devTools 을 켜서 잘 작동됐나 봐보자.

 

 

 

이제 Article.js 에 값을 뿌려줄 차례다.

 

Comment 에서 댓글리스트를 조회하려면 articleId 값이 있어야 조회가 가능하므로 props로 articleId를 넘겨주는 부분을 추가해주자.

 

그리고 현재 어떤 게시판 소속인지 알려주기 위해서  게시판 명도 자리도 만들겠다.

 

// Article.js

import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory, useParams } from 'react-router-dom';
import Comment from './Comments';
import { articleActions } from '../slices/articleSlice';

function Article() {
    const params = useParams();
    const { article, status, statusText } = useSelector((state) => state.articleReducer);
    const boardList = useSelector((state) => state.boardReducer.boardList);
    const dispatch = useDispatch();
    const history = useHistory();

    useEffect(() => {
        dispatch(articleActions.getArticle(params?.articleId ?? 0));
    }, [dispatch, params?.articleId]);
    return (
        <>
            {
                status === 200 ?
                    <>
                        <div>
                            <span>게시판: </span>
                            <span>
                            {
                                boardList.length > 0 &&
                                boardList.find((board) => board.id === parseInt(article?.boardId))?.name
                            }
                            </span>
                        </div>
                        <div>
                            <div><span>제목: </span><span>{article?.title ?? ""}</span></div>
                            <div><span>조회수: </span><span>{article?.views ?? ""}</span></div>
                            <div><span>작성일시: </span><span>{(article.insertDate) ? new Date(article?.insertDate).toLocaleString() : ""}</span></div>
                            <div><span>내용: </span><span>{article?.content ?? ""}</span></div>
                        </div>
                        <div>
                            <Comment articleId={params?.articleId ?? 0} />
                        </div>
                    </>
                :
                    <div>
                        <div>
                            <span>{status}</span>
                        </div>
                        <div>
                            <span>{statusText}</span>
                        </div>
                    </div>
            }
        </>
    );
}

export default Article;

 

게시판 명은 추가로 조회 안 하고 있는 state에서 꺼내왔다.

 

선호하지 않는 방식이지만.. 게시판 명 하나니까

 

{
    boardList.length > 0 &&
    boardList.find((board) => board.id === parseInt(article?.boardId))?.name
}

 

그리고 중요한 건 url 파라미터 특성답게 param 은 String으로 온다.

그래서 쓰려면 형변환해줘야 한다.

 

 

 

json-server 만 아니었으면 서브쿼리로 게시판명을 같이 조회해오거나 게시글 entity에 join 되어있는 게시판 정보에서 그냥 게시판명 꺼내 썼을 것이다.

 

 

 

<div>
	<span>작성일시: </span><span>{(article.insertDate) ? new Date(article?.insertDate).toLocaleString() : ""}</span>
</div>

 

작성일시의 경우 new Date(불러온 값).toString() 을 해주는 이유는 리액트에서 Date 형태는 바로 렌더링되지 않기 때문이다.

 

 

 

게시글을 클릭해보고 내용이 잘 나오는지 확인해보자.

 

 

 

새로고침할 때마다 조회수가 증가하는 것을 알 수 있다.

 

 

json-server에도 잘 나오는지 터미널을 종종 확인해줘야 한다.

api 경로까지 확인할 수 있다.

 

 

 

 

 

 

원래 조회수 update 방법으로 patch를 사용하려 했었다.

patch 도 REST API 중 하나인데 딱 한 곳만 콕 집어 수정할 때 사용한다.

 

나도 최근에 안 개념이다.

 

axios-http.com/docs/api_intro/

 

Axios API | Axios Docs

Axios API The Axios API Reference Requests can be made by passing the relevant config to axios. axios(config) axios({ method: 'post', url: '/user/12345', data: { firstName: 'Fred', lastName: 'Flintstone' } }); axios({ method: 'get', url: 'http://bit.ly/2mT

axios-http.com

 

velog.io/@insutance/REST-API-HTTP-Method-PUT-vs-PATCH

 

[REST API] HTTP Method PUT vs PATCH

restful API 를 공부하던 와중 update 부분을 만들 때 , PUT 을 사용해야 한다는 걸 알게 되었다.하지만 구글링을 통해 또 알게 된 내용은 update 를 PATCH 를 통해서 하는 방법도 있다는 걸 알게 되었다.이

velog.io

 

 

설명을 더 찾아보니 일부 웹서버나 브라우저는 지원을 안 한다고 한다. 그리고 최근에 생긴 개념이라 하는데. 오히려 권장이 안 되는 건지..

 

 

redux-advanced.vlpt.us/3/01.html

 

3-1. json-server 이해하기 · GitBook

3-1 json-server 사용하기 json server 는 아주 짧은 시간에 REST API 를 구축해주는 라이브러리입니다. 하지만, REST API 서버의 기본적인 기능을 대부분 갖추고 있는데요, 프로덕션 전용은 아닙니다. 프로

redux-advanced.vlpt.us

 

json-server 는 patch룰 지원하는 것 같다.

 

 

 

블로그 포스팅하기 전에 소스를 먼저 작성해서 테스트한 뒤 잘 되면 그 때 작성하는 편이라 patch도 사실 시도해봤는데

 

 

 

 

server 와의 연결은 되는데! views가 업데이트가 안된다!!!!!!!

 

 

 

그래서 포스팅은 put 을 쓴 방법이 올라갔다.

 

생각해보니 views 업데이트될 때 views 말고도 updateDate 도 업데이트 해줘야 한다. 결론은 put 쓰길 잘했다로!^^

 

json-server에서 put으로 업데이트하려면 모든 컬럼, 컬럼 value들을 가지고 있어야 한다.

 

 

 

 

 

만약 존재하지 않는 게시글 아이디를 url에 입력한다면

 

이렇게 뜬다.

 

 

 

 

 

목차돌아가기:

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

 

[React][CRUD] create-board-tutorial-v2

* 인용시 출처 부탁드립니다. 완성 소스 code: github.com/cruellaDev/react-create-board-v2 cruellaDev/react-create-board-v2 updated version of react-create-board. Contribute to cruellaDev/react-create-..

binaryjourney.tistory.com

 

반응형