JSON Server
가짜 Rest api를 호출하여 연습용 서버를 만들 수 있다
먼저 root프로젝트에 data.json을 만든다 (src폴더보다도 밖)
//data.json
{
"posts" : [
{
"id" : 1,
"title" : "정민규의 나이",
"body" : "25살"
},
{
"id" : 2,
"title" : "정민규의 성별",
"body" : "남자"
},
{
"id" : 3,
"title" : "정민규의 최애메뉴",
"body" : "치킨"
}
]
}
npx json-server ./data/json --port 4000
그 다음 터미널을 열어서 서버를 개통시켜준다.
터미널에 나온 해당 링크를 클릭해주고
주소를 localhost:4000/posts 로 이동해주면
입력한 데이터가 잘 출력되어 나온다.
https://www.postman.com/downloads/?utm_source=postman-home
위 프로그램을 사용하면 보다 쉽게 json파일에 접근하고, 정보를 수정시키고 할 수 있다.
이제 전 글에서 작성했던 프로젝트를 이렇게 만든 api를 사용하여 접근해보자.
yarn add axios
예전에 배웠던 axios라이브러리를 사용할 것이기에 먼저 설치해준다.
//api/post.js
import axios from 'axios';
export const getPosts = async () => {
const response = await axios.get('http://localhost:4000/posts');
return response.data;
}
export const getPostById = async (id) => {
const response = await axios.get(`http://localhost:4000/posts/${id}`);
return response.data;
}
기존 가짜api코드를 정상적으로 바꾸고 실행시키면
실행도 잘되고 네트워크 요청도 잘 되는것을 확인할 수 있다.
CORs and Webpack DevServer Proxy
현재 프로젝트에서 사용하는 주소는 http://localhost:3000/
api는 http://localhost:4000/posts이다.
포트가 다르기에 원래는 api를 사용할수 없어서 따로 설정해줘야하는데 json에는 미리 그것이 되어있어 별 문제없이 사용할 수 있다.
하지만 따로 데이터파일을 만들거나 협업을 하거나 할때는 서버에 따로 설정을 해줘야하는데, 프록시 설정을 해두면 이런 번거로움을 피할 수 있다.
프록시 설정을 해보자.
//package.json 의 맨 끝
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"proxy" : "http://localhost:4000"
}
다음처럼 "proxy" 를 입력하고 로컬 주소를 입력해준다. 받아오는 api주소를 입력하면 되겠다.
그 이후, 서버를 껏다 켜주면 된다.
redux-saga
redux thunk 다음으로 가장 많이 사용되는 미들웨어이다.
액션을 모니터링하고 있다가 액션이 발생하면 어떠한 작업 ( 함수실행,액션실행,상태조회 등)을 실행하는 것이다.
비동기 작업을 진행할 때 기존요청을 취소할수도 있다. thunk처럼 함수를 타입의 값을 디스패치할 필요가 없다. 비동기 작업이 실패했을때 재시도 하는 기능을 추가하는등 다양한 기능을 쓸 수 있다.
Generator라는 문법을 사용하기에 이에 대해 간단히 알아보자.
Generator
함수의 흐름을 특정 구간에 멈춰놓았다가 다시실행할 수 있다.
결과값을 여러번 내보낼 수 있다.
function* generatorFunction() {
console.log('안녕하세요?');
yield 1; //함수의 흐름을 멈추고 1을 반환
console.log('제너레이터 함수');
yield 2;
console.log('function*');
yield 3;
return 4;
}
위함수는 아래와 같이 동작한다.
즉, 함수를 한번 실행할때마다 멈추게 되며, yield에 써진 값이 반환된다.
이번엔 아래 예시를 한번 보자
function* sumGenerator() {
console.log('sumGenerator이 시작됐습니다.');
let a = yield;
console.log('a값을 받았습니다.');
let b = yield;
console.log('b값을 받았습니다.');
yield a + b;
}
역시 비슷한 맥락으로 작동된다.
끝나지 않는 Generator도 만들 수 있다.
function* inifinitedAddGenerator() {
let result = 0;
while(true) {
result += yield result;
}
}
이처럼 적으면 무한이 작동하는 Generator가 된다.
이처럼 redux-saga는 Generator에 기반되어 있다. 어렵긴 하지만 유틸함수들이 이미 잘 만들어있으니 사용하는 법을 한번 알아보자.
yarn add redux-saga
리덕스 사가 라이브러리를 설치하고 시작해보자.
그다음 이전에 만들었던 thunk를 이용해 만들었던 아래 counter예제를 redux-saga로 만들어 보자.
//counter.js
export const increaseAsync = () => (dispatch) => {
setTimeout(() => {
dispatch(increase());
},1000)
}
export const decreaseAsync = () => (dispatch) => {
setTimeout(() => {
dispatch(decrease());
},1000)
}
아래와 같이 Generator문법에 따라서 기본적인 Saga를 작성해주었다.
//counter.js
import { delay,put, takeEvery, takeLatest } from 'redux-saga/effects' //effects : reduxsaga가 수행하도록 작업을 명령하는 것
const INCREASE = 'INCREASE'; //action type
const DECREASE = 'DECREASE';
const INCREASE_ASYNC = 'INCREASE_ASYNC' //redux-saga
const DECREASE_ASYNC = "DECREASE_ASYNC"
export const increase = () => ({ type : INCREASE}); //action생성함수
export const decrease = () => ({ type : DECREASE});
export const increaseAsync = () => ({ type : INCREASE_ASYNC });
export const decreaseAsync = () => ({ type : DECREASE_ASYNC });
function* increaseSaga() {
yield delay(1000);
yield put(increase()); //dispatch와 유사
}
function* decreaseSaga() {
yield delay(1000);
yield put(decrease());
}
export function* counterSaga() { //내보내줘야함. 결국 rootsaga가 있어야함.
yield takeEvery(INCREASE_ASYNC, increaseSaga); //INCREASE_ASYNC가 디스패치될때마다 increaseSaga실행
yield takeLatest(DECREASE_ASYNC,decreaseSaga); //기존건 무시하고 가장 마지막것만 처리
}
const initialState = 0; //초기상태
export default function counter (state = initialState,action) { //리듀서
switch (action.type) {
case INCREASE:
return state +1;
case DECREASE:
return state - 1;
default:
return state;
}
}
counterSaga는 내보내줘야하 기때문에 export해주었다.
//modules/index.js
import { combineReducers } from "redux"; //root리듀서를 만들기 위해
import counter, { counterSaga } from "./counter";
import posts from './posts';
import { all } from 'redux-saga/effects'
const rootReducer = combineReducers({counter,posts});
export function* rootSaga() {
yield all([counterSaga()]);
}
export default rootReducer;
그 이후 rootReducer를 만들었던 것처럼 rootSaga도 만들어주어야 한다.
//index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { createStore , applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import rootReducer, { rootSaga } from './modules';
import logger from 'redux-logger';
import {composeWithDevTools} from 'redux-devtools-extension'; //개발자도구 적용
import ReduxThunk from 'redux-thunk';
import { Router } from 'react-router-dom';
import {createBrowserHistory} from 'history';
import createSagaMiddleware from 'redux-saga';
const customHistory = createBrowserHistory();
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
rootReducer,
composeWithDevTools(
applyMiddleware(
sagaMiddleware,
ReduxThunk.withExtraArgument({history : customHistory}),
logger
)));//미들웨어 적용
sagaMiddleware.run(rootSaga) //rootSaga를 호출할 필요는
ReactDOM.render(
<Router history = {customHistory}>
<Provider store={store}>
<App />
</Provider>
</Router>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
이전에 만들었던 index.js에 createSagaMiddleware를 불러오고 스토어에 sagamiddleware를 추가시켜주고
sagaMiddleware.run 함수를 rootSaga를 파라미터로 해서 적어준다.
그다음 App.js에서 다음처럼 Counter예제를 불러오면
function App() {
return (
<>
<CounterContainer/>
<Route path = "/" component={PostListPage} exact />
<Route path="/:id" component={PostPage} />
</>
);
}
잘 작동이 되는것을 알 수 있다.
이때 +버튼은 3번연속 누르면 +3이 결국 되지만, -는 3번연속누르면 1만 감소하는것을 볼 수 있는데, 이는 takeEvery와 takeLatest의 차이라고 볼 수 있다.
redux-saga로 Promise 다루기
이전에 reduxthunk같은경우는
export const getPosts = () => async dispatch => {
dispatch({ type: GET_POSTS }); // 요청이 시작됨
try {
const posts = postsAPI.getPosts(); // API 호출
dispatch({ type: GET_POSTS_SUCCESS, posts }); // 성공
} catch (e) {
dispatch({ type: GET_POSTS_ERROR, error: e }); // 실패
}
};
다음과 같이 작동했다. 즉, 함수를 만들어서 해당함수로 비동기작업을 처리한 후에, 필요 시점에 특정액션이 디스패치 되는 방식으로 작동했다. saga는 이와는 약간 다르게 작동한다.
export const getPost = id => ({ type: GET_POST, payload: id, meta: id });
function* getPostsSaga() {
try {
const posts = yield call(postsAPI.getPosts); // call 을 사용하면 특정 함수를 호출하고, 결과물이 반환 될 때까지 기다려줄 수 있습니다.
yield put({
type: GET_POSTS_SUCCESS,
payload: posts
}); // 성공 액션 디스패치
} catch (e) {
yield put({
type: GET_POSTS_ERROR,
error: true,
payload: e
}); // 실패 액션 디스패치
}
}
저번에 만들었던 Post예제를 바꾸어보겠다. Counter예제를 바꾼것과 상당히 유사하다.
//.modules/posts.js
import * as postsAPI from '../api/post';//postsAPI.getpost등으로 사용가능하게
import { createPromiseThunk, createPromiseThunkById, handleAsyncActions, handleAsyncActionsById, reducerUtils } from '../lib/asyncUtils';
import {call ,put, takeEvery} from 'redux-saga/effects';
/*액션*/
const GET_POSTS = 'GET_POSTS';
const GET_POSTS_SUCCESS = 'GET_POSTS_SUCCESS';
const GET_POSTS_ERROR = 'GET_POSTS_ERROR';
const GET_POST = 'GET_POST';
const GET_POST_SUCCESS = 'GET_POST_SUCCESS';
const GET_POST_ERROR = 'GET_POST_ERROR';
const CLEAR_POST = 'CLEAR_POST'; //이전 포스트가 잠깐 보이는 문제를 해결하기 위해
/* saga */
export const getPosts = () => ({ type : GET_POSTS });
export const getPost = (id) => ({
type : GET_POST,
payload : id,
meta : id,
});
function* getPostsSaga() {
try {
const posts = yield call(postsAPI.getPosts);
yield put({
type : GET_POSTS_SUCCESS,
payload : posts
})
} catch (e) {
yield put({
type : GET_POSTS_ERROR,
payload : e,
error : true
})
}
}
function* getPostSaga(action) {
const id = action.payload;
try{
const post = yield call(postsAPI.getPostById,id);
yield put({
type: GET_POST_SUCCESS,
payload : post,
meta : id,
})
} catch(e) {
yield put({
type: GET_POST_ERROR,
payload : e,
error : true
})
}
}
export function* postsSaga() {
yield takeEvery(GET_POSTS, getPostsSaga);
yield takeEvery(GET_POST,getPostSaga);
}
export const goToHome = () => (dispatch,getState,{history}) => { // 3번째 파라미터안에 history가있어 비구조화 할당으로 가져옴
history.push('/'); //withExtraargument에서 extra가 들어오는데 그안에 history가 있다
}
export const clearPost = () => ({ type : CLEAR_POST});
const initialState = {
posts:reducerUtils.initial(),
post: {}
}
const getPostsReducer = handleAsyncActions(GET_POSTS,'posts',true);
const getPostReducer = handleAsyncActionsById(GET_POST,'post',true);
export default function posts(state = initialState,action) {
switch(action.type){
case GET_POSTS:
case GET_POSTS_SUCCESS:
case GET_POSTS_ERROR:
return getPostsReducer(state,action)
case GET_POST:
case GET_POST_SUCCESS:
case GET_POST_ERROR:
return getPostReducer(state,action);
case CLEAR_POST:
return{
...state,
post : reducerUtils.initial(),
}
default:
return state;
}
}
이후 index.js에서
//modules/index.js
import { combineReducers } from "redux"; //root리듀서를 만들기 위해
import counter, { counterSaga } from "./counter";
import posts, { postsSaga } from './posts';
import { all } from 'redux-saga/effects'
const rootReducer = combineReducers({counter,posts});
export function* rootSaga() {
yield all([counterSaga(), postsSaga]);
}
export default rootReducer;
만들어둔 root Saga를 추가해주기만 하면 된다.
이 코드역시 getpostSaga와 getpostsSaga가 굉장히 유사한점을 알 수 있는데, 코드 리팩토링을 한번 진행해 보도록 하자
이전에 thunk함수 리팩토링한 과정과 상당히 유사하다.
//asyncUtils.js
import {call,put} from 'redux-saga/effects';
export const createPromiseSaga = (type, promiseCreator) => {
const [SUCCESS, ERROR ] = [`${type}_SUCCESS`, `${type}_ERROR`];
return function* saga(action) {
try{
const result = yield call(promiseCreator, action.payload);
yield put({
type : SUCCESS,
payload : result
});
} catch (e) {
yield put({
type : ERROR,
payload : e,
error : true
});
}
}
}
export const createPromiseSagaById = (type,promiseCreator) => {
const [SUCCESS, ERROR ] = [`${type}_SUCCESS`, `${type}_ERROR`];
return function* saga(action) {
const id = action.meta;
try{
const result = yield call(promiseCreator, action.payload);
yield put({
type : SUCCESS,
payload : result,
meta : id,
});
} catch (e) {
yield put({
type : ERROR,
payload : e,
error : true,
meta : id
});
}
}
}
후에, post.js에서 saga를 선언했던 부분을 단 두줄로 줄일 수 있다!!
/* saga */
export const getPosts = () => ({ type : GET_POSTS });
export const getPost = (id) => ({
type : GET_POST,
payload : id,
meta : id,
});
const getPostsSaga = createPromiseSaga(GET_POSTS,postsAPI.getPosts);
const getPostSaga = createPromiseSagaById(GET_POST,postsAPI.getPostById);
export function* postsSaga() {
yield takeEvery(GET_POSTS, getPostsSaga);
yield takeEvery(GET_POST,getPostSaga);
}
기능역시 원래와 똑같이 잘 작동하는것을 알 수 있다.
이전 글에서 thunk와 라우터를 history를 이용해서 연동했었는데 saga또한 비슷하게 할 수 있다.
먼저 index.js에서 context에 history를 다음과 같이 추가해준다.
//index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { createStore , applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import rootReducer, { rootSaga } from './modules';
import logger from 'redux-logger';
import {composeWithDevTools} from 'redux-devtools-extension'; //개발자도구 적용
import ReduxThunk from 'redux-thunk';
import { Router } from 'react-router-dom';
import {createBrowserHistory} from 'history';
import createSagaMiddleware from 'redux-saga';
const customHistory = createBrowserHistory();
const sagaMiddleware = createSagaMiddleware({
context : {
history : customHistory // saga에서 조회가 가능
}
});
const store = createStore(
rootReducer,
composeWithDevTools(
applyMiddleware(
sagaMiddleware,
ReduxThunk.withExtraArgument({history : customHistory}),
logger
)));//미들웨어 적용
sagaMiddleware.run(rootSaga) //rootSaga를 호출할 필요는
ReactDOM.render(
<Router history = {customHistory}>
<Provider store={store}>
<App />
</Provider>
</Router>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
context안에 history를 customhistory로 추가해주었다.
그후 effects안의 getContext 를 import해준 후에, 이를 사용해서 history를 불러온 후에 사용하면 된다.
//.modules/posts.js
import * as postsAPI from '../api/post';//postsAPI.getpost등으로 사용가능하게
import { createPromiseSaga, createPromiseSagaById, handleAsyncActions, handleAsyncActionsById, reducerUtils } from '../lib/asyncUtils';
import { takeEvery, getContext} from 'redux-saga/effects';
/*액션*/
const GET_POSTS = 'GET_POSTS';
const GET_POSTS_SUCCESS = 'GET_POSTS_SUCCESS';
const GET_POSTS_ERROR = 'GET_POSTS_ERROR';
const GET_POST = 'GET_POST';
const GET_POST_SUCCESS = 'GET_POST_SUCCESS';
const GET_POST_ERROR = 'GET_POST_ERROR';
const GO_TO_HOME = 'GO_TO_HOME'
const CLEAR_POST = 'CLEAR_POST'; //이전 포스트가 잠깐 보이는 문제를 해결하기 위해
/* saga */
export const getPosts = () => ({ type : GET_POSTS });
export const getPost = (id) => ({
type : GET_POST,
payload : id,
meta : id,
});
const getPostsSaga = createPromiseSaga(GET_POSTS,postsAPI.getPosts);
const getPostSaga = createPromiseSagaById(GET_POST,postsAPI.getPostById);
function* goToHomeSaga() {
const history = yield getContext('history'); //history 사용가능
history.push('/');
}
export function* postsSaga() {
yield takeEvery(GET_POSTS, getPostsSaga);
yield takeEvery(GET_POST,getPostSaga);
yield takeEvery(GO_TO_HOME,goToHomeSaga)
}
export const goToHome = () => ({ type : GO_TO_HOME}) //thunk와는 달리 순수 객체로
export const clearPost = () => ({ type : CLEAR_POST});
const initialState = {
posts:reducerUtils.initial(),
post: {}
}
const getPostsReducer = handleAsyncActions(GET_POSTS,'posts',true);
const getPostReducer = handleAsyncActionsById(GET_POST,'post',true);
export default function posts(state = initialState,action) {
switch(action.type){
case GET_POSTS:
case GET_POSTS_SUCCESS:
case GET_POSTS_ERROR:
return getPostsReducer(state,action)
case GET_POST:
case GET_POST_SUCCESS:
case GET_POST_ERROR:
return getPostReducer(state,action);
case CLEAR_POST:
return{
...state,
post : reducerUtils.initial(),
}
default:
return state;
}
}
Saga에서 현재 상태를 조회할 수도 있다.
//.modules/posts.js
import * as postsAPI from '../api/post';//postsAPI.getpost등으로 사용가능하게
import { createPromiseSaga, createPromiseSagaById, handleAsyncActions, handleAsyncActionsById, reducerUtils } from '../lib/asyncUtils';
import { takeEvery, getContext, select} from 'redux-saga/effects';
/*액션*/
const GET_POSTS = 'GET_POSTS';
const GET_POSTS_SUCCESS = 'GET_POSTS_SUCCESS';
const GET_POSTS_ERROR = 'GET_POSTS_ERROR';
const GET_POST = 'GET_POST';
const GET_POST_SUCCESS = 'GET_POST_SUCCESS';
const GET_POST_ERROR = 'GET_POST_ERROR';
const GO_TO_HOME = 'GO_TO_HOME'
const CLEAR_POST = 'CLEAR_POST'; //이전 포스트가 잠깐 보이는 문제를 해결하기 위해
const PRINT_STATE = 'PRINT_STATE' //saga로 상태조회
/* saga */
export const getPosts = () => ({ type : GET_POSTS });
export const getPost = (id) => ({
type : GET_POST,
payload : id,
meta : id,
});
export const printState = () => ({ type : PRINT_STATE });
const getPostsSaga = createPromiseSaga(GET_POSTS,postsAPI.getPosts);
const getPostSaga = createPromiseSagaById(GET_POST,postsAPI.getPostById);
function* goToHomeSaga() {
const history = yield getContext('history'); //history 사용가능
history.push('/');
}
function* printStateSaga() {
const state = yield select(state => state.posts);
console.log(state);
}
export function* postsSaga() {
yield takeEvery(GET_POSTS, getPostsSaga);
yield takeEvery(GET_POST,getPostSaga);
yield takeEvery(GO_TO_HOME,goToHomeSaga);
yield takeEvery(PRINT_STATE,printStateSaga)
}
export const goToHome = () => ({ type : GO_TO_HOME}) //thunk와는 달리 순수 객체로
export const clearPost = () => ({ type : CLEAR_POST});
const initialState = {
posts:reducerUtils.initial(),
post: {}
}
const getPostsReducer = handleAsyncActions(GET_POSTS,'posts',true);
const getPostReducer = handleAsyncActionsById(GET_POST,'post',true);
export default function posts(state = initialState,action) {
switch(action.type){
case GET_POSTS:
case GET_POSTS_SUCCESS:
case GET_POSTS_ERROR:
return getPostsReducer(state,action)
case GET_POST:
case GET_POST_SUCCESS:
case GET_POST_ERROR:
return getPostReducer(state,action);
case CLEAR_POST:
return{
...state,
post : reducerUtils.initial(),
}
default:
return state;
}
}
'FrontEnd > React' 카테고리의 다른 글
26_타입스크립트 & 리덕스 (0) | 2022.01.07 |
---|---|
25_타입스크립트 & 리액트 (0) | 2022.01.07 |
22_리덕스 미들웨어 (0) | 2022.01.05 |
21_리액트_리덕스 (0) | 2022.01.02 |
20_리액트_라우터 (0) | 2021.12.30 |