목차돌아가기:
binaryjourney.tistory.com/pages/ReactCRUD-create-board-tutorial-v2
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/
velog.io/@insutance/REST-API-HTTP-Method-PUT-vs-PATCH
설명을 더 찾아보니 일부 웹서버나 브라우저는 지원을 안 한다고 한다. 그리고 최근에 생긴 개념이라 하는데. 오히려 권장이 안 되는 건지..
redux-advanced.vlpt.us/3/01.html
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