일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- Algorithm
- SW
- sw expert academy
- Get
- react-redux
- Python
- 항해플러스
- 테코테코
- axios
- redux-toolkit
- 코딩테스트합격자되기
- 리액트
- maeil-mail
- useDispatch
- 프로그래머스
- C++
- react-router
- 알고리즘
- redux
- 매일메일
- createSlice
- redux-saga
- JavaScript
- react
- 자바
- java
- 이코테
- programmers
- 항해99
- json-server
- Today
- Total
Binary Journey
[React][CRUD] 게시판 만들기 All in One (3). 게시판 조회해오기, redux, react-redux, redux-saga, axios, get, REST API 본문
[React][CRUD] 게시판 만들기 All in One (3). 게시판 조회해오기, redux, react-redux, redux-saga, axios, get, REST API
binaryJournalist 2021. 4. 14. 14:15
목차돌아가기:
binaryjourney.tistory.com/pages/ReactCRUD-create-board-tutorial-v2
들어가기 전에 파일 구조를 맞추고 들어가겠다.
Board.js
// Board.js
import React from 'react';
import { Link } from "react-router-dom";
function Board() {
return (
<div>
<ul>
<li>
<Link to="/">
<span>Main</span>
</Link>
</li>
<li>
<Link to="/board/1">
<span>board1</span>
</Link>
</li>
<li>
<Link to="/board/2">
<span>board2</span>
</Link>
</li>
</ul>
</div>
);
}
export default Board;
일단 sidebar 역할을 하는 Board 안의 board를 조회해오겠다.
순서는 boardSlice 만들기 -> boardSlice의 boardReducer 를 rootReducer 에 집어넣기
-> boardSaga 만들기 -> rootSaga에 boardSaga 넣기 -> Board에서 액션 호출하기
이렇게 할 것이다.
파일 생성이 끝난 후에 state를 보는 법도 설명하겠다.
우선 /src/slices 에 boardSlice 와 articleSlice를 만든다.
// boardSlice.js
import { createSlice } from "@reduxjs/toolkit";
const name = "board";
const initialState = {};
const reducers = {};
const boardSlice = createSlice({
name,
initialState,
reducers
});
export const boardReducer = boardSlice.reducer;
export const boardActions = boardSlice.actions;
// articleSlice.js
import { createSlice } from "@reduxjs/toolkit";
const name = "article";
const initialState = {};
const reducers = {};
const articleSlice = createSlice({
name,
initialState,
reducers
});
export const articleReducer = articleSlice.reducer;
export const articleActions = articleSlice.actions;
createSlice 안에서 name, initialState, reducers 를 직접 설정해줘도 좋다.
밖에서 객체로 만들어 넣어줘도 동작은 잘 한다.
그리고 rootReducers 에 만든 boardReducer를 import 하여 연결해준다.
rootReducer.js
import { combineReducers } from 'redux';
import { articleReducer } from './slices/articleSlice';
import { boardReducer } from './slices/boardSlice';
const rootReducer = combineReducers({ articleReducer, boardReducer });
export default rootReducer;
여기서부터 새 reducer가 추가된 state 형태를 볼 수 있는데 우선 크롬에 리덕스 개발자도구 (redux dev tools) extension이 추가되어 있어야 한다.
(현재 포스팅한 모든 글은 크롬 웹브라우저에서 화면을 확인한다.)
크롬에서 redux extension 만 구글링해줘도 나오는데
redux dev tools 를 클릭하여 들어간다.
파란 버튼을 누르고 설치가 완료되면 파란버튼이 "chrome에서 삭제"로 바뀔 것이다.
그리고 새 창을 띄워 localhost:3000 을 입력해서 다시 들어간 뒤에
f12 를 눌러본다 (혹은 마우스 오른쪽 버튼 클릭 + 검사)
위 항목 중 "Redux" 로 들어간다.
그럼 오른쪽에 action, state, diff, trace, test 가 있을 것인데
우리가 자주 사용할 것은 action, state 부분이다
action은 action호출했을 시 type, payload를 확인할 것이고
state 는 액션이 호출됐을 시점의 state를 보여준다.
일단 state를 눌러보면
방금 만든 articleReducer와 boardReducer 가 들어가 있을 것이다.
이 이유는 store 에 reducer로서 rootReducer를 올려놨기 때문이다.
boardSlice 안 initialState 안에 내용을 추가해보고 redux dev tool 에 어떻게 나오는지 확인해보자!
import { createSlice } from "@reduxjs/toolkit";
const name = "board";
const initialState = {
boardList: [], // 추가
};
const reducers = {};
const boardSlice = createSlice({
name,
initialState,
reducers
});
export const boardReducer = boardSlice.reducer;
export const boardActions = boardSlice.actions;
initiaState 는 object 형태로 되어있어서 안에 내가 받고 싶은 자료의 이름을 key로, value에는 자료형까지도 미리 선언해놓을 수 있다.
redux dev tools 의 state를 확인해보면 (안 보일 시 새로고침 필요)
boardList: []가 들어가있는 걸 볼 수 있다.
이제 마저 action을 만들자!
// boardSlice.js
import { createSlice } from "@reduxjs/toolkit";
const name = "board";
const initialState = {
boardList: [],
};
const reducers = {
getBoardList: (state, action) => {}, // view에서 호출
getBoardListSuccess: (state, action) => {}, // saga에서 api 연결 성공시 return 값 적용
getBoardListFail: (state, action) => {}, // api 연결 실패시 return 값 적용
};
const boardSlice = createSlice({
name,
initialState,
reducers
});
export const boardReducer = boardSlice.reducer;
export const boardActions = boardSlice.actions;
saga로 가야 하지만 api연결이 먼저 필요하다.
/utils 에 axios.js 를 만들자.
// /utils/axios.js
import Axios from "axios";
const axiosInstance = Axios.create({
baseURL: "http://localhost:4000/",
timeout: 3000,
});
export default axiosInstance;
간단한 json-server를 이용하는 것이기 때문에 config 없이 이렇게만 만든다.
axios 모듈 호출을 한 곳에서만 관리하기 위해 axios.js 파일을 따로 만든 것이다.
/src/sagas 에 boardSaga.js 를 생성한다
// /sagas/boardSaga.js
import { all, call, fork, put, take } from 'redux-saga/effects';
import { boardActions } from '../slices/boardSlice';
import axios from '../utils/axios';
// api 서버 연결 주소
function apiGetBoard(boardId) {
return axios.get(`boards/${boardId}`);
}
function apiGetBoardList() {
return axios.get(`boards`);
}
// api 서버 연결 후 action 호출
function* asyncGetBoardList() {
try {
const response = yield call(apiGetBoardList);
console.log(response);
} catch(e) {
console.error(e);
yield put(boardActions.getBoardListFail(e.response));
}
}
// action 호출을 감시하는 watch 함수
function* watchGetBoardList() {
while(true) {
yield take(boardActions.getBoardList);
yield call(asyncGetBoardList);
}
}
export default function* boardSaga()
{
yield all([fork(watchGetBoardList)]);
}
function 옆 *은 오타가 아니다. 제너너레이터 함수다.
그리고 yield는 꼭 써줘야 한다.
json-server 의 response가 어떻게 오는지 확인하기 위해 console.log(response)로 값을 확인하는 것을 추천한다.
이제 rootSaga에 생성한 boardSaga를 연결한다.
// rootSaga.js
import { map } from 'ramda';
import { all, fork } from "redux-saga/effects"
import boardSaga from "./sagas/boardSaga";
let combineSagas = {};
combineSagas = Object.assign(combineSagas, { boardSaga }); // 수정부분
export default function* rootSaga() {
yield all(map(fork, combineSagas));
}
이 포스팅 작성하기 전까지 saga 동작이 안됐었는데 boardSaga를 객체 형태로 넣어주지 않아서 그랬다.
도움을 주신 christopher 님께 감사를
이제 정말로 Board에서 action을 호출할 것이다.
먼저 필요한 훅 import
import React, { useEffect } from 'react';
import { useDispatch } from 'react-redux';
Board 컴포넌트 안에
const dispatch = useDispatch();
액션을 호출할 useDispatch() 훅을 dispatch 안에 담아준다.
그리고 화면을 로드하자마자 dispatch 함수를 호출해줄 useEffect 구문을 작성한다.
useEffect(() => {
dispatch(boardActions.getBoardList());
}, [dispatch])
총 코드는 이러하다.
// Board.js
import React, { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { Link } from 'react-router-dom';
import { boardActions } from '../slices/boardSlice';
function Board() {
const dispatch = useDispatch();
useEffect(() => {
dispatch(boardActions.getBoardList());
}, [dispatch])
return (
<div>
<ul >
<li >
<Link to="/">
<span>Main</span>
</Link>
</li>
<li >
<Link to="/board/1">
<span>board1</span>
</Link>
</li>
<li >
<Link to="/board/2">
<span>board2</span>
</Link>
</li>
</ul>
</div>
);
}
export default Board;
response 의 status 가 200 이면 성공이라는 뜻이다. 그리고 data 안에 우리가 원하는 값이 들어있다.
response.data 값을 쉽게 보기 위해 console.table(response.data) 를 써보겠다.
아름답다 아름다와
response.status를 통해서 우리가 원할 때 원하는 메서드를 이용하게 할 수 있다.
나는 다음과 같이 만들었다.
// boardSaga.js
import { all, call, fork, put, take } from 'redux-saga/effects';
import { boardActions } from '../slices/boardSlice';
import axios from '../utils/axios';
// api 서버 연결 주소
function apiGetBoard(boardId) {
return axios.get(`boards/${boardId}`);
}
function apiGetBoardList() {
return axios.get(`boards`);
}
// api 서버 연결 후 action 호출
function* asyncGetBoardList() {
try {
const response = yield call(apiGetBoardList);
if (response?.status === 200) {
yield put(boardActions.getBoardListSuccess(response));
} else {
yield put(boardActions.getBoardListFail(response));
}
} catch(e) {
console.error(e);
yield put(boardActions.getBoardListFail(e.response));
}
}
// action 호출을 감시하는 watch 함수
function* watchGetBoardList() {
while(true) {
yield take(boardActions.getBoardList);
yield call(asyncGetBoardList);
}
}
export default function* boardSaga()
{
yield all([fork(watchGetBoardList)]);
}
보통 개발 시 의도한 결과대로만 동작을 하기 때문에 연결 실패 시 어떻게 동작할지도 생각해줘야 한다. 그래서 나는 연결 실패의 경우도 추가해줄 것이다.
사용하는 apiGetBoardList안 url 주소를 boards 에서 board로 바꾼다.
function apiGetBoardList() {
return axios.get(`board`);
}
그럼 status 404 가 뜰 것이다.
연결이 실패할 경우 액션 getBoardListFail 을 타게 해놨으므로 boardSlice 내 getBoardListFail 에서 값을 가공하면 된다.
나는 response 의 값에 따라 이렇게 작성하였다.
//boardSlice.js
//...
const initialState = {
boardList: [],
status: 0,
statusText: "Loading"
};
const reducers = {
getBoardList: (state, action) => {},
getBoardListSuccess: (state, action) => {
state.boardList = action.payload?.data ?? [];
state.status = action.payload?.status;
state.statusText = action.payload?.statusText ?? "Success";
},
getBoardListFail: (state, action) => {
state.boardList = initialState.boardList
state.status = action.payload?.status ?? 500;
state.statusText = action.payload?.statusText ?? "Network Error";
},
};
redux devTools 에서 state 값을 확인해 보면
잘 들어가 있다.
흔히 잘 보던 404 Not Found!
이제 에러가 날 경우 Board에 어떻게 나오는지 만들어줘야 한다
boardReducer의 state 구독할 useSelector를 만들면 된다.
// Board.js
import { useDispatch, useSelector } from 'react-redux';
const { boardList, status, statusText } = useSelector((state) => state.boardReducer);
그리고 JSX return 부분을 수정한다.
// Board.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import { boardActions } from '../slices/boardSlice';
function Board() {
const { boardList, status, statusText } = useSelector((state) => state.boardReducer);
const dispatch = useDispatch();
useEffect(() => {
dispatch(boardActions.getBoardList());
}, [dispatch]);
return (
<>
{
status === 200 ?
<div>
<ul>
<li>
<Link to="/">
<span>Main</span>
</Link>
</li>
<li>
<Link to="/board/1">
<span>board1</span>
</Link>
</li>
<li>
<Link to="/board/2">
<span>board2</span>
</Link>
</li>
</ul>
</div>
:
<div>
<div>
<span>{status}</span>
</div>
<div>
<span>{statusText}</span>
</div>
</div>
}
</>
);
}
export default Board;
에러가 발생했을 때는 이런 화면이 뜬다
boardSaga 에서 return axios.get(`board`);로 잠깐 바꿔놓았던 것을 다시 return axios.get(`boards`);로 고쳐놓으면 sidebar 가 잘 보인다.
// boardSlice.js
getBoardListFail: (state, action) => {
state.boardList = initialState.boardList
state.status = action.payload?.status ?? 500;
state.statusText = action.payload?.statusText ?? "Network Error";
},
여기서 ?? null 병합 연산자인데
x = a ?? b
a 가 undefined 혹은 null 일 경우에는 b를 x 에 담으라는 코드이다.
따라서 boardSlice는 json-server 가 꺼져 아무런 response를 보내지 않으면 undefined로 올 것인데 그 경우 status 는 500으로 statusText는 Network Error 가 담기게 된다.
그리고 혹시 ?. 와 ?? 를 처음 본다면 아래 포스팅을 참고하길 바란다.
이젠 받아온 값을 map 함수를 이용하여 뿌릴 것이다
map 함수가 문제없이 잘 돌아가려면 값이 무조건 배열 형태로 잘 왔는지 확인해야 한다.
// Board.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import { boardActions } from '../slices/boardSlice';
function Board() {
const { boardList, status, statusText } = useSelector((state) => state.boardReducer);
const dispatch = useDispatch();
useEffect(() => {
dispatch(boardActions.getBoardList());
}, [dispatch]);
return (
<>
{
status === 200 ?
<div>
<ul >
<li key={0}>
<Link to="/">
<span>Main</span>
</Link>
</li>
{
boardList.length > 0 ?
boardList.map((board) => (
<li key={board?.id}>
<Link to={{ pathname: `/board/${board?.id}` }}>
<span>{board?.name}</span>
</Link>
</li>
))
: <div> 게시판이 없습니다. </div>
}
</ul>
</div>
:
<div>
<div>
<span>{status}</span>
</div>
<div>
<span>{statusText}</span>
</div>
</div>
}
</>
);
}
export default Board;
총 수정된 코드는 다음과 같다.
목차돌아가기:
binaryjourney.tistory.com/pages/ReactCRUD-create-board-tutorial-v2