목차
- 비동기 Action
- 리덕스 미들웨어
- redux-devtools
- redux-thunk
- redux-promise-middleware
- Ducks 패턴
- redux-saga
1. 비동기 Action
비동기 API를 호출할 때, 호출을 시작하고 응답을 받았을 때, 앱에서 두 순간 모두 상태 변화를 요구한다면, 리듀서에 각각 다른 액션을 보내야 할 것입니다.
예를 들어, 아래와 같이 말이죠.
export const GET_USERS_START = "GET_USERS_START";
export const GET_USERS_SUCCESS = "GET_USERS_SUCCESS";
export const GET_USERS_FAILED = "GET_USERS_FAILED";
// 비동기 API 호출이 시작됨을 알리는 액션
export function getUsersStart() {
return {
type: GET_USERS_START,
}
}
// 성공적인 응답을 받았음을 알리는 액션
export function getUsersSuccess(data) {
return {
type: GET_USERS_SUCCESS,
data
}
}
// API 호출이 실패했음을 알리는 액션
export function getUsersFailed(error) {
return {
type: GET_USERS_FAILED,
error
}
}
리듀서에서는 이 액션을 처리할 함수를 생성해 줍니다.
import { GET_USERS_START, GET_USERS_SUCCESS, GET_USERS_FAILED } from '../actions/users';
const initialState = {
loading: false,
data: [],
error: null
};
export default function users(state = initialState, action) {
switch (action.type) {
case GET_USERS_START:
return {
...state,
loading: true,
error: null
};
case GET_USERS_SUCCESS:
return {
...state,
loading: false,
data: action.data
}
case GET_USERS_FAILED:
return {
...state,
loading: false,
error: action.error
}
default:
return state;
}
}
마지막으로, 컴포넌트에서 비동기로 axios를 사용해 github API를 호출해보면 아래와 같습니다.
먼저, react-redux를 사용하는 환경에서 컨테이너와 뷰를 분리했다고 가정했을 때, 컨테이너 코드는 아래와 같습니다.
import ... from '...';
export default function UserListContainer() {
const users = useSelector((state) => state.users.data);
const dispatch = useDispatch();
const getUsers = useCallback(async function getUsers() {
try{
dispatch(getUsersStart());
const res = await axios.get("https://api.github.com/users");
dispatch(getUsersSuccess(res.data));
} catch(error) {
dispatch(getUsersFailed(error));
}
}, [dispatch]);
return <UserList users={users} getUsers={getUsers} />;
}
- useSelector를 사용해 현재 상태를 불러옵니다.
- dispatch에 의존하는 useCallback을 선언합니다. useCallback을 사용하지 않으면, 컴포넌트가 리랜더링 되면서 하위 컴포넌트인 UserList가 getUsers를 계속 호출해 무한 루프에 빠집니다.
- getUsers 함수에선 비동기 API 호출의 시작, 성공, 실패 상태를 리듀서에 보내도록 세팅합니다.
이제 뷰 부분에서 전달받은 getUser 함수를 호출합니다.
import { useEffect } from 'react';
export default function UserList({ users, getUsers }) {
useEffect(() => {
getUsers();
}, [getUsers])
return (
<ul>
{users.map((user) => {
return <li key={user.id}>{user.login}</li>
})}
</ul>
)
}
- getUsers에 의존하는 useEffect를 사용해서 컴포넌트가 렌더링 되었을 때 함수를 호출하도록 합니다. useEffect를 사용하지 않으면, dispatch에 의해 재실행되는 useCallback 때문에 무한 루프에 빠지게 됩니다.
2. 리덕스 미들웨어
- 리덕스 미들웨어는 Context API, MobX와 차별화가 되는 부분입니다.
- 미들웨어를 사용하면, 액션이 리듀서로 가기 전에 추가적인 작업을 할 수 있습니다.
- 예) 조건에 따른 액션 무시, 액션 로깅 등
- https://react.vlpt.us/redux-middleware/
import { applyMiddleware, createStore } from 'redux';
import todoApp from './reducers/reducers';
function middleware1(store) {
console.log('middleware1', 0);
return (next) => { // next: 다음 실행될 미들웨어(미들웨어2)
console.log('middleware1', 1);
return action => {
console.log('middleware1', 2);
const returnVal = next(action); // 다음 미들웨어를 실행
console.log('middleware1', 3);
return returnVal;
}
};
}
function middleware2(store) {
console.log('middleware2', 0);
return (next) => { // next: 다음 미들웨어는 없으므로 next는 리듀서가 된다.
console.log('middleware2', 1);
return action => {
console.log('middleware2', 2);
const returnVal = next(action);
console.log('middleware2', 3);
return returnVal;
}
};
}
const store = createStore(todoApp, applyMiddleware(middleware1, middleware2));
export default store;
- applyMiddleware : 스토어에 미들웨어를 적용할 때 사용하는 리덕스 함수입니다.
실행해보면 콘솔의 실행 순서는 아래와 같습니다.
3. redux-devtools
- 리덕스의 미들웨어로써, 스토어 상태 조회나 액션을 추적할 수 있도록 도와주는 개발자 도구입니다.
먼저 redux-devtools를 설치합니다.
$ npm i -D redux-devtools-extension
이제 디버깅을 할 요소에 composeWithDevTools로 감싸줍니다.
import { applyMiddleware, createStore } from 'redux';
import todoApp from './reducers/reducers';
import { composeWithDevTools } from 'redux-devtools-extension';
const store = createStore(todoApp, composeWithDevTools(applyMiddleware()));
export default store;
크롬에 Redux DevTools를 설치합니다.
https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=ko
마지막으로 크롬에서 개발자 도구를 열고, Redux 탭을 확인해 보면 액션을 추적할 수 있습니다.
4. redux-thunk
- 비동기 처리를 위해 가장 많이 사용되는 리덕스 미들웨어입니다.
- 액션에서 객체를 리턴하지 않고, 함수를 리턴해서 액션을 dispatch 할 수 있도록 해줍니다.
- 비동기적 액션의 dispatch를 지연시키거나, 조건에 따른 dispatch를 수행하게 할 수 있습니다.
redux-thunk 설치
$ npm i redux-thunk
applyMiddleware를 사용해서 thunk를 넣어주면, 액션에서 dispatch를 할 수 있습니다.
/* store.js */
...
import thunk from 'redux-thunk';
const store = createStore(todoApp, composeWithDevTools(applyMiddleware(thunk)));
export default store;
다음으로 action 생성자가 객체가 아닌 함수를 반환하도록 합니다.
/* userAction.js */
export function getUsersThunk() {
return async (dispatch) => {
try {
dispatch(getUsersStart());
const res = await axios.get("https://api.github.com/users");
dispatch(getUsersSuccess(res.data));
} catch(error) {
dispatch(getUsersFailed(error));
}
}
}
async await으로 비동기를 처리할 수도 있지만, 액션 생성자에서 이를 처리함으로써 액션 처리에 대한 관심사를 분리할 수 있는 장점이 있습니다. 또한, thunk는 미들웨어이기 때문에, 리듀서로 전달되기 전에 조건에 따라 액션을 무시하도록 설정할 수 있습니다.
5. redux-promise-middleware
- JavaScript의 Promise 기반 비동기 작업(async await)을 편하게 해주는 미들웨어입니다.
- 위의 thunk는 start, success, failed 액션 타입을 모두 정의해서 처리해줬다면, 이 미들웨어는 pending, fulfilled, rejected라는 타입을 자동으로 처리해줍니다.
먼저 미들웨어를 설치합니다.
$ npm i redux-promise-middleware
스토어에 미들웨어를 추가합니다.
import promise from 'redux-promise-middleware';
const store = createStore(todoApp, composeWithDevTools(applyMiddleware(promise)));
export default store;
액션 생성자에서 기존처럼 객체를 리턴하고, payload에 비동기 처리 함수를 넣습니다.
const GET_USERS = 'GET_USERS';
export const GET_USERS_PENDING = 'GET_USERS_PENDING';
export const GET_USERS_FULFILLED = 'GET_USERS_FULFILLED';
export const GET_USERS_REJECTED = 'GET_USERS_REJECTED';
export function getUsersPromise() {
return {
type: GET_USERS,
payload: async () => {
const res = await axios.get("https://api.github.com/users");
return res.data;
}
}
}
컨테이너에서 이 액션을 사용해 dispatch 합니다.
...
import { getUsersPromise } from '../redux/actions/users';
export default function UserListContainer() {
const users = useSelector((state) => state.users.data);
const dispatch = useDispatch();
const getUsers = useCallback(() => {
dispatch(getUsersPromise());
});
return <UserList users={users} getUsers={getUsers} />;
}
리듀서에서 미들웨어가 생성한 액션 타입에 대한 로직을 생성합니다.
export default function users(state = initialState, action) {
switch (action.type) {
...
case GET_USERS_PENDING:
return {
...
};
case GET_USERS_FULFILLED:
return {
...
};
case GET_USERS_REJECTED:
return {
...
};
}
}
}
이렇게 start, success, failed 상태를 모두 정의해서 사용해야 했던 thunk보다 좀 더 편리하게 자동화가 된 코드로 바뀐 걸 확인할 수 있습니다. 하지만 promise 미들웨어는 이미 정의된 액션 타입만을 전달하기 때문에, 애플리케이션의 복잡성이 늘어나고 확장 용이하도록 하려면 thunk가 유리하다는 걸 알 수 있습니다.
6. Ducks 패턴
- 기존에 파일 트리를 액션, 리듀서 등으로 나눴었다면, Ducks 패턴은 기능을 중심으로 파일을 구성하는 패턴입니다.
- 하나의 기능을 중심으로 네임스페이스(const), 액션, 리듀서 등으로 하나의 파일에 모은 것을 모듈이라고 칭합니다.
예를 들어, 기존엔 액션은 액션끼리, 리듀서는 리듀서끼리 모아놨다면 아래와 같을 것인데요,
Ducks 패턴에선 기능을 중심으로 만들기 때문에, modules라는 폴더 아래에 하나의 파일에 모두를 넣어서 처리합니다.
// widgets.js
// Action type
const LOAD = 'widgets/LOAD';
const CREATE = 'widgets/CREATE';
const UPDATE = 'widgets/UPDATE';
const REMOVE = 'widgets/REMOVE';
// Reducer
export default function reducer(state = {}, action = {}) {
switch (action.type) {
// do reducer stuff
default: return state;
}
}
// Action 생성자
export function loadWidgets() {
return { type: LOAD };
}
export function createWidget(widget) {
return { type: CREATE, widget };
}
export function updateWidget(widget) {
return { type: UPDATE, widget };
}
export function removeWidget(widget) {
return { type: REMOVE, widget };
}
https://velog.io/@dolarge/React-Redux-Ducks-%ED%8C%A8%ED%84%B4
이렇게 만듦으로써, 하나를 수정하더라도 전체 수정이 편리해진다는 장점이 있기 때문에, 실무에서 많이 사용되는 패턴입니다.
7. redux-saga
- 비동기 처리를 위해 사용되는 리덕스 미들웨어입니다.
- thunk와의 차이점은 액션이 함수를 반환하지 않고 객체를 반환함으로써, 리덕스의 순수 라이프 사이클을 유지할 수 있으며, 그로인해 테스트가 쉬워집니다.
/* actions.js */
import { put, call, takeEvery } from 'redux-saga/effects';
...
const GET_USERS_SAGA_START = 'GET_USERS_SAGA_START';
export function* getUsersSaga(action) {
try {
yield put(getUsersStart());
const res = yield call(axios.get, "https://api.github.com/users");
yield put(getUsersSuccess(res.data));
} catch(error) {
yield put(getUsersFailed(error));
}
}
export function getUsersSagaStart() {
return {
type: GET_USERS_SAGA_START
}
}
export function* usersSaga() {
yield takeEvery(GET_USERS_SAGA_START, getUsersSaga);
}
- function* : ES6에 추가된 제너레이터 함수로, yield를 iterating 돌면서 코드를 실행시킵니다.
- put : saga에서 액션을 dispatch 할 때 사용하는 함수입니다.
- call : saga에서 함수를 실행할 때 사용하는 함수입니다.
- takeEvery : dispatch 된 액션 타입에 따라 특정 함수를 실행시켜주는 함수입니다.
/* rootSaga.js */
import { all } from '@redux-saga/core/effects';
import { usersSaga } from './users';
export default function* rootSaga() {
yield all([usersSaga()]);
}
- all : 제너레이터 함수를 인자로 넣으면, 함수들이 병렬적으로 실행되고, 전부 결과가 resolve될 때까지 기다립니다.
/* store.js */
import createSagaMiddleware from 'redux-saga';
import rootSaga from './actions/rootSaga';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(todoApp, composeWithDevTools(applyMiddleware(
sagaMiddleware
))
);
sagaMiddleware.run(rootSaga);
export default store;
'JavaScript > Redux' 카테고리의 다른 글
Redux - react-redux (0) | 2022.04.18 |
---|---|
Redux - 사용해보기 (0) | 2022.03.19 |
Redux - Basic (0) | 2022.03.16 |