위의 이미지가 redux 동작원리의 전부입니다. (미들웨어가 빠졌지만 이는 뒤에서 설명하겠습니다.) 설명하기에 앞서 각각의 요소에 대해서 알아보죠.
액션은 type
필드를 가진 자바스크립트 객체입니다. 쉽게 생각해서, 어떤 일이 일어났는지를 설명하는 이벤트라고 생각하셔도 무방합니다. 보통, type
과 payload
프로퍼티를 가지며 type
은 어떤 액션인지를 나타내며, payload
는 데이터를 담습니다.
액션은 dispatch를 통해 reducer 함수로 보내지며 기존의 state를 기반으로 새로운 state를 생성합니다.(불변성을 지키는 것이 redux의 원칙이죠.)
redux에서 dispatch는 액션을 reducer로 전달합니다. 즉, state를 업데이트하는 유일한 방법은 store.dispatch
함수를 호출하는 것입니다. 이벤트를 발생시키는 역할을 한다고 생각하시면 됩니다.
reducer함수는 기존의 state와 action을 받아서 새로운 state를 만들어내는 함수입니다. 이벤트 리스너라고 생각하면 됩니다. 리듀서는 순수함수로서 몇 가지 규칙을 가집니다. 링크를 참고하세요.
제가 좀 더 구체적으로 알고싶은 부분인데요. redux에서는 오직 하나의 리듀서만이 존재합니다. 실무에서는 유지 보수를 위해 여러 개의 리듀서를 만든 뒤에 하나의 루트 리듀서로 병합하는데요. 실제로 어떤 모습을 가지고 있는지 살펴보죠.
아래와 같이 user, post 리듀서를 만듭니다.
function user(state = "minji", action) {
return state;
}
function post(state = "post1", action) {
return state;
}
리덕스의 combinedReducer api를 활용해서 리듀서들을 합칩니다.
const combined = combineReducer({ user, post });
이렇게 합쳐진 combined 이라는 루트 리듀서는 아래와 같은 모습을 가집니다.
function combined(state = {}, action) {
return {
user: user(state.user, action),
post: post(state.post, action)
};
}
즉, 우리가 dispatch를 통해 루트 리듀서를 호출하면 각각의 리듀서 함수가 실행되고 action이 존재하는 리듀서만이 새로운 상태를 반환하겠죠. 눈 여겨볼 점은 combined 함수의 첫번째 인자로 넘긴 state인데요. 현재 초기값이 {}
이기 때문에 각각의 리듀서 함수의 첫번째 인자로 넘겨진 state.a
와 state.b
는 undefined
입니다.
만약 초기 상태를 설정하고 싶다면 createStore api
에서 2번째 인자인 preloadedState
를 넘겨주면 됩니다.
사실 이러한 개념들을 이해하고 redux를 사용하는 것은 매우 쉽습니다. 그게 redux의 장점이죠. 하지만 좀 더 깊은 이해를 위해서 직접 구현해보도록 하겠습니다.
function createStore(reducer) {
let state;
const listeners = [];
const getState = () => {
return state;
};
const subscribe = (listener) => {
listeners.push(listener);
return function unsubscribe() { // 클로저가 활용되는 것을 확인할 수 있습니다.
const index = listeners.indexOf(listener);
listeners.splice(index, 1);
};
};
const dispatch = (action) => {
state = reducer(state, action);
listeners.forEach((listener) => listener());
};
return {
getState,
subscribe,
dispatch
};
}
굉장히 간단하게 구현한 것이지만 위의 코드를 쳐보는 것만으로도 redux가 어떤식으로 동작하는지 알 수 있었습니다.
리덕스의 리듀서 함수에는 몇 가지 규칙이 있다고 앞서 설명드렸는데요. 이 규칙에 의해 리듀서는 side effect를 발생시켜선 안됩니다.
side effect는 간략하게 함수의 외부에 존재하는 state 혹은 behavior에 변화를 주는 것을 말합니다. 링크를 참고하세요.
따라서 리듀서에 비동기 로직이 존재할 수 없죠. 하지만, 우리는 api 서버에 네트워크 요청을 보내서 데이터를 가져와야만 합니다. 리덕스에서는 이를 어떻게 처리할까요?
미들웨어를 사용해서 비동기 로직을 처리합니다. 아래는 리덕스 미들웨어 동작원리를 그림으로 나타낸 것입니다.
리덕스 미들웨어에는 대표적으로 두 가지 미들웨어가 있습니다. redux-thunk 그리고 redux-saga입니다. 오늘은 redux-thunk에 대해 알아보고 다음에 redux-saga를 정리해보겠습니다.
제가 이해한 redux-thunk는 결국 액션을 자바스크립트 객체뿐만 아니라 함수로 보내는 것입니다. 액션으로 함수를 보냈을때 이를 실행하는 것이죠. 직접 구현하면서 이를 이해해봅시다.
function createThunkMiddleware() {
return ({dispatch, getState}) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
next(action);
}
}
위의 함수에서 리턴되는 함수가 결국 thunk middleware함수입니다. 이 함수는 redux store에서 dispath, getState api를 받고, 다음 미들웨어로 액션을 보내는 next를 받습니다. 결국 마지막 함수로 우리가 dispatch할 액션을 인자로 받습니다.
우리가 함수를 dispatch하게 되면 action으로 받고 타입에 따라서 다르게 처리하는 것을 확인할 수 있습니다. 따라서 우리가 thunk middleware에 보낼 함수를 작성할때는 아래와 같이 작성합니다.
const getUser = (arg) => (dispatch, getState) => {
// 비동기 로직
getState(something);
dispatch(something);
}
dispatch할 때는 아래와 같이 함수를 실행해서 보내주면 되겠죠. 그럼 dispatch와 getState를 인자로 받는 함수 자체를 thunk middleware로 넘기게 됩니다.
dispatch(getUser(args));
배가 고파서 마지막에 글을 잘 못 쓴 것 같지만 핵심이 되는 동작원리만 잘 파악하면 api는 공식문서에서 찾아서 사용하면 됩니다. 다음에는 redux-saga 그리고 공식문서에서 권장하는 @reduxjs/toolkit에 대해서 알아보겠습니다.