[fe] redux 미들웨어 정리
Redux library
Redux 미들웨어 정리
미들웨어란 ?
미들웨어는 액션을 디스패치 했을때 리듀서에서 이를 처리하기 앞서 사전에 지정된 작업들을 하는 중간 처리과정의 개념이다.
- 미들웨어를 중간에 놓음으로서 처리할 수 있는 다양성
- 전달받은 액션을 콘솔에 기록
- 액션을 취소한다
- 다른 종류의 액션도 필요시 추가로 디스패치한다.
dispatch() 메소드 호출을 통해 store에 전달하는 액션을 가로채는게 핵심 개념인데 그렇다면 어떤 상황에서 가르채는것이 필요할까?
일반적으로 많이 쓰는 용도로는
- API 요청을 할때 미리 request 액션을 dispatch() 하여 로딩아이콘을 띄운다.
- API 요청 처리가 완료되면 결과에 따라
Success
나Fail
로 각각 다른 액션을 dispatch() 하여 결과를 업데이트한다.
대략적인 예시 코드는 아래와 같다. 기존 액션생성과 비교해보면 단순한 하나의 액션요청이 아니라 큰 개념에서의 액션 하나에 필요한 부수적인 작업들에 대해 액션을 여러개 dispatch 할 수 있다는 것이 더 디테일한 api 처리를 할 수 있다는 redux-thunk의 장점인것 같다.
export const getPosts = () => async dispatch => {
dispatch({ type: GET_POSTS }); // 요청이 시작됨
try {
const posts = await postsAPI.getPosts(); // API 호출
dispatch({ type: GET_POSTS_SUCCESS, posts }); // 성공
} catch (e) {
dispatch({ type: GET_POSTS_ERROR, error: e }); // 실패
}
};
더 자세한 내용을 통해 실제 내부 구현 원리부터 해서 차근차근 알아가보도록 하자
1. redux-thunk
액션 생성함수를 간단히 작성할 수 있게 도와주고, 리듀서에서 switch문을 생략해주는 라이브러리 라고 하는데 무슨 뜻인지 한 번에 이해되기에는 쉽지 않다. 그래서 조금 더 심플하게 단계적으로 생각해보면
thunk
는 특정 작업을 나중에 하도록 미루기 위해 함수 형태로 감싼것을 칭한다.
예시코드
const x = 1 + 2; //즉시 실행
const foo = () => 1 + 2; // 추후 함수 호출시 실행
위의 코드 처럼 함수로 한번 감싸면 해당 함수가 호출 되었을 때 1+2의 연산이 수행된다. 이를 통해 redux-thunk의 역할을 유추해보면, redux-thunk는 dispatch가 원하는 경우에 호출 될 수 있도록 세부적으로 컨트롤 가능하게 하는 도구이다
아래 일반적인 액션생성함수와 이를 실행하는 dispatch 구조를 살펴보면 dispatch가 실행되는 순간 액션이 리듀서에게 전달되어서 상태값 변경이 수행됨을 알 수 있다.
const actionCreator = (payload) => ({action: 'ACTION', payload});
dispatch(actionCreator());
아래는 redux-thunk 가 사용되었을때의 똑같은 예시이다.
const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
function increment() {
return {
type: INCREMENT_COUNTER
};
}
function incrementAsync() {
return dispatch => { // dispatch 를 파라미터로 가지는 함수를 리턴합니다.
setTimeout(() => {
// 1 초뒤 dispatch 합니다
dispatch(increment());
}, 1000);
};
}
dispatch(incrementAsync());
위의 코드의 특징은 incrementAsync 함수를 dispatch로 호출했을때 increament의 내용이 바로 실행되는 것이 아니라 dispatch를 내부적으로 가진 함수를 반환한것이기 때문에 이 함수내에서는 dispatch를 조건에 맞게 사용하여 1초뒤에 increament가 실행되는 방식으로 컨트롤하였다. 따라서 thunk를 사용함으로써 dispatch를 원하는 상황에 맞게 컨트롤하여 함수내부에서 재사용되어 사용될 수 있다.
즉 다시 정리해보면, 기존의 일반 객체 타입 액션생성함수(action)은 단순히 리듀서가 실행해줄 명령을 큐카드에 작성하여 전달해주는게 전부였다. 예를들어 increment
를 수행하기 원하면 액션 생성함수는 단순히 {type:’incrementAsync’}인 객체(데이터)(큐카드)를 리듀서에게 전달한다. 그러면 리듀서는 해당 큐카드(메세지가 담긴)를 확인한 후 단순히 increment를 수행한다. 이 과정에는 리듀서나 액션에서 별도의 무언가를 고민할 필요없이 순수객체로 전달하여 명령을 전달하고 전달받고 끝난다.
하지만 redux-thunk를 사용하게 되면 단순히 객체로 된 action 내용을 리듀서에게 전달해주는 것이 아니라 조금더 복잡하고 다양한 상황이 담긴 내용을 함수로 만들어서 전달해줄 수 있다. 이럴 경우 해당 action 내용이 바로 reducer 한테 전달되는 것이 아니라 중간에 미들웨어를 걸쳐 차례차례 중간 과정을 모두 수행한다. 위의 예시에서는 action을 함수타입으로 만들었기 때문에 dispatch를 했을때 바로 reducer가 액션을 수행하는것이 아니라 미들웨어에서 함수 안의 내용을 수행한다. 그래서 setTimeout이 실행되고 그 안에 있는 dispatch가 1초후에 실행되는데 이때 action 함수는 순수 객체인 increment()이기 때문에 이번 dispatch 에서는 리듀서로 action이 전달되게 된다.
즉, 요약하면
액션생성함수 = {}; // 객체 저장
thunk액션생성함수 = (dispatch) => (); // 함수 저장
위의 상황처럼 정리해볼 수 있다.
실제 redux-thunk에서는 dispatch만 파라미터로 전달해주는게 아니라 getState() 라는 함수도 전해준다. 따라서 현재 state의 조건에 따라 dispatch를 할지 안할지도 컨트롤해줄수가 있다. 아래 코드를 통해 확인해보면 아래 코드에서는 counter의 값이 홀수일때만 dispatch를 해주는 방법으로 컨트롤할 수 있다.
const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
function increament() {
return {
type: INCREMENT_COUNTER
};
}
function incrementIfOdd() {
return (dispatch, getState) => {
const { counter } = getState();
if (counter % 2 === 0) {
return;
}
dispatch(increment());
};
}
dispatch(incrementAsync());
그렇다면 실제 redux-thunk의 내부구조가 어떻게 생겼길래 위와 같이 갑자기 dispatch와 getState를 어딘가에서 파라미터로 받아오고 있는지 확인해보면,
function createThunkMiddleware(extraArgument) {
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;
위의 createThunkMiddleware() 함수 내의 if문을 유의깊게 보면, 만약 전달받은 액션생성함수가 객체가 아니라 함수타입이면 내부에서 다시 액션함수를 생성해주는데 이때 dispatch와 getState값을 넘겨주고 있다. 따라서 위의 incrementIfOdd 예제에서 보는거와 같이 액션생성함수가 함수타입일 경우 파라미터로 dispatch와 getState를 전달 받아 내부적으로 다시 객체타입의 액션생성함수를 생성하여 dispatch해준다. 이렇게 되면 dispatch로 전달받은 action은 객체 타입이기 때문에 next(action)이 수행되어 리듀서로 최종 전달되게 된다.
아래 그림을 통해 액션이 리듀서까지 전달되는 과정을 알 수 있다.
그렇다면 비동기 API 처리 상황에서 미들웨어는 필수인가 선택적 필요인가 ?
필수라고 하긴 어렵지만 보통 일반적인 API 호출시 변경되어야 하는 store의 state 값들은 여러개가 있을 수 있다. 예를들어 loading, data, 에러처리 등 최소 3개의 값들이 변경되어야하는데, 이때 이 3가지 세트를 하나의 액션으로 변경해주고자 묶을때는 redux-thunk를 쓰는게 훨씬 편리할 수 있다. 하나의 thunk 액션함수 안에 3번의 dispatch를 통한 상태값 변경과 api 호출 구문을 모두 비동기 처리로 넣어놓으면 api 호출과 함께 세트로 묶인 내용들이 변경되기 때문에 각각의 액션들을 따로 만들고 하나의 함수에 동기식으로 넣어서 만드는거보다 훨씬 수월한 방법이 될 수 있다.