React/게시판만들기 v2.

[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

 

[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

 

 

들어가기 전에 파일 구조를 맞추고 들어가겠다.

 

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 옆 *은 오타가 아니다. 제너너레이터 함수다.

 

ko.javascript.info/generators

 

제너레이터

 

ko.javascript.info

 

그리고 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 가 담기게 된다.

 

 

 

그리고 혹시 ?. 와 ?? 를 처음 본다면 아래 포스팅을 참고하길 바란다.

 

binaryjourney.tistory.com/52

 

[Javascript] Optional Chaining ( ?.)

오늘 신기한 걸 알게 돼서 포스팅을 해본다. 동료분도 지금 리액트 독학 중이신데 ?. << 이걸 처음 써봤다고 알려주셨다. 현재 우리가 계속 써왔던 방식은 const keyName = (object.hasOwnProperty("keyName") ? o

binaryjourney.tistory.com

 

 

 

이젠 받아온 값을 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

 

[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

 

반응형