redux-thunk
redux-thunk는 리덕스를 사용하는 프로젝트에서 비동기 작업을 처리할 때 가장 기본적으로 사용하는 라이브러리이다.
Thunk란?
특정 잡업을 나중에 할 수 있도록 미루기 위해 함수 형태로 감싼 것을 의미한다.
const addOne = x => x + 1;
const addOneThunk = x => () => addOne();
const fn = addOneThunk(1);
setTimeout(() => {
const value = fn(); // fn이 실행되는 시점에 연산
console.log(value);
}, 1000);
redux-thunk 라이브러리를 사용하면 thunk 함수를 만들어서 디스패치할 수 있다.
리덕스 미들웨어가 그 함수를 전달받아 store의 dispatch와 getState를 파라미터로 넣어서 호출해준다.
const sampleThunk = () => ( dispatch, getState ) => {
// 현재 상태를 참조, 새 액션 디스패치가 가능하다
}
redux-thunk를 미들웨어에 적용
우선 redux-thunk 라이브러리를 설치해 보자.
npm add redux-thunk
그리고 스토어를 만들 때 redux-thunk를 적용하자
ReduxThunk를 redux-thunk에서 가져와 미들웨어에 적용시켜주자
index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { configureStore } from '@reduxjs/toolkit';
import { Provider } from 'react-redux';
import App from './App';
import reportWebVitals from './reportWebVitals';
import rootReducer from './modules';
// import loggerMiddleware from './lib/loggerMiddleware';
import { createLogger } from 'redux-logger';
import ReduxThunk from 'redux-thunk';
const root = ReactDOM.createRoot(document.getElementById('root'));
const logger = createLogger();
const store = configureStore({
reducer: rootReducer,
middleware: [logger, ReduxThunk],
});
root.render(
<Provider store={store}>
<App />
</Provider>
);
// 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();
Thunk 생성 함수 만들기
redux-thunk는 액션 생성 함수에서 일반 액션 객체를 반환하는 대신에 함수를 반환한다.
increaseAsync함수와 decreaseAsync 함수를 만들어 카운터 값을 비동기적으로 변경시켜보자
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);
//Thunk생성 함수 만들기
//1초 뒤에 increase 혹은 decrease 함수를 디스패치함
export const increaseAsync = () => dispatch => {
setTimeout(() => {
dispatch(increase());
}, 1000);
};
export const decreaseAsync = () => dispatch => {
setTimeout(() => {
dispatch(decrease());
}, 1000);
};
const initialState = 0; // 상태는 객체일 필요가 없고 숫자로도 동작 가능하다.
const counter = handleActions(
{
[INCREASE]: state => state + 1,
[DECREASE]: state => state - 1,
},
initialState
);
export default counter;
모듈을 수정했으니 컨테이너에서 호출한 액션 생성 함수도 비동기적으로 동작하는 함수로 바꿔주자
container/CounterContainer.js
import { connect } from 'react-redux';
import { increaseAsync, decreaseAsync } from '../modules/counter';
import Counter from '../component/Counter';
const CounterContainer = ({ number, increaseAsync, decreaseAsync }) => {
return (
<Counter number={number} onIncrease={increaseAsync} onDecrease={decreaseAsync} />
);
};
export default connect(
state => ({
number: state.counter
}),
{
increaseAsync,
decreaseAsync
}
)(CounterContainer);
웹 요청 비동기 작업 처리하기
JSONPlaceholder(https://jsonplaceholder.typicode.com)에서 제공되는 가짜 API를 이용해서 연습하자
포스트 읽기( :id 는 1~100사이의 숫자)
GET https://jsonplaceholder.typicode.com/posts/id
모든 사용자 정보 불러오기
GET https://jsonplaceholder.typicode.com/users
API를 호출할 때는 주로 Promise 기반인 웹 클라이언트인 axios를 사용한다.
npm add axios
API를 모두 함수화 하여 가독성을 높히고 유지보수를 쉽게 만들어 보자
lib/api.js
import axios from 'axios';
export const getPost = id =>
axios.get(`https://jsonplaceholder.typicode.com/posts/${id}`);
export const getUsers = () =>
axios.get(`https://jsonplaceholder.typicode.com/users`);
이제 새로운 리듀서를 만들자
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_USER';
const GET_USERS_SUCCESS = 'sample/GET_USER_SUCCESS';
const GET_USERS_FAILURE = 'sample/GET_USER_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;
너무 길다...
이후에 리팩토링을 하겠지만 보기에 좋지 않다.
이제 해당 리듀서를 루트 리듀서에 포함시키자
modules/index.js
// 루트 리듀서
import { combineReducers } from 'redux';
import counter from './counter';
import sample from './sample';
const rootReducer = combineReducers({
counter,
sample
})
export default rootReducer;
그리고 렌더링 되는 컴포넌트를 만들어주자
components/Sample.js
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 && (
<ul>
{users.map(users => (
<li key={users.id}>
{users.username} ({users.email})
</li>
))}
</ul>
)}
</section>
</div>
);
};
export default Sample;
이때 불러온 데이터에 대한 유효성 검사가 중요하다.
만약 데이터가 없다면 자바스크립트 오류가 발생하므로 유효성 검사를 하는 것이 중요하다.
이제 컨테이너 컴포넌트를 만들어보자
containers/SampleContainer.js
import { connect } from 'react-redux';
import Sample from '../component/Sample';
import { getPost, getUsers } from '../modules/sample';
import { useEffect } from '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}
loadingUsers={loadingUsers}
/>
);
};
// 컴포넌트와 리덕스를 연동
export default connect(
({ sample }) => ({
post: sample.post,
users: sample.users,
loadingPost: sample.loadingPost,
loadingUsers: sample.loadingUsers
}),
{
getPost,
getUsers
}
)(SampleContainer);
마무리로 App.js에 렌더링 해보자
App.js
import SampleContainer from './container/SampleContainer';
const App = () => {
return (
<div>
<SampleContainer />
</div>
);
}
export default App;
리팩토링
위에 있는 저 긴 thunk함수를 보자.....
너무 길다고 느껴진다...
그래서!!!
줄여보자
API요청하는 thunk함수를 줄여주자.
modules/createRequestThunk.js
import { finishLoading, startLoading } from '../modules/loading';
export default function createRequestThunk(type, request) {
// 성공 및 실패 액션 타입을 정의한다.
const SUCCESS = `${type}_SUCCESS`;
const FAILURE = `${type}_FAILURE`;
return params => async dispatch => {
dispatch({ type }); // 시작됨
dispatch(startLoading(type));
try {
const response = await request(params);
dispatch({
type: SUCCESS,
payload: response.data
}); // 성공
dispatch(finishLoading(type));
}
catch (e) {
dispatch({
type: FAILURE,
payload: e,
error: true
}); // 에러 발생
dispatch(startLoading(type));
throw e;
}
};
}
// 사용법: creatRequestThunk(`GET_USERS`, api.getUsers);
기존에 사용했던 thunk함수를 새로만든 함수로 대체해 보자
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_USER';
const GET_USERS_SUCCESS = 'sample/GET_USER_SUCCESS';
const GET_USERS_FAILURE = 'sample/GET_USER_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;
똑같은 기능을 더 짧게 구성했다!!!
이제 요청의 로딩 상태를 관리하는 작업을 리팩토링해보자
로딩상태만 관리하는 리덕스 모듈을 따로 생성하여 처리해보자
modules/loading.js
import { createAction, handleActions } from 'redux-actions';
const START_LOADING = `loading/START_LOADING`;
const FINISH_LOADING = `loading/FINISH_LOADING`;
// 요청을 위해 액션 타입을 payload로 설정한다. (ex: `sample/GET_POST`)
export const startLoading = createAction(
START_LOADING,
requestType => requestType
); //createAction의 두번째 파라미터는 payload를 어떻게 정할지를 설정한다
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;
요청이 시작될 때 디스패치할 액션이다.
{
type: 'loading/START_LOADING',
payload: 'sample/GET_POST'
}
위의 액션이 디스패치되면 loading 리듀서가 관리하고 있는 상태에서 sample/GET_POST값을 true로
설정해준다. 만약 기존 상태에 sample/GET_POST 필드가 없다면 새로 값을 설정해 준다.
그리고 요청이 끝난다면 다음 액션을 디스패치해야 한다.
{
type: 'loading/START_FINISH',
payload: 'sample/GET_POST'
}
그러면 true로 설정했던 값을 false로 전환시켜준다.
이제 리듀서를 루트 리듀서에 포함시키자
modules/index.js
// 루트 리듀서
import { combineReducers } from 'redux';
import counter from './counter';
import sample from './sample';
import loading from './loading';
const rootReducer = combineReducers({
counter,
sample,
loading
})
export default rootReducer;
loading 리덕스 모듈에서 만든 액션 생성 함수는 createRequstThunk에서 사용한다.
lib/createRequestThunk.js
import { finishLoading, startLoading } from '../modules/loading';
export default function createRequestThunk(type, request) {
// 성공 및 실패 액션 타입을 정의한다.
const SUCCESS = `${type}_SUCCESS`;
const FAILURE = `${type}_FAILURE`;
return params => async dispatch => {
dispatch({ type }); // 시작됨
dispatch(startLoading(type));
try {
const response = await request(params);
dispatch({
type: SUCCESS,
payload: response.data
}); // 성공
dispatch(finishLoading(type));
}
catch (e) {
dispatch({
type: FAILURE,
payload: e,
error: true
}); // 에러 발생
dispatch(startLoading(type));
throw e;
}
};
}
// 사용법: creatRequestThunk(`GET_USERS`, api.getUsers);
이제 컨테이너 컴포넌트에 적용하자
import { connect } from 'react-redux';
import Sample from '../component/Sample';
import { getPost, getUsers } from '../modules/sample';
import { useEffect } from 'react';
const SampleContainer = ({
getPost,
getUsers,
post,
users,
loadingPost,
loadingUsers
}) => {
// 클래스 형태 컴포넌트였다면 componentDidMount
useEffect(() => {
// useEffect 에 파라미터로 넣는 함수는 async 로 할 수 없기 때문에
// async 함수를 선언하고 호출함
const fn = async () => {
try {
await getPost(1);
await getUsers(1);
} catch (e) {
console.log(e); // 에러 조회
}
};
fn();
}, [getPost, getUsers]);
return (
<Sample
post={post}
users={users}
loadingPost={loadingPost}
loadingUsers={loadingUsers}
/>
);
};
// 컴포넌트와 리덕스를 연동
export default connect(
({ sample, loading }) => ({
post: sample.post,
users: sample.users,
loadingPost: loading[`sample/GET_POST`],
loadingUsers: loading[`sample/GET_USERS`]
}),
{
getPost,
getUsers
}
)(SampleContainer);
같은 동작을 더 짧게 구성했다!!!
훨씬 보기 좋다.
그렇다면 이제 sample리듀서에서 필요없는 코드를 지워보도록 하자
modules/sample.js
import { handleActions } from 'redux-actions';
import * as api from '../lib/api';
import createRequestThunk from '../lib/createRequestThunk';
// 액션 타입을 선언
// 한 요청당 세개를 만들어야 된다
const GET_POST = 'sample/GET_POST';
const GET_POST_SUCCESS = 'sample/GET_POST_SUCCESS';
const GET_USERS = 'sample/GET_USER';
const GET_USERS_SUCCESS = 'sample/GET_USER_SUCCESS'; // 리듀서에서 더이상로딩 중에 대한 상태를 관리할 필요가 없다.
// thunk함수를 생성한다.(액션 함수 작성)
// thunk함수 내부에서는 시작할 때, 성공했을 때, 실패했을 때 다른 액션을 디스패치한다.
export const getPost = createRequestThunk(GET_POST, api.getPost);
export const getUsers = createRequestThunk(GET_USERS, api.getUsers);
// 초기 상태를 선언한다
// 요청의 로딩 중 상태는 loading이라는 객체에서 관리한다.
const initialState = {
post: null,
users: null
};
// 리듀서 함수 작성
const sample = handleActions(
{
[GET_POST_SUCCESS]: (state, action) => ({
...state,
post: action.payload
}),
[GET_USERS_SUCCESS]: (state, action) => ({
...state,
users: action.payload,
}),
},
initialState
);
export default sample;
sample리듀서에서는 성공 케이스만 잘 관리하면 된다.
실패 케이스를 관리하고 싶다면 _FAILURE가 붙은 액션을 리듀서에서 처리하면 된다.
이렇게 해서 redux-thunk를 다뤄봤다.
처음 사용할 때는 작성해야 할 코드가 많아 불편할 수 있지만,
유용한 함수를 만들어 상태를 관리한다면 가독성이 좋은 코드를 구현할 수 있다.
'React' 카테고리의 다른 글
React) 19장 코드 스플리팅 ( React.lazy-Suspense, loadable_component ) (0) | 2022.05.23 |
---|---|
React) 18장 리덕스 미들웨어를 이용한 비동기 작업관리 ( redux-saga ) (0) | 2022.05.21 |
React) 18장 리덕스 미들웨어를 이용한 비동기 작업관리( 미들웨어 ) (0) | 2022.05.20 |
React) 17장 리덕스를 이용한 Todo리스트 (0) | 2022.05.14 |
React) 16장 redux 라이브러리 (0) | 2022.05.12 |