API 서버를 연동할 때 API 요청에 대한 상태를 잘 관리해야 함
(ex) 요청이 시작되었을 때는 로딩중을, 요청이 성공하거나 실패했을 때는 로딩이 끝났음을 명시해야 한다.
요청이 성공하면 서버에서 받아 온 응답에 대한 상태를 관리하고,
요청이 실패하면 서버에서 반환한 에러에 대한 상태를 관리해야 한다.
→ 리액트 프로젝트에서 리덕스를 사용하고 있으며 이러한 비동기 작업을 관리해야 한다면 '미들웨어'를 사용해 매우 효율적이고 편하게 상태 관리를 할 수 있음
미들웨어
- 리덕스 미들웨어는 액션을 디스패치했을 때 리듀서에서 이를 처리하기에 앞서 사전에 지정된 작업들을 실행한다
- 미들웨어는 액션과 리듀서 사이의 중간자라고 볼 수 있음
- 리듀서가 액션을 처리하기 전에 미들웨어가 할 수 있는 작업
- 전달받은 액션을 단순히 콘솔에 기록
- 전달받은 액션 정보를 기반으로 액션을 아예 취소하거나, 다른 종류의 액션을 추가로 디스패치할 수 있음
미들웨어 만들기
// src/lib/loggerMiddleware.js
const loggerMiddleware = store => next => action => {
// 미들웨어 기본 구조
};
export default loggerMiddleware;
화살표 함수를 연달아서 사용했는데, 일반 function 키워드로 풀어서 쓴다면 다음과 같다
const loggerMiddleware = function loggerMiddleware(store) {
return function(next) {
return function(action) {
// 미들웨어 기본 구조
};
};
};
- 미들웨어는 결국 함수를 반환하는 함수를 반환하는 함수
- 함수에서 파라미터로 받아오는 값
- store : 리덕스 스토어 인스턴스
- action : 디스패치된 액션
- next : 파라미터는 함수 형태이며, store.dispatch와 비슷한 역할을 한다. 차이점은 next(action)을 호출하면 그 다음 처리해야 할 미들웨어에게 액션을 넘겨주고, 만약 그 다음 미들웨어가 없다면 리듀서에게 액션을 넘겨준다
- 미들웨어 내부에서 store.dispatch를 사용하면 첫 번째 미들웨어부터 다시 처리함
- 만약 미들웨어에서 next를 사용하지 않으면 액션이 리듀서에 전달되지 않음(액션이 무시됨)
미들웨어 구현
이전 상태, 액션 정보, 새로워진 상태를 콘솔에 보여줌
// src/lib/loggerMiddleware.js
const loggerMiddleware = store => next => action => {
console.group(action && action.type); // 액션 타입으로 log를 그룹화함
console.log('이전 상태', store.getState());
console.log('액션', action);
next(action); // 다음 미들웨어 혹은 리듀서에게 전달
console.log('다음 상태', store.getState()); // 업데이트된 상태
console.groupEnd(); // 그룹 끝
};
export default loggerMiddleware;
- 만든 리덕스 미들웨어를 스토어에 적용
- 미들웨어는 스토어를 생성하는 과정에서 적용
// src/index.js
...
import { createStore, applyMiddleware } from 'redux';
...
import loggerMiddleware from './lib/loggerMiddleware';
const store = createStore(rootReducer, applyMiddleware(loggerMiddleware));
root.render(
<Provider store = {store}>
<App />
</Provider>
);
redux-logger 사용하기
// src/index.js
...
// import loggerMiddleware from './lib/loggerMiddleware';
import { createLogger } from 'redux-logger';
const logger = createLogger();
const store = createStore(rootReducer, applyMiddleware(logger));
const store = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<Provider store = {store}>
<App />
</Provider>
);
비동기 작업을 처리하는 미들웨어 사용
redux-thunk
- 비동기 작업을 처리할 때 가장 기본적으로 많이 사용하는 미들웨어
- 객체가 아닌 함수형태의 액션을 디스패치할 수 있게 해줌
3-1-1) Thunk
특정 작업을 나중에 할 수 있도록 미루기 위해 함수 형태로 감싼 것을 의미함
// 주어진 파라미터에 1을 더하는 함수
// 이 연산 작업을 나중으로 미루고 싶은 경우
const addOne = x => x+1;
function addOneThunk(x) {
const thunk = () => addOne(x);
return thunk;
}
const fn = addOneThunk(1);
setTimeout(() => {
const value = fn(); // fn이 실행되는 시점에 연산
console.log(value);
}, 1000);
3-1-2) 미들웨어 적용하기
// src/index.js
...
import {createLogger} from 'redux-logger';
import thunk from 'redux-thunk';
const logger = createLogger();
const store = createStore(rootReducer, applyMiddleware(logger, thunk));
...
3-1-3) Thunk 생성 함수 만들기
redux-thunk는 액션 생성 함수에서 일반 액션 객체를 반환하는 대신 함수를 반환함
// src/modules/counter.js
import { createAction, handleActions } from 'redux-actions';
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);
// 1초 뒤에 increase 혹은 decrease 함수를 디스패치함
export const increaseAsync = () => dispatch => {
setTimeout(() => {
dispatch(increase());
}, 1000);
};
export const decreaseAsync = () => dispatch => {
setTimeout(() => {
dispatch(decrease());
}, 1000);
};
...
// src/container/CounterContainer.js
...
import { increaseAsync, decreaseAsync } from '../modules/counter';
import Counter from '../components/Counter';
const CounterContainer = ({ number, increaseAsync, decreaseAsync }) => {
return (
<Counter number={number} onIncrease={increaseAsync} onDecrease={decreaseAsync} />
);
};
export default connect(
state => ({
number: state.counter
}),
{
increaseAsync,
decreaseAsync
}
)(CounterContainer);
- 버튼을 누르면 숫자가 1초 뒤 변경됨
- 처음 디스패치 되는 액션은 함수 형태이고, 두번째 액션은 객체 형태임
3-1-4) 웹 요청 비동기 작업 처리하기
유지보수를 위해 API를 모두 함수화한다
// src/lib/api.js
import axios from 'axios';
export const getPost = id =>
axios.get(`https://jsonplaceholder.typicode.com/posts/${id}`);
export const getUsers = id =>
axios.get('https://jsonplaceholder.typicode.com/users');
새로운 리듀서를 만들어줌
// src/modules/sample.js
import { handleActions } from 'redux-actions';
import * as api from '../lib/api';
// 액션 타입을 선언
// 한 요청당 세 개를 만든다.
const GET_POST = 'sample/GET_POST';
const GET_POST_SUCCESS = 'sample/GET_POST_SUCCESS';
const GET_POST_FAILURE = 'sample/GET_POST_FAILURE';
const GET_USERS = 'sample/GET_USERS';
const GET_USERS_SUCCESS = 'sample/GET_USERS_SUCCESS';
const GET_USERS_FAILURE = 'sample/GET_USERS_FAILURE';
// thunk 함수를 생성
// thunk 함수 내부에서는 시작할 때, 성공했을 때, 실패했을 때 다른 액션을 디스패치한다.
export const getPost = id => async dispatch => {
dispatch({ type: GET_POST }); // 요청을 시작한 것을 알림
try {
const response = await api.getPost(id);
dispatch({
type: GET_POST_SUCCESS,
payload: response.data
}); // 요청 성공
} catch (e) {
dispatch({
type: GET_POST_FAILURE,
payload: e,
error: true
}); // 에러 발생
throw e; // 나중에 컴포넌트단에서 에러를 조회할 수 있게 해줌
}
};
export const getUsers = () => async dispatch => {
dispatch({ type: GET_USERS }); // 요청을 시작한 것을 알림
try {
const response = await api.getUsers();
dispatch({
type: GET_USERS_SUCCESS,
payload: response.data
}); // 요청 성공
} catch (e) {
dispatch({
type: GET_USERS_FAILURE,
payload: e,
error: true
}); // 에러 발생
throw e;
}
};
// 초기 상태를 선언
// 요청의 로딩 중 상태는 loading이라는 객체에서 관리
const initialState = {
loading: {
GET_POST: false,
GET_USERS: false
},
post: null,
users: null
}
const sample = handleActions(
{
[GET_POST]: state => ({
...state,
loading: {
...state.loading,
GET_POST: true // 요청 시작
}
}),
[GET_POST_SUCCESS]: (state, action) => ({
...state,
loading: {
...state.loading,
GET_POST: false // 요청 완료
},
post: action.payload
}),
[GET_POST_FAILURE]: (state, action) => ({
...state,
loading: {
...state.loading,
GET_POST: false // 요청 완료
}
}),
[GET_USERS]: state => ({
...state,
loading: {
...state.loading,
GET_USERS: true // 요청 시작
}
}),
[GET_USERS_SUCCESS]: (state, action) => ({
...state,
loading: {
...state.loading,
GET_USERS: false // 요청 완료
},
users: action.payload
}),
[GET_USERS_FAILURE]: (state, action) => ({
...state,
loading: {
...state.loading,
GET_USERS: false // 요청 완료
}
}),
},
initialState
);
export default sample;
// src/modules/index.js
import { combineReducers } from 'redux';
import counter from './counter';
import sample from './sample';
const rootReducer = combineReducers({
counter,
sample
});
export default rootReducer;
// src/components/Sample.js
import React from 'react';
const Sample = ({ loadingPost, loadingUsers, post, users }) => {
return (
<div>
<section>
<h1>포스트</h1>
{loadingPost && '로딩 중...'}
{!loadingPost && post && (
<div>
<h3>{post.title}</h3>
<h3>{post.body}</h3>
</div>
)}
</section>
<hr />
<section>
<h1>사용자 목록</h1>
{loadingUsers && '로딩 중...'}
{!loadingUsers && users && (
<div>
{users.map(user => (
<li key={user.id}>
{user.username} ({user.email})
</li>
))}
</div>
)}
</section>
</div>
);
};
export default Sample;
- 데이터를 불러와 렌더링해 줄 때는 유효성 검사를 해주는 것이 중요함
- post && 를 사용하면 post 객체가 유효할 때만 그 내부의 post.title 혹은 post.body 값을 보여줌
- 만약 데이터가 없는 상태라면 post.title을 조회하려고 할 때 오류가 발생하므로 반드시 유효성 검사 필요
// src/containers/SampleConatiner.js
import React from 'react';
import { connect } from 'react-redux';
import Sample from '../components/Sample';
import { getPost, getUsers } from '../modules/sample';
const { useEffect } = React;
const SampleContainer = ({
getPost,
getUsers,
post,
users,
loadingPost,
loadingUsers
}) => {
// 클래스 형태 컴포넌트였다면 componentDidMount
useEffect(() => {
getPost(1);
getUsers(1);
}, [getPost, getUsers]);
return (
<Sample
post={post}
users={users}
loadingPost={loadingPost}
loadingUser={loadingUsers}
/>
);
};
export default connect(
({ sample }) => ({
post: sample.post,
users: sample.users,
loadingPost: sample.loading.GET_POST,
loadingUsers: sample.loading.GET_USERS
}),
{
getPost,
getUsers
}
)(SampleContainer);
// src/App.js
import React from 'react';
import SampleContainer from './containers/SampleContainer';
const App = () => {
return (
<div>
<SampleContainer />
</div>
);
};
export default App;
3-1-5) 리팩토링
(1) API 요청을 해주는 thunk 함수
// src/lib/createRequestThunk.js
export default function createRequestThunk(type, request) {
// 성공 및 실패 액션 타입을 정의한다.
const SUCCESS = `${type}_SUCCESS`;
const FAILURE = `${type}_FAILURE`;
return params => async dispatch => {
dispatch({ type }); // 시작됨
try {
const response = await request(params);
dispatch({
type: SUCCESS,
payload: response.data
}); // 성공
} catch(e) {
dispatch({
type: FAILURE,
payload: e,
error: true
}); // 에러 발생
throw e;
}
};
}
// 사용법: createRequestThunk('GET_USERS', api.getUsers);
// src/modules/sample.js
...
import createRequestThunk from '../lib/createRequestThunk';
...
// thunk 함수를 생성
// thunk 함수 내부에서는 시작할 때, 성공했을 때, 실패했을 때 다른 액션을 디스패치한다.
export const getPost = createRequestThunk(GET_POST, api.getPost);
export const getUsers = createRequestThunk(GET_USERS, api.getUsers);
...
(2) 요청의 로딩 상태를 관리하는 작업
// src/modules/loading.js
import { createAction, handleActions } from 'redux-actions';
const START_LOADING = 'loading/START_LOADING';
const FINISH_LOADING = 'loading/FINISH_LOADING';
/*
요청을 위한 액션 타입을 payload로 설정한다.(ex. "sample/GETPOST")
*/
export const startLoading = createAction(
START_LOADING,
requestType => requestType
);
export const finishLoading = createAction(
FINISH_LOADING,
requestType => requestType
);
const initialState = {};
const loading = handleActions(
{
[START_LOADING]: (state, action) => ({
...state,
[action.payload]: true
}),
[FINISH_LOADING]: (state, action) => ({
...state,
[action.payload]: false
})
},
initialState
);
export default loading;
// src/modules/index.js
...
import loading from './loading';
const rootReducer = combineReducers({
counter,
sample,
loading
});
export default rootReducer;
3-2) redux-saga
- redux-thunk 다음으로 가장 많이 사용되는 비동기 작업 관련 미들웨어 라이브러리
- 액션을 모니터링하고 있다가, 특정 액션이 발생하면 이에 따라 특정 작업을 하는 방식
- 특정 작업 : 자바스크립트를 실행하는 것, 다른 액션을 디스패치 하는 것, 현재 상태를 불러오는 것
- 대부분의 경우 redux-thunk로 구현할 수 있지만 redux-sage를 사용하는 것이 유리한 상황
- 기존 요청을 취소 처리해야할 때 (불필요한 중복 요청 방지)
- 특정 액션이 발생했을 때 다른 액션을 발생시키거나, API 요청 등 리덕스와 관계없는 코드를 실행할 때
- 웹소켓을 사용할 때
- API 요청 실패시 재요청
- Generator 문법을 사용
3-2-1) Generator 함수
'React' 카테고리의 다른 글
[React] 서버 사이드 렌더링 (0) | 2023.05.02 |
---|---|
[React] 코드 스플리팅 (0) | 2023.05.01 |
[React] 리덕스를 사용해 상태 관리 (0) | 2023.04.06 |
[React] 리덕스 라이브러리 (0) | 2023.04.05 |
[React] Context API (0) | 2023.04.05 |