18.1 작업 환경 준비
modules/counter.js
- counter 리덕스 모듈
import {createAction, handleActions} from 'redux-actions';
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
//액션 생성
export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);
const initialState = 0;
//리듀서 함수
const counter = handleActions(
{
[INCREASE]: state => state +1,
[DECREASE]: state => state -1
},
initialState
);
export default counter;
modules/index.js
- 루트 리듀서 생성
import { combineReducers } from "redux";
import counter from "./counter";
const rootReducer = combineReducers({
counter
});
export default rootReducer;
src/index.js
- 스토어 생성한 후, Provider로 리액트 프로젝트에 리덕스 적용
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { createStore } from 'redux';
import rootReducer from './modules';
import { Provider } from 'react-redux';
const store = createStore(rootReducer); //스토어 생성
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App/>
</Provider>
);
component/Counter.js
- 카운트 컴포넌트
const Counter = ({onIncrease, onDecrease, number}) => {
return (
<div>
<h1>{number}</h1>
<button onClick={onIncrease}>+1</button>
<button onClick={onDecrease}>-1</button>
</div>
);
};
export default Counter;
containers/CounterContainer.js
import { connect } from "react-redux";
import {increase, decrease} from '../modules/counter';
import Counter from "../components/Counter";
const CounterContainer = ({number,increase, decrease}) => {
return (
<Counter number={number} onIncrease={increase} onDecrease={decrease}/>
);
};
//컴포넌트와 리덕스 연동
export default connect(
state=>({
number:state.counter
}),
{
increase,
decrease
}
)(CounterContainer);
18.2 미들웨어란?
- 리덕스 미들웨어는 액션을 디스패치했을 때 리듀서에서 이를 처리하기에 앞서 사전에 지정된 작업들을 실행
- 미들웨어는 액션과 리듀서 사이의 중간자라고 볼 수 있음
18.2.1 미들웨어 만들기
- 액션이 디스패치될 때마다 액션의 정보와 액션이 디스패치되기 전후의 상태를 콘솔에 보여주는 로깅 미들웨어 작성
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;
- 미들웨어는 함수를 반환하는 함수
- 이전 상태, 액션 정보, 새로워진 상태를 보여줌
store
- 리덕스 스토어 인스턴스
action
-디스패치된 액션
next
- store.dispatch와 비슷한 역할
- next(action)을 호출하면 그 다음 처리해야 할 미들웨어에게 액션을 넘겨주고, 만약 그 다음 미들웨어가 없다면 리듀서에게 액션을 넘겨줌
- 미들웨어에서 next를 사용하지 않으면 액션이 리듀서에 전달되지 않음
index.js
- 미들웨어는 스토어를 생성하는 과정에서 적용
import ReactDOM from 'react-dom/client';
import App from './App';
import { applyMiddleware, createStore } from 'redux';
import rootReducer from './modules';
import { Provider } from 'react-redux';
import loggerMiddleware from './lib/loggerMiddleware';
const store = createStore(rootReducer, applyMiddleware(loggerMiddleware)); //스토어 생성
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App/>
</Provider>
);
18.2.2 redux-logger 사용하기
npm install redux-logger
index.js
import ReactDOM from 'react-dom/client';
import App from './App';
import { applyMiddleware, createStore } from 'redux';
import rootReducer from './modules';
import { Provider } from 'react-redux';
import {createLogger} from 'redux-logger';
const logger = createLogger();
const store = createStore(rootReducer, applyMiddleware(logger)); //스토어 생성
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App/>
</Provider>
);
18.3 비동기 작업을 처리하는 미들웨어 사용
18.3.1 redux-thunk
- 비동기 작업을 처리할 때 가장 많이 사용하는 미들웨어
- 객체가 아닌 함수 형태의 액션을 디스패치 가능
18.3.1.1 Thunk란?
Thunk
- 특정 작업을 나중에 할 수 있도록 미루기 위해 함수 형태로 감싼 것
ex)
- addOne()을 호출했을 때 바로 1+1이 연산되는데 이를 미룸
const addOne = x => x+1;
cost addOneThunk = x => () => addOne(x);
const fn = addOneThunk(1);
setTimeout(() => {
const value = fn(); //fn이 실행되는 시점에 연산
console.log(value);
},1000);
18.3.1.2 미들웨어 적용하기
redux-thunk 미들웨어 설치
npm install redux-thunk
index.js
- 스토어를 만들 때 redux-thunk 적용
...
import { Provider } from 'react-redux';
import {createLogger} from 'redux-logger';
import thunk from 'redux-thunk';
const logger = createLogger();
const store = createStore(rootReducer, applyMiddleware(logger, thunk)); //스토어 생성
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App/>
</Provider>
);
18.3.1.3 Thunk 생성 함수 만들기
- redux-thunk는 액션 생성 함수에서 일반 액션 객체를 반환하는 대신 함수를 반환
- increaseAsync와 decreaseAsync 함수를 만들어 카운터 값을 비동기적으로 한번 변경
modules/counter.js
...
//1초 뒤에 incrase 혹은 decrease 함수를 디스패치함
export const increaseAsync = () => dispatch => {
setTimeout(() => {
dispatch(increase());
},1000);
};
export const decreaseAsync = () => dispatch => {
setTimeout(() => {
dispatch(decrease());
},1000);
};
...
container/CounterContainer.js
import { connect } from "react-redux";
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);
- 처음 디스패치되는 액션은 함수형태이고, 두 번째 액션은 객체 형태
18.3.1.4 웹 요청 비동기 작업 처리하기
- thunk의 속성을 활용하여 웹 요청 비동기 작업 처리
lib/api.js
- API 함수화
import axios from "axios";
//포스트 읽기
export const getPost = id =>
axios.get(`http://jsonplaceholder.typicode.com/posts/${id}`);
//모든 사용자 정보 불러오기
export const getUsers = id =>
axios.get(`http://jsonplaceholder.typicode.com/users`);
modules/sample.js
- API를 사용하여 데이터를 받아와 상태를 관리할 sample 리듀서 생성
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;
modules/index.js
- sample 리듀서 루트 리듀서에 포함
...
const rootReducer = combineReducers({
counter,
sample
});
...
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(user => (
<li key={user.id}>
{user.username} ({user.email})
</li>
))}
</ul>
)}
</section>
</div>
);
};
export default Sample;
- 데이터를 불러와서 렌더링해 줄때는 유효성 검사를 해주는 것이 중요!!
-> post&&를 사용하면 post 객체가 유효할 때만 그 내부의 post.tilte 혹은 post.body 값을 보여줌
-> 만약 데이터가 없는 상태라면 post.title을 조회하려고 할 때 자바스크립트 오류가 발생
containers/SampleContainer.js
import { useEffect } from "react";
import { connect } from "react-redux";
import Sample from "../components/Sample";
import {getPost,getUsers} from '../modules/sample';
const SampleContainer = ({
getPost,
getUsers,
post,
users,
loadingPost,
loadingUsers
}) => {
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.loading.GET_POST,
loadingUsers: sample.loading.GET_USERS
}),
{
getPost,
getUsers
}
)(SampleContainer);
18.3.1.5 리팩토링
- 반복되는 로직을 따로 분리
lib/createRequestThunk.js
- API 요청을 해주는 thunk 함수를 한 줄로 생성할 수 있게 해줌
- 액션 타입과 API를 요청하는 함수를 파라미터로 넣어주면 나머지 작업 대신 처리
import { startLoading, finishLoading } 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;
}
};
}
//사용법 : createRequestThunk('GET_USERS",api.getUSers);
moudles/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_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 = createRequestThunk(GET_POST, api.getPost);
export const getUsers = createRequestThunk(GET_USERS, api.getUsers);
...
modules/loading.js
- 요청의 로딩 상태 관리하는 작업 개선
- 로딩 상태만 관리하는 리덕스 모듈을 따로 생성하여 처리
import {createAction, handleActions} from 'redux-actions';
const START_LOADING = 'loading/START_LOADING';
const FINISH_LOADING = 'loading/FINISH_LOADING';
/*
요청을 위한 액션 타입을 payload로 설정
*/
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;
요청이 시작될 때 디스패치할 액션
- 이 액션이 디스패치되면 loading 리듀서가 관리하고 있는 상태에서 sample/GET_POST 값을 true로 설정
{
type:'loading/START_LOADING',
payload:'sample/GET_POST'
}
요청이 끝날 때 디스패치할 액션
- 기존에 true로 설정했던 값을 다시 false로 전환
{
type:'loading/FINISH_LOADING',
payload:'sample/GET_POST'
}
modules/index.js
- loading 루트 리듀서에 포함
...
const rootReducer = combineReducers({
counter,
sample,
loading
});
...
containers/SampleContainer.js
...
export default connect (
({sample,loading}) => ({
post: sample.post,
users: sample.users,
loadingPost: loading['sample/GET_POST'],
loadingUsers: loading['sample/GET_USERS']
}),
{
getPost,
getUsers
}
)(SampleContainer);
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_USERS'; //시작
const GET_USERS_SUCCESS = 'sample/GET_USERS_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;
'react' 카테고리의 다른 글
[리액트를 다루는 기술] 19장 코드 스플리팅 (0) | 2023.12.11 |
---|---|
[리액트를 다루는 기술] 18장 리덕스 미들웨어를 통한 비동기 작업 관리(2) (0) | 2023.12.11 |
[리액트를 다루는 기술] 17장 리덕스를 사용하여 리액트 애플리케이션 상태 관리하기 (0) | 2023.11.24 |
[리액트를 다루는 기술] 16장 리덕스 라이브러리 이해하기 (0) | 2023.11.22 |
[리액트를 다루는 기술] 15장 Context API (2) | 2023.11.22 |