// async/await로 만든 액션 생성 함수
export const fetchData = async () => {
//임의의 api 주소에 async await으로 (get) 요청을 하였다.
const response = await APIadress.get('/data')
return {
type: 'FETCH_DATA',
payload: response
}
}
// 결과 : 아래 에러 발생
// action must be plain objects. Use custom middleware for async actions
// 이유 : reducer로 dispatch 되는 최초의 action 형태는
// plain object가 아니라 async await의 함수의 형태이기 때문
// Promise로 만든 액션 생성 함수
export const fetchData = async () => {
//임의의 api 주소에 (get) 요청을 promise 형태로 하였다.
const promise = await APIadress.get('/data')
return {
type: 'FETCH_DATA',
payload: response
}
}
// 결과 : 에러는 나지 않지만 정상 작동하지 않음
// 이유 : Promise를 사용하면 reducer에서
// '반환된 데이터가 아직 없다'(객체에 아무것도 없다!)라고 판단하기 때문
// 미들웨어 기본 템플릿
const middleware = store => next => action => {
// 처리 로직
};
// 일반 함수 형식
const middleware = function middleware(store) {
return function(next) {
return function(action) {
// 처리 로직
}
}
}
store
는 리덕스 스토어 인스턴스로, 이 안에 dispatch
, getState
, subscribe
내장함수들이 들어있다.next
는 액션을 다음 미들웨어에게 전달하는 함수로 store.dispatch
와 비슷한 역할을 한다. next(action)
형태로 사용하며, next(action)
를 호출하면 그 다음 처리해야할 미들웨어에게 액션을 넘겨주고, 만약 미들웨어가 없다면 리듀서에게 액션을 넘겨준다. 만약 미들웨어 내부에서 next
를 사용하지 않으면 액션이 리듀서에게 전달되지 않는다. 즉 액션이 무시되는 것이다.action
은 디스패치되어 현재 처리하고 있는 액션을 가리킨다.store.dispatch
를 사용하면 첫번째 미들웨어부터 다시 처리한다.next(action)
을 호출하게 되면 다음 미들웨어로 액션이 넘어간다. 그리고 만약 미들웨어에서 store.dispatch
를 사용하면 다른 액션을 추가적으로 발생시킬 수 도 있다.// src/lib/loggerMiddleware.js
// 이전상태, 액션정보, 새로워진 생태를 순차적으로 콘솔에 기록하는 미들웨서
const loggerMiddleware = store => next => action => {
// 미들웨어 기본 구조
console.group(action && action.type); // 액션 타입을 그룹명으로 설정
console.log('이전상태', store.getState());
console.log('action', action);
next(action); // 다음 미들웨어 혹은 리듀서에게 전달
// next 함수를 쓰지 않으면 리듀서에게 전달이 되지 않기 때문에 액션이 무시 됨
// next 함수를 기준으로 이전이 이전 상태, 이후가 다음 상태
console.log('다음 상태', store.getState());
console.groupEnd(); // 그룹 끝
}
export default loggerMiddleware;
// src/index.js - 미들웨어 스토어에 적용
import React from 'react';
import ReactDOM from 'react-dom';
import { applyMiddleware, createStore } from 'redux';
import { Provider } from 'react-redux';
import './index.css';
import App from './App';
import rootReducer from './modules';
import loggerMiddleware from './lib/loggerMiddleware';
// 미들웨어는 스토어를 생성하는 과정에서 applyMiddleware() 함수를 사용하여 적용한다.
const store = createStore(rootReducer, applyMiddleware(loggerMiddleware));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
npm i redux-logger
src/index.js
에 적용하기import React from 'react';
import ReactDOM from 'react-dom';
import { applyMiddleware, createStore } from 'redux';
import { Provider } from 'react-redux';
import './index.css';
import App from './App';
import rootReducer from './modules';
// import loggerMiddleware from './lib/loggerMiddleware';
import { createLogger } from 'redux-logger';
const logger = createLogger();
const store = createStore(rootReducer, applyMiddleware(logger));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
컴퓨터 프로그래밍에서, 썽크(Thunk)는 기존의 서브루틴에 추가적인 연산을 삽입할 때 사용되는 서브루틴이다. 썽크는 주로 연산 결과가 필요할 때까지 연산을 지연시키는 용도로 사용되거나, 기존의 다른 서브루틴들의 시작과 끝 부분에 연산을 추가시키는 용도로 사용되는데, 컴파일러 코드 생성 시와 모듈화 프로그래밍 방법론 등에서는 좀 더 다양한 형태로 활용되기도 한다.
// thunk 예1
// 1+2를 연산하고 싶다면 알해와 같이 코드를 작성하면 된다.
const x = 1 + 2;
// 아래와 같이 코드를 작성하면, 연산이 바로 실행되지 않고
// foo() 함수가 호촐되어야 실행된다.
const foo = () => 1 + 2;
썽크(Thunk)는 "고려하다"라는 영어 단어인 "Think"의 은어 격 과거분사인 "Thunk"에서 파생된 단어인데, 연산이 철저하게 "고려된 후", 즉 실행이 된 후에야 썽크의 값이 가용해지는 데서 유래된 것이라고 볼 수 있다.
Thunk란 특정 작업을 나중에 할 수 있도록 미루기 위해 함수 형태로 감싼 것을 칭한다.
// thunk 예2
// 파라미터로 전달받은 값에 1을 더해서 반환하는 함수
const addOne = x => x + 1;
addOne(1); // 2
// 함수 실행 시 바로 1+1을 연산
// 연산 작업을 1초 후애 실행하는 함수
const addOne = x => x + 1; // 파라미터에 1을 더하는 함수
function addOneThunk(x) { // addOne 함수를 반환하는 함수를 반환하는 함수
const thunk = () => addOne(x);
return thunk;
}
const fn = addOneThunk(1); // () => addOne(1);
setTimeout(() => {
const value = fn(); // fn이 실행되는 시점 - 연산이 실행됨
console.log(value)
}, 1000)
// 화살표 함수로 변환
const addOne = x => x + 1;
const addOneThunk = x => () => addOne(x);
const fn = addOneThunk(1); // () => addOne(1);
setTimeout(() => {
const value = fn(); // fn이 실행되는 시점
console.log(value)
}, 1000)
redux-thunk 라이브러리를 사용하면 thunk 함수를 만들어서 디스패치 할 수 있다. 그러면 리덕스 미들웨어가 그 함수를 전달받아 store의 dispatch와 getState를 파라미터로 넣어 호출해준다.
// redux-thunk에서 사용할 수 있는 thunk 함수 예시
const simpleThunk = () => (dispatch, getState) => {
// 현재 상태를 참조할 수 있고, 새 액션을 디스패치할 수 있다.
}
const actionCreator = (payload) => ({action: 'ACTION', payload});
dispatch
와 getState
를 파라미터로 가질 수 있다.// 액션 타입 선언
const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
// 액션 생성 함수
const increase = () => ({action: INCREMENT_COUNTER});
// dispatch를 매개변수로 갖고 있는 함수를 리턴
// 이 함수를 thunk 함수라고 한다.
const increaseAsync = () => dispatch => {
// 1초 뒤 dispatch
setTimeout(() => {
dispatch(increase());
}, 1000)
}
// INCREMENT_COUNTER 액션이 1초 뒤 디스패치
store.dispatch(increaseAsync());
// dispatch와 getState를 매개변수로 갖는 함수를 리턴
const dispatchIfOdd = () => (dispatch, getState) => {
const { counter } = getState();
if (counter % 2 === 0) {
return;
}
dispatch(increment());
}
dispatch, getState
를 파라미터 갖는다면 스토어의 상태에도 접근 할 수 있다. 따라서, 현재의 스토어 상태의 값에 따라 액션이 dispatch
될 지 무시될지 정할 수 있다.dispatch
와 getState
를 넣어서 실행해준다. 실제로, redux-thunk 의 코드는 정말로 간단하다. 아래 그 코드를 보면 더 이해하기 쉽다.function createThunkMiddleware(extraArgument) {
// store에서 dispatch와 getState를 비구조화 할당으로 전달
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
npm i redux-thunk
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { applyMiddleware, createStore } from 'redux';
import { Provider } from 'react-redux';
import './index.css';
import App from './App';
import rootReducer from './modules';
import { createLogger } from 'redux-logger';
import ReduxThunk from 'redux-thunk'; // 1. import redux-thunk
const logger = createLogger();
// createStore의 매개 변수에 applyMiddleware() 함수 할당
// applyMiddleware() 함수의 매개변수에 ReduxThunk를 할당
const store = createStore(rootReducer, applyMiddleware(logger, ReduxThunk));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
dispatch
, getState
를 파라미터로 넣어 사용하는 원리다. 그래서 thunk 함수 내부에서 api 요청, 다른 액션 dispatch
, 현재 상태 조회 등의 작업을 할 수 있다. 대부분의 경우에서는 redux-thunk로 충분히 기능을 구현할 수 있다.function*
키워드를 사용한다.// 일반 함수 - 아래처럼 여러 개의 값을 반환하는 것은 불가능 하다.
function generalFunction() {
return 1;
return 2;
return 3;
}
// 제너레이터 함수 선언 - function* 키워드 사용
// function와 *를 띄워도 상관없지만 붙이는 것을 권장
function* generateFunction() {
yield 1;
yield 2;
return 3;
}
// '제너레이터 함수'는 '제너레이터 객체'를 생성한다.
let generator = generateFunction();
alert(generator); // [object Generator]
next()
라는 메서드를 갖고 있다. 제너레이터가 처음 만들어지면 함수의 흐름은 멈춰 있는 상태인데, next()
메서드를 호출하면 가장 가까운 yield <value>
문을 만날 때까지 명령을 실행한다.(value를 생략할 수도 있는데, 이 경우엔 undefined가 된다) 이후, yield <value>
문을 만나면 실행을 멈추고 산출하고자 하는 값인 value가 바깥 코드에 반환된다.next()
는 항상 아래 두 프로퍼티를 가진 객체를 반환합니다.value
: 산출 값done
: 함수 코드 실행이 끝났으면 true
, 아니라면 false
function* generateFunction() {
console.log('hello');
yield 1; // 1번 실행 - 여기까지 실행 후 value: 1을 반환
console.log('generateFunction');
yield 2; // 2번 실행 - 여기까지 실행 후 value: 2를 반환
console.log('end')
return 3; // 3번 실행 - 여기까지 실행 후 value: 2를 반환, 제너레이터가 끝났음을 알림
}
let generator = generateFunction();
// 1번
console.log(generator.next());
// 결과 - hello / {value: 1, done: false}
// 2번
console.log(generator.next());
// 결과 - generateFunction / {value: 2, done: false}
// 3번
console.log(generator.next());
// 결과 - end / {value: 3, done: true}
// 4번
console.log(generator.next());
// 결과 - {value: undefined, done: true}
next()
메서드를 보면 짐작할 수 있듯이, 제너레이터는 이터러블이다. 따라서 for..of
반복문을 사용해 값을 얻을 수 있습니다. 이 방식을 이용하면 .next().value
을 호출하는 것 보다 쉽게 모든 value를 얻을 수 있다.function* generateSequence() {
yield 1;
yield 2;
return 3;
}
let generator = generateSequence();
for(let value of generator) {
alert(value); // 1, 2가 출력됨
}
for..of
이터레이션이 done: true
일 때 마지막 value를 무시하기 때문이다. 그러므로 for..of
를 사용했을 때 모든 값이 출력되길 원한다면 yield
로 값을 반환해야 한다.function* generateSequence() {
yield 1;
yield 2;
yield 3;
}
let generator = generateSequence();
for(let value of generator) {
alert(value); // 1, 2, 3
}
function* generateSequence() {
yield 1;
yield 2;
yield 3;
}
let sequence = [0, ...generateSequence()];
alert(sequence); // 0, 1, 2, 3
yield
가 양방향 길과 같은 역할을 하기 때문이다. yield
는 결과를 바깥으로 전달할 뿐만 아니라 값을 제너레이터 안으로 전달하기까지 한다.next()
함수에 파라미터를 넣고, 제너레이터 함수에서는 yield
를 사용하여 해당 값을 조회하여 값을 안, 밖으로 전달할 수 있다.generator.next(arg)
를 호출할 때 인수 arg
는 yield
의 결과가 된다.
// 제너레이터 함수 선언
function* gen() {
// 질문을 제너레이터 밖 코드에 던지고 답을 기다린다.
let result = yield "2 + 2 = ?"; // (*)
alert(result);
}
//제너레이터 객체 생성
let generator = gen();
// generator.next() -> { value: "2 + 2 = ?", done: false }
// yield의 value를 반환한다.
let question = generator.next().value;
// next() 함수의 매개변수가 yield에 전달되고, yield는 result에 저장된다.
// 그 후 alert(result); 가 실행된다.
generator.next(4); // { value: undefined, done: true }
generator.next()
를 처음 호출할 땐 항상 매개변수가 없어야 한다. 매개변수가 있더라도, 이미 yield
에 value
가 있기 때문에 무시된다. generator.next()
를 호출하면 제너레이터 함수가 실행되고 첫 번째 yield "2+2=?"
의 결과가 반환된다. 이 시점에는 제너레이터가 (*)로 표시한 줄에서 실행을 잠시 멈춘다.yield
의 결과가 제너레이터를 호출하는 외부 코드에 있는 변수, question
에 할당된다.generator.next(4)
에서 제너레이터가 다시 시작되고 4는 result
에 할당된다. ( let result = 4
)next(4)
를 즉시 호출하지 않고 있는데, 이는 제너레이터가 기다려주기 때문에 호출을 나중에 해도 문제가 되지 않는다.next/yield
를 이용해 결과를 전달 및 교환한다.// 제너레이터 함수 정의
function* gen() {
let ask1 = yield "2 + 2 = ?";
alert(ask1); // 4
let ask2 = yield "3 * 3 = ?"
alert(ask2); // 9
}
// 제너레이터 생성
let generator = gen();
alert( generator.next().value ); // "2 + 2 = ?"
alert( generator.next(4).value ); // "3 * 3 = ?"
alert( generator.next(9).done ); // true
next()
가 호출되면, 실행이 시작되고 첫 번째 yield
에 도달한다."2 + 2 = ?"
)은 바깥 코드로 반환된다.next(4)
는 첫 번째 yield
의 결과가 될 4
를 제너레이터 안으로 전달한다. 그리고 다시 실행이 이어집니다.yield
에 다다르고, 산출 값("3 * 3 = ?"
)이 제너레이터 호출 결과가 됩니다.next(9)
는 두 번째 yield
의 결과가 될 9
를 제너레이터 안으로 전달합니다. 그리고 다시 실행이 이어지는데, done: true
이므로 제너레이터 함수는 종료된다.function* watchGenerator() {
console.log('모니터링 중...');
let prevAction = null;
while(true) {
const action = yield;
console.log('이전 액션: ', prevAction);
prevAction = action;
if(action.type === 'HELLO') {
console.log('안녕하세요!');
}
}
}
const watch = watchGenerator();
watch.next();
// 모니터링 중...
// { value: undefined, done: false }
watch.next({type: 'TEST'});
// 이전 액션: null
// { value: undefined, done: false }
watch.next({type: 'HELLO'});
// 이전 액션 : { type: 'TEST' }
// 안녕하세요!
// { value: undefined, done: false }
// modules/counter.js
import { createAction, handleActions } from "redux-actions";
import { delay, put, takeEvery, takeLatest } from "@redux-saga/core/effects";
// 동기 액션 타입 선언
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
// 동기 액션 생성 함수
export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);
// 비동기 액션 타입 선언
const INCREASE_ASYNC = 'counter/INCREASE_ASYNC';
const DECREASE_ASYNC = 'counter/DECREASE_ASYNC';
// 비동기 액션 생성 함수
// 마우스 클릭 이벤트가 payload 안에 들어가지 않도록
//() => undefined 를 두 번째 파라미터로 넣어준다.
export const increaseAsync = createAction(INCREASE_ASYNC, () => undefined);
export const defreascAsync = createAction(DECREASE_ASYNC, () => undefined);
// 제너레이터 함수 정의. 이 제너레이터 함수를 사가(saga)라고 부른다.
function* increaseSaga() {
yield delay(1000); // 1초를 기다린다.
yield put(increase()); // 특정 액션 디스패치
}
function* decreaseSaga() {
yield delay(1000);
yield put(decrease());
}
export function* counterSaga() {
// takeEvery는 들어오는 모든 액션에 대해 특정 작업을 처리
// 모든 INCREASE_ASYNC 액션에 대해 increaseSaga 함수 실행
yield takeEvery(INCREASE_ASYNC, increaseSaga);
// takeLatest는 기존에 진행 중이던 작업이 있다면 취소하고
// 가장 마지막으로 실행된 작업만 수행
// DECREASE_ASYNC액션에 대해서 기존에 진행 중이던 작업이 있다면
// 취소 처리하고 가장 마지막으로 실행된 작업에 대해서만 decreaseSaga 함수 실행
yield takeLatest(DECREASE_ASYNC, decreaseSaga);
}
// 상태는 꼭 객체일 필요가 없다.
const initialState = 0;
const counter = handleActions(
{
[INCREASE]: state => state + 1,
[DECREASE]: state => state - 1
},
initialState
)
export default counter;
delay(ms)
: 작업을 지연 시키는 메서드put(action)
: 특정 액션을 디스패치 시키는 메서드takeEvery(actionType, saga)
: actionType
을 모니터링 하고 있다가, actionType
이 발생하면 saga
를 실행시키는 메서드. saga
가 아직 종료되지 않았는데 actionType
이 발생하면, 기존 saga
실행을 중지하지 않고 saga
를 또 실행 시킨다. (새로은 saga
태스크 생성)takeLatest(actionType, saga)
: actionType
을 모니터링 하고 있다가, actionType
이 발생하면 saga
를 실행시키는 메서드. takeEvery()
와 달리, takeLatest()
는 saga
가 아직 종료되지 않았는데 actionType
이 발생할 경우, 이미 실행 중인 saga
태스크를 종료한 후 마지막으로 발생한 actionType
에 대해서만 saga
를 실행 시킨다. 즉 takeLatest()
는 어느 순간에서도 단 하나의 saga
태스크만 실행되게 한다. (액션이 중첩되어 디스패치 됐을 경우 가장 마지막 액션만 처리)all()
함수를 이용해 여러 사가를 합칠 수 있다.all(arr)
: all()
함수를 사용해서 제너레이터 함수를 배열의 형태로 인자로 넣어주면, 제너레이터 함수들이 병행적으로 동시에 실행되고, 전부 resolve
될때까지 기다린다. Promise.all
과 비슷하다.yield all([testSaga1(), testSaga2()])
testSaga1()
과 testSaga2()
가 동시에 실행되고, 모두 resolve될 때까지 기다린다.// moudule/index.js
import { combineReducers } from "redux";
import { all } from "@redux-saga/core/effects"; // import all method
import counter, { counterSaga } from "./counter"; // import countSaga
import sample from "./sample";
import loading from './loading';
const rootReducer = combineReducers({counter, sample, loading});
// 루트 사가 생성
export function* rootSaga() {
// all 함수는 여러 사가를 합쳐주는 역할을 한다.
yield all([counterSaga()]);
}
export default rootReducer;
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { applyMiddleware, createStore } from 'redux';
import { Provider } from 'react-redux';
import './index.css';
import App from './App';
import rootReducer, { rootSaga } from './modules'; // import root saga
import { createLogger } from 'redux-logger';
import ReduxThunk from 'redux-thunk';
// import createSagaMiddleware
import createSagaMiddleware from '@redux-saga/core';
import { composeWithDevTools } from 'redux-devtools-extension';
const logger = createLogger();
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(logger, ReduxThunk, sagaMiddleware))
);
sagaMiddleware.run(rootSaga);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
delay(ms)
: ms
이후에 resolve
하는 Promise
객체를 리턴한다.put(actionType)
: actionType
을 dispatch
하도록 한다.takeEvery()
: 위 설명 참고takeLatest()
: 위설명 참고all(arr)
: 위 설명 참고call(function, arg)
: function
에 매개변수로 arg
를 넣어 실행 시키는 함수. API를 호출해야 하는 상황에서는 사가 내부에서 직접 호출 하지 않고 call()
함수를 사용한다.select(selector)
: 사가 내부에서 현재 상태를 참조할 때 사용하는 메서드. selector
는 (state[, ...args]) => args
형태의 함수로, 현재 스토어의 상태를 매개변수로 갖는다.const number = yield select(state ⇒ state.counter);
throttle(ms, actionType, saga)
: saga
실행 주기를 제한하는 함수로, actionType
이 발생할 경우 saga
를 실행 시키되, ms
에 한 번만 실행 시킨다.throttle(3000, INCREASE_ASYNC, increaseSaga)
: INCREASE_ASYNC
이 발생할 경우 3초에 increaseSaga
를 단 한 번만 호출한다.