벨로퍼트 리액트 - 7. 리덕스 미들웨어
7-9. CORS와 Webpack DevServer Proxy
브라우저에서 API를 요청할때 → 브라우저의 현재 주소 = API의 주소 도메인 이어야만 데이터에 접근 가능.
다른 도메인에서 API를 요청해서 사용할수있게 해줘야할때 ⇒ CORS 설정이 필요함
json-server의 경우 모든 도메인을 허용해주는 CORS 규칙이 적용되어있으나 Open API를 만드는게 아니라면 모든 도메인을 허용하면 안됨! 해야 할 경우에는 특정 도메인만 허용해야함.
실제 서비스를 개발할때 - 서버 API 요청할때 기본적으로는 차단되기때문에 서버쪽에 도메인을 허용하도록 구현해야하나 웹팩 개발서버에서 제공하는 Proxy가 있으면 상관없음!
1) proxy 설정하기
웹팩 개발서버의 프록시를 사용
: 브라우저 API 요청시 백엔드 서버에 직접적으로 요청하지 않고 현재 개발서버 주소로 요청하게 됨.
웹팩 개발 서버에서 해당 요청을 받아 백엔드 서버로 전달하고 백엔드에서 응답한 내용을 다시 브라우저쪽으로 반환함. 웹팩 개발서버의 proxy 설정은 웹팩 설정을 통해서 적용하나 CRA를 통해 만든 리액트 프로젝트에서는 package.json "proxy" 값을 설정해서 적용할 수 있다.
package.json 에서 proxy 설정
(...),
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"proxy": "<http://localhost:4000>"
}
api/posts.js ← url 수정
import axios from 'axios';
export const getPosts = async () => {
const response = await axios.get('/posts');
// 요청하는 주소의 도메인이 생략된 경우 현재 페이지의 도메인(지금은 <http://localhost:3000>)을 가리킨다
return response.data;
};
export const getPostById = async id => {
const response = await axios.get(`/posts/${id}`);
return response.data;
};
⇒ 개발 서버를 열면 Network 탭에서 localhost:3000으로 요청한 것을 확인할 수 있다!
리액트로 만든 서비스와 API가 동일한 도메인에서 제공되는 경우 위와 같이 진행할 수 있다.
하지만 API의 도메인과 서비스의 도메인이 다르다면 axios의 글로벌 baseURL을 설정할 수 있다.
ex) 예시 코드
axios.defaults.baseURL = process.env.NODE_ENV === 'development' ? '/' : '<https://api.velog.io/>';
위와 같이 설정하게 된다면 개발환경에서는 프록시 서버쪽으로 요청하고 프로덕션에선 실제 API 서버로 요청을 하게 된다.
7-10. redux-saga
1) 소개
redux-saga : 액션을 모니터링하고있다가 특정 액션이 발생하면 이에 따라 특정 작업을 하는 방식으로 사용하는 라이브러리. (* 특정작업 : 특정 자바스크립트 실행, 타 액션 디스패치, 현재 상태 불러오기 등)
redux-saga는 redux-thunk가 못하는 작업들을 처리할 수 있다!
- 비동기 작업시 기존 요청 취소 처리 가능
- 특정 액션 발생시 이에 따라 다른 액션이 디스패치되게끔 하거나, 자바스크립트 코드를 실행할 수 있다.
- 웹소켓 사용하는 경우 Channel이라는 기능을 사용해 코드를 효율적으로 관리할 수 있다.
- API 요청 실패시 재요청 작업 가능
redux-saga는 제공되는 기능도 많고 진입장벽도 높음.
자바스크립트 Generator 문법을 사용함. (이 문법을 이해하지 못하면 redux-saga를 배우는것이 매우 어렵다)
2) Generator 문법 배우기
Generator의 핵심 기능
: 함수를 작성할때 함수를 특정구간에 멈춰놓을 수도 있고, 원할때 다시 돌아가게 할 수도 있고, 결과값을 여러번 반환할 수도 있다.
예시 함수
function weirdFunction() {
return 1;
return 2;
return 3;
return 4;
return 5;
}
⇒ 이 함수는 호출할때마다 무조건 1을 반환하게 될것이다.
하지만, 제너레이터 함수를 사용한다면 함수에서 값을 순차적으로 반환할 수 있다. 또 함수의 흐름을 도중에 멈춰놓았다가 나중에 이어서 진행할 수도 있다.
function* generatorFunction() {
console.log('안녕하세요?');
yield 1;
console.log('제너레이터 함수');
yield 2;
console.log('function*');
yield 3;
return 4;
}
function* : 제너레이터 함수를 만들때 사용하는 키워드
제너레이터 함수 호출시에는 한 객체가 반환되는데, 이것을 제너레이터라고 한다.
제너레이터 생성 코드
const generator =generatorFunction();
제너레이터 함수를 호출한다고 해서 해당 함수 안의 코드가 바로 시작되지는 않는다.
generator.next() 를 호출해야만 코드가 실행되고, yield를 한 값을 반환하고 코드의 흐름을 멈춘다. 이후 generator.next() 를 다시 호출하면 흐름이 이어서 다시 시작된다.
제너레이터 함수 예시2) next 호출시 인자를 전달하여 제너레이터 함수 내부에서 사용하기
function* sumGenerator() {
console.log('sumGenerator이 시작됐습니다.');
let a = yield;
console.log('a값을 받았습니다.');
let b = yield;
console.log('b값을 받았습니다.');
yield a + b;
}
3) Generator로 액션 모니터링하기
Generator로 액션을 모니터링하고 특정 액션이 발생했을때 사용자가 원하는 자바스크립트 코드를 실행시킬 수 있다.
예시코드)
function* watchGenerator() {
console.log('모니터링 시작!');
while(true) {
const action = yield;
if (action.type === 'HELLO') {
console.log('안녕하세요?');
}
if (action.type === 'BYE') {
console.log('안녕히가세요.');
}
}
}
4) 리덕스 사가 설치 및 비동기 카운터 만들기
예제) thunk를 사용해서 구현했던 비동기 카운터 기능을 redux-saga로 구현하기
라이브러리 설치
$ npm add redux-saga
modules/counter.js 수정.
- increaseAsync, decreaseAsync thunk 함수 지우고 일반 액션 생성 함수로 대체
- increaseSaga, decreaseSaga 생성. (Saga : redux-saga에서 부르는 제너레이터 함수)
import { delay, put } from 'redux-saga/effects' ;
// 액션 타입
const INCREASE = 'INCREASE';
const DECREASE = 'DECREASE';
const INCREASE_ASYNC = 'INCREASE_ASYNC';
const DECREASE_ASYNC = 'DECREASE_ASYNC';
// 액션 생성 함수
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
export const increaseAsync = () => ({ type: INCREASE_ASYNC });
export const decreaseAsync = () => ({ type: DECREASE_ASYNC });
function* increaseSaga() {
yield delay(1000); // 1초 기다림
yield put(increase()); // put: 특정 액션 디스패치
};
function* decreaseSaga() {
yield delay(1000); // 1초 기다림
yield put(decrease()); // put: 특정 액션 디스패치
};
// 초깃값 (상태가 객체가 아니라 그냥 숫자여도 상관 없습니다.)
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;
}
}
put : 새로운 액션 디스패치
액션 모니터링 함수
- takeEvery : 특정 액션 타입에 대하여 디스패치되는 모든 액션을 처리
- takeLatest : 특정 액션 타입에 대하여 디스패치된 갖아 마지막 액션만을 처리. 특정 액션을 처리하는 동안 동일한 타입의 새로운 액션이 디스패치될 경우 기존작업을 무시하고 새로운 작업을 시작함.
counterSaga 함수로 액션 모니터링하기(modules/counter.js)
import { delay, put, takeEvery, takeLatest } from 'redux-saga/effects' ;
// 액션 타입
const INCREASE = 'INCREASE';
const DECREASE = 'DECREASE';
const INCREASE_ASYNC = 'INCREASE_ASYNC';
const DECREASE_ASYNC = 'DECREASE_ASYNC';
// 액션 생성 함수
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
export const increaseAsync = () => ({ type: INCREASE_ASYNC });
export const decreaseAsync = () => ({ type: DECREASE_ASYNC });
function* increaseSaga() {
yield delay(1000); // 1초 기다림
yield put(increase()); // put: 특정 액션 디스패치
};
function* decreaseSaga() {
yield delay(1000); // 1초 기다림
yield put(decrease()); // put: 특정 액션 디스패치
};
// 다른 곳에서 불러와서 사용해야해서 export 사용
export function* counterSaga() {
yield takeEvery(INCREASE_ASYNC, increaseSaga); // 모든 INCREASE_ASYNC 액션을 처리
yield takeLatest(DECREASE_ASYNC, decreaseSaga); // 가장 마지막으로 디스패치된 DECREASE_ASYNC 액션만 처리
};
// 초깃값 (상태가 객체가 아니라 그냥 숫자여도 상관 없습니다.)
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;
}
}
루트 사가 만들기 (프로젝트에서 여러개의 사가를 만들게 되기 때문)
modules/index.js
import { combineReducers } from 'redux';
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()]); // all 은 배열 안의 여러 사가를 동시에 실행시킨다
}
export default rootReducer;
리덕스 스토어에 redux-saga 미들웨어 적용
index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { applyMiddleware, createStore } 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 { BrowserRouter } from 'react-router-dom';
import createSagaMiddleware from 'redux-saga';
const root = ReactDOM.createRoot(document.getElementById('root'));
const sagaMiddleware = createSagaMiddleware(); // 사가 미들웨어 만들기
const store = createStore(
rootReducer,
// logger를 사용하는 경우 logger가 맨 마지막에 위치해야함
composeWithDevTools(
applyMiddleware(
ReduxThunk,
sagaMiddleware, // 사가 미들웨어 적용
logger
)
)
// 여러개의 미들웨어 적용 가능
);
sagaMiddleware.run(rootSaga); // 루트 사가 실행
// !주의! 스토어 생성이 된 다음에 루트 사가를 실행해야한다
root.render(
<React.StrictMode>
<BrowserRouter>
<Provider store={store}>
<App />
</Provider>
</BrowserRouter>
</React.StrictMode>
);
reportWebVitals();
App.js에서 CounterContainer 렌더링
import CounterContainer from "./containers/CounterContainer";
// import PostListContainer from "./container/PostListContainer";
import PostListPage from "./pages/PostListPage";
import PostPage from "./pages/PostPage";
import { Routes, Route } from 'react-router-dom';
function App() {
return (
<>
<CounterContainer />
{/* <PostListContainer /> */}
<Routes>
<Route path="/" element={<PostListPage />} >
<Route path=":id" element={<PostPage />} />
</Route>
</Routes>
</>
);
}
export default App;
+를 5회 눌렀을때 = 5
⇒ 모든 액션이 처리됨
-를 5회 눌렀을때 =4
⇒ 마지막으로 디스패치된 액션만 처리됨