● 실습 source
src > index.js
index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
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>
);
src > modules > counter.js
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);
const initialState = 0;
const counter = handleActions({
[INCREASE] : state => state + 1,
[DECREASE] : state => state - 1
}, initialState);
export default counter;
src > modules > index.js
index.js
import { combineReducers } from 'redux';
import counter from './counter';
const rootReducer = combineReducers({
counter
})
export default rootReducer;
src > containers > CounterContainer.jsx
CounterContainer.jsx
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)
src > components > Counter.jsx
Counter.jsx
const Counter = ({onIncrease, onDecrease, number}) => {
return (
<div>
<h1>{number}</h1>
<button onClick={onIncrease}>+1</button>
<button onClick={onDecrease}>-1</button>
</div>
)
}
export default Counter;
src > App.js
App.js
import CounterContainer from './containers/CounterContainer';
const App = ()=>{
return (
<div>
<CounterContainer />
</div>
)
}
export default App;
● 미들웨어란?
이전에 node.js에서 CORS에러를 처리하는 공부를하다가 bodyparser, morgan, express, path, cors 등 여러가지 미들웨어 모듈에 대해서 알아보고 공부를 했었는데요, 리덕스에서도 미들웨어라는 개념이 있네요. 미들웨어란 정확히 무엇을 뜻할까요?
미들웨어는 서로 다른 소프트웨어 구성 요소 간의 통신을 통합하고 간소화하는 데 도움이 되는 다리 또는 접착제 역할을 합니다. 일반적으로 메시지 라우팅, 변환, 보안, 로깅 및 오류 처리와 같은 공통 서비스를 제공하여 여러 애플리케이션 또는 시스템에서 공유할 수 있습니다.
라고 합니다. 그냥 글만 보면 미들웨어가 도대체 무엇인지에 대해서 정확히 파악하기가 힘든데요, 제가 node.js에서 미들웨어를 직접 사용해보면서 느낀 것은
어떠한 단계로 넘어가기 '전' 에 어떤 처리를 해주는 것
이라고 파악한 것 같습니다. 따라서 리덕스 미들웨어도 액션을 디스패치했을 때, 리듀서에서 이를 처리하기전에 앞서 사전에 지정된 작업들을 실행합니다. 즉, 미들웨어는 액션과 리듀서 사이의 중간자라고 볼 수 있습니다.
미들웨어어로 할 수 있는 작업은 여러가지가 있습니다.
- 콘솔에 액션 출력하기
- 액션 취소하기
- 다른 종류의 액션을 추가로 디스패치하기
등이 있습니다.
● 미들웨어를 만들기
실제 프로젝트를 작업할 때는 미들웨어를 직접 만들어서 사용할 일은 그리 많지 않다고 합니다. 이미 다른 개발자들이 미들웨어를 구축해놓았기 때문이죠. 하지만 원하는 미들웨어를 찾을 수 없는 경우에는 직접 만들거나 기존 미들웨어들을 커스터마이징해서 사용할 수 있다고 합니다.
미들웨어를 그래도 만들어보며 실습을.. 해야겟졈?
경로 : src > lib > loggerMiddleware.js
loggerMiddleware.js
const loggerMiddleware = store => next => action => {
};
export default loggerMiddleware;
미들웨어의 기본 구조는 위와 같습니다. 함수를 (store => next => action) 순으로 기입합니다.
미들웨어는 결국 함수를 반환하는 함수를 반환하는 함수입니다. 여기에서 함수 파라미터로 받아오는 store는 리덕스 스토어 인스턴스를, action은 디스패치된 액션을 가르킵니다. next 파라미터는 함수 형태이며, store.dispatch와 비슷한 역할을 합니다. 하지만 큰 차이점이 있는데, next(action)를 호출하면 그다음 처리해야 할 미들웨어에게 액션을 넘겨주고, 만약 그 다음 미들웨어가 없다면 리듀서에게 액션을 넘겨준다는 것입니다.
이는 node.js에서도 비슷했습니다. next함수를 호출하면 다음 미들웨어로 넘어갔었죠? 리덕스 미들웨어도 비슷합니다.
- 미들웨어 내부에서 store.dispatch를 사용하면 첫번째 미들웨어부터 다시 처리합니다.
- 만약 미들웨어에서 next를 사용하지 않으면 액션이 리듀서에 전달되지 않습니다. 즉, 액션이 무시됩니다.
이제 미들웨어를 마저 구현해보겠습니다. 다음정보를 순차적으로 콘솔에 보여 줍니다.
- 이전 상태
- 액션 정보
- 새로워진 상태
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;
다음 index.js에 적용해줍니다.
src > index.js
index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { createStore, applyMiddleware } 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>
);
createStore 두번째 인자로 redux devtool을 적용했던 것이 기억나시나욤? 보니까 저렇게 applyMiddleware를 불러와 미들웨어를 적용할 수 있었네욥••• 신기방기
그럼 이제 미들웨어가 정말 작동하는지 확인해보겠습니다.
넘나 신기한것•••
코드 중간에 보면 next(action)이 있죠? 따라서 action을 원하는 조건에 충족한다면 다음 미들웨어 or 리듀서에게 전달할 수 있게 하거나, action을 무시하게 할수도 있습니다. 즉, 액션을 보내는 과정속에서 여러가지 많은 작업들을 할 수 있습니다.
이러한 미들웨어 속성을 사용하여 네트워크 요청과 같은 비동기 작업을 관리하면 매우 유용합니다.
● redux-logger 사용하기
오픈 소스 커뮤니티에 올라와있는 redux-logger 미들웨어를 설치하고 사용해보겠습니다.
npm i redux-logger
src > index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { createStore, applyMiddleware } 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>
);
넘나 신기한 것 . . . 콘솔에 색상도 입혀지고 액션 디스패치 시간도 나타납니다. 리덕스에서 미들웨어를 사용할 때는 이렇게 이미 완성된 미들웨어를 라이브러리로 설치해서 사용하는 경우가 많다고 합니다.
● 비동기 작업을 처리하는 미들웨어 사용
비동기 작업을 처리할 때 도움을 주는 미들웨어는 다양합니다.
- redux-thunk : 비동기 작업을 처리할 때 가장 많이 사용하는 미들웨어입니다. 객체가 아닌 함수 형태의 액션을 디스패치 할 수 있게 해줍니다.
- redux-saga : thunk다음으로 가장 많이 사용되는 비동기 작업 관련 미들웨어 라이브러리입니다. 특정 액션이 디스패치 되었을 때 정해진 로직에 따라 다른 액션을 디스패치시키는 규칙을 작성하여 비동기 작업을 처리할 수 있게 해줍니다.
⚬ redux-thunk
먼저 thunk 라는 의미가 뭔 뜻일까요?
컴퓨터 프로그래밍에서 썽크(Thunk)는 기존의 서브루틴에 추가적인 연산을 삽입할 때 사용되는 서브루틴이다. 썽크는 주로 연산 결과가 필요할 때까지 연산을 지연시키는 용도로 사용되거나, 기존의 다른 서브루틴들의 시작과 끝 부분에 연산을 추가시키는 용도로 사용되는데, 컴파일러 코드 생성시와 모듈화 프로그래밍 방법론 등에서는 좀 더 다양한 형태로 활용되기도 한다.
썽크(Thunk)는 "고려하다"라는 영어 단어인 "Think"의 은어 격 과거분사인 "Thunk"에서 파생된 단어인데, 연산이 철저하게 "고려된 후", 즉 실행이 된 후에야 썽크의 값이 가용해지는 데서 유래된 것이라고 볼 수 있다.[1]
즉, thunk는 특정 작업을 나중에 할 수 있도록 미루기 위해 함수 형태로 감싼 것을 의미합니다. 예를 들어 주어진 파라미터에 1을 더하는 함수를 만들고 싶다면 아래와 같을 것 입니다.
const addOne = x => x+1;
addOne(2) // 3
만약에 이 연산 작업을 나중에 하도록 미루고 싶다면 어떻게 해야할까요?
const addOne = x => x+1;
function addOneThunk(x){
const thunk = ()=> addOne(x);
return thunk;
}
const fn = addoneThunk(1);
setTimeOut(()=>{
const value = fn(n); // f(n)이 실행되는 시점에 연산
console.log(value);
}, 1000);
이렇게 하면 특정 작업을 나중에 하도록 미룰 수 있습니다. 만약 addOneThunk를 화살표 함수로만 사용한다면 다음과 같이 구현할 수 있습니다.
const addOne = x => x + 1;
const addOneThunk = x => () => addOne(x);
const fn = addOneThunk(1);
setTimeOut(()=>{
const value = fn();
console.log(value);
}, 1000);
화살표 함수를 사용하면 더 깔끔하고 논리구조를 이해하기가 뭔가 더 쉬운것같네요. 저만그런가염?ㅎ;
redux-thunk 라이브러리를 사용하면 thunk함수를 만들어서 디스패치할 수 있습니다. 그러면 리덕스 미들웨어가 그 함수를 전달받아 store의 dispatch와 getState를 파라미터로 넣어서 호출해줍니다.
다음은 redux-thunk에서 사용할 수 있는 예시 thunk 함수입니다.
const sampleThunk = () => (dispatch, getState) => {
// 현재 상태 참조가능,
// 새 액션 디스패치 가능
}
- 미들웨어 적용하기
이제 redux-thunk 미들웨어를 설치하고 프로젝트에 적용해보겠습니다.
npm i redux-thunk
src > index.js
index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { createStore, applyMiddleware } from 'redux';
import rootReducer from './modules';
import { Provider } from 'react-redux';
import { createLogger } from 'redux-logger';
import thunk from 'redux-thunk'; // thunk 적용
const logger = createLogger();
// applyMiddleware 인자로 thunk 추가
const store = createStore(rootReducer, applyMiddleware(logger, thunk));
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
- Thunk 생성 함수 만들기
redux-thunk는 액션 생성 함수에서 일반 액션 객체를 반환하는 대신에 함수를 반환합니다. increaseAsync와 decreaseAsync함수를 만들어서 카운터 값을 비동기적으로 변경시켜 보겠습니다.
경로 : modules > counter.js
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);
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;
thunk 미들웨어에서 ()=> (dispatch, getStore) => { } 순으로 미들웨어를 사용하는 것처럼 코드를 적고, dispatch를 1초뒤에 보내지도록 해줍니다.
리덕스 모듈을 수정했으면 CounterContainer.js도 수정합니다.
경로 src > containers > CounterContainer.js
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)
처음에 디스패치 되는 액션은 함수이고, 두번째로 디스패치 되는 액션은 객체인걸 확인할 수 있습니다.
와우 겁나 신기;;
reducer는 순수함수여야하기 때문에 비동기적인 로직을 포함할 수 없습니다. 따라서, redux-thunk 라이브러리는 리듀서 함수가 아닌 액션 생성 함수에서 비동기 로직을 담자는 취지입니다.
[리듀서는 순수함수여야하니까 비동기처리는 액션 생성자 함수에서?]
Q. 그러니까 내가 redux-thunk에 대해 이해한바가 맞는지 확인해줘. 리듀서 함수는 순수 함수여야하기 때문에 비동기 함수를 내부에서 쓸수 없다고 하잖아. 그러니까 액션 생성자 함수에서 비동기
ddaeunbb.tistory.com
- 웹 요청 비동기 작업 처리하기
이번에는 thunk 속성을 활용하여 웹 요청 비동기 작업을 처리하는 방법에 대해 알아보겠습니다.
웹 요청을 위해 가짜 AP를 사용하겠습니다.
# 포스트 읽기 : https://jsonplaceholder.typicode.com/posts/:id
# 모든 사용자 정보 불러오기 : https://jsonplaceholder.typicode.com/users
API를 호출할 때에는 axios를 사용하겠습니다.
경로 : src > lib > api.js
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`);
각 API를 호출하는 함수를 따로 작성하면, 나중에 사용할 때 가독성도 좋고 유지 보수도 쉬워집니다. 다른 파일에서 불러와 사용할 수 있도록 export를 해줍니다.
새로운 리듀서를 생성해줍니다.
src > modules >sample.js
sample.js
import { handleAction } 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';
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;
}
};
const initialState = {
loading : {
GET_POST : false,
GET_USERS : false
},
post : null,
users : null
};
const sample = handleAction({
[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
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.jsx
Sample.jsx
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>
{loadingPost && '로딩 중...'}
{!loadingPost && users && (
<ul>
{
users.map(user => <li key={user.id}>{user.name} ({user.email})</li>)
}
</ul>
)}
</section>
</div>
)
}
export default Sample;
다음으로 컨테이너 컴포넌트를 만드러줍니다.
src > containers > SampleContainer.jsx
import { connect } from 'react-redux'
import Sample from '../components/Sample';
import { getPost, getUsers } from '../modules/sample'
import { useEffect } from 'react';
const SampleContainer = ({post, users, loadingPost, loadingUsers, getPost, getUsers})=>{
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);
- 리팩토링
API 요청을 할 때마다 17줄 정도 되는 thunk함수를 작성하는 것과 로딩 상태를 리듀서에서 관리하는 작업은 귀찮아질 뿐만 아니라, 코드도 길어지게 만듭니다. 그러므로 반복되는 로직을 따로 분리하여 코드 양을 줄여보겟읍니다.
src > lib > createRequestThunk.js
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;
}
}
}
그럼 sample.js에 있는 기존 thunk 함수 코드를 대체해봅시다
sample.js
import { handleAction } from 'redux-actions';
import createRequestThunk from '../lib/createRequestThunk'
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';
export const getPost = createRequestThunk(GET_POST, api.getPost);
export const getUsers = createRequestThunk(GET_USERS, api.getUsers);
const initialState = {
loading : {
GET_POST : false,
GET_USERS : false
},
post : null,
users : null
};
const sample = handleAction({
[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 > loading.js
loading.js
import { createAction, handleAction } from 'redux-actions';
const START_LOADING = 'loading/START_LOADING';
const FINISH_LOADING = 'loading/FINISH_LOADING';
export const startLoading = createAction(
START_LOADING,
requestType => requestType
);
export const finishLoading = createAction(
FINISH_LOADING,
requestType => requestType
);
const initialState = {};
const loading = handleAction({
[START_LOADING] : (state, action) => ({
...state,
[action.payload] : true
}),
[FINISH_LOADING] : (state, action) => ({
...state,
[action.payload] : false
})
}, initialState);
export default loading;
다음 리듀서를 index.js에서 합쳐줍니다.
src > modules > index.js
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 리덕스 모듈에서 만든 액션 생성 함수는 앞에서 만든 createRequestThunk에서 사용해줍니다.
src > lib > createRequestThunk.js
createRequestThunk.js
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;
}
}
}
그러면 SampleContainer에서 로딩 상태를 다음과 같이 조회할 수 잇읍니다..
SampleContainer.jsx
import { connect } from 'react-redux'
import Sample from '../components/Sample';
import { getPost, getUsers } from '../modules/sample'
import { useEffect } from 'react';
const SampleContainer = ({post, users, loadingPost, loadingUsers, getPost, getUsers})=>{
useEffect(()=>{
getPost(1);
getUsers(1);
}, [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);
우와.. 여기가 저의 한계네요.. 너무 어렵다, 미쳣는데.. saga 어떻게 공부하냐.. 하•••
thunk 이해 30%정도밖에 못한거 같은..ㅎ_ㅎ 내일 또해야겟 ㅎ다 ㅎ.. ㅎ..