목차 돌아가기: binaryjourney.tistory.com/pages/ReactCRUD-create-board-tutorial
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 를 하면
이렇게 나올 것이다.
그리고 board.json을 비운 채로 json-server를 실행해보자
서버에 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로 보내는 것이다.
조회수 기능이 완료되었으니 새 글을 등록해보자
조회수에 1이 들어가있는 것이 보이는가?
화면에만 증가값이 보이는 것이 아니라 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) 해보자
처음에는 로딩 표시인 "목록을 불러오는 중입니다" 가 뜨고
조금 지나면
에러가 발생할 때 뜨는 내용이 나타난다.
그럼 다시 json 서버를 켜보고 화면을 새로고침해보자
"목록을 불러오는 중입니다."가 찰나로 보이고 바로 목록 리스트가 조회된다.
다음은 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
블로그에서는
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