양 쪽을 연결하여 데이터를 주고 받을 수 있도록 중간에서 매개 역할을 하는 소프트웨어 - Wikipedia
미들웨어가 뭐에요?
미들웨어
는 컴퓨터 공학에서 분야에 따라 다양하게 정의되는 용어입니다. 클라이언트-서버와 같은 웹 아키텍처에서 요청이 서버에 도달하기 전 먼저 받아와 특수한 비즈니스 로직을 처리하는 것도 미들웨어라고 불립니다. Node.js(Express) 로 서버를 개발해보신 분들은 미들웨어에 익숙하신 분들도 있을겁니다.
Redux 의 미들웨어는 Express 의 미들웨어와 형태는 다르지만, 기능적으로 유사한 특징을 갖고있습니다. Action
이 dispatch 되는 순간과 Reducer
에 도달하는 시점 사이에서 여러 비즈니스 로직을 수행할 수 있습니다.
아래 그림은 Redux 의 아키텍처 및 데이터의 흐름을 잘 보여주고 있습니다.
여기서 middleware 가 추가된 그림은 아래와 같은데요, Action
과 Reducer
사이에 Middleware
가 위치하고 있습니다.
미들웨어를 왜 써야 하나요?
Redux 공식 문서에서는 Redux 를 활용할 때 다음과 같이 세 가지 원칙을 지켜야 한다고 말하고 있습니다.
- 단 하나의 Store를 가져야 하고(
Single source of truth
) - 상태는 action 에 의해서만 변경되어야 하며(
State is read-only
) - 순수 함수에 의해서만 상태가 변경되어야 합니다(
Changes are made with pure functions
)
순수 함수는 동일한 입력에 대해서는 항상 같은 결과를 출력하는 함수인데요, 이러한 특징으로 인해서 순수함수는 그 결과값을 쉽게 예측할 수 있습니다. 즉, 동일한 Action
의 결과값은 예측 가능해야 하기 때문에 Redux 의 Reducer
는 반드시 순수 함수로 작성되어야 합니다. 만약 Reducer
가 “순수하지 않은”, 즉 Side-Effect
를 갖는 함수로 작성되면 어떻게 될까요?
이에 대한 답을 말씀드리기 전에 먼저 짚고 넘어가야 할 게 있습니다.
잠깐만요, 사이드 이펙트는 뭐에요?
React 로 개발을 하다보면 종종 사이드 이펙트(Side-Effect
)라는 용어를 듣게됩니다. AJAX call, DOM 조작 등이 포함된다고 하는데, 정확히 어떤 것을 지칭하는 용어일까요?
사실 사이드 이펙트는 React 에 한정됭 용어가 아닙니다. 함수의 특수한 기능을 지칭하는 일반적인 프로그래밍 용어입니다. 함수가 정의된 scope 밖에 있는 어떠한 것들을 수정한다면, 해당 함수는 사이드 이펙트를 갖는 함수라고 불립니다. 만약 특정 함수가 전역 변수를 수정하거나, 네트워크 요청을 보낸다면 사이드 이펙트를 발생시키는 것이죠.
그래서 Reducer가 사이드 이펙트를 만들면 어떻게 되는데요?
결론부터 말하자면, 동일한 Action
에 대한 결과값을 예측할 수 없게 됩니다. 이는 테스트를 매우 어렵게 만들죠. 단지 테스트가 어려운 것에서 끝나는 것이 아니라, 개발자가 자신이 작성한 코드의 결과를 예측할 수 없다는 점에서 프로그램에 치명적인 오류를 만들 수 있습니다.
지금부터는, Reducer 에서 사이드 이펙트를 발생시킬 때 어떤 문제가 생길 수 있는지 Redux 공식 Document 에 포함된 Todo 예제를 수정하면서 간단히 알아봅시다.
Todo Example
1. Redux 레포 클론
먼저 Redux 공식 git repository 를 클론해줍시다.
git clone https://github.com/reduxjs/redux.git
2. Todo 예제 실행
그런 다음 examples/todos
폴더로 이동해서 yarn start
명령을 실행해줍시다. 그러면 다음과 같은 Todo 앱이 나타날거에요.
이 예제는 Todo 를 추가하고, Todo 를 클릭해서 상태를 토글하며, 각 상태별 Todo 를 필터링하는 간단한 기능이 포함된 앱입니다.
3. Todo reducer 수정
우리는 다른 것 보다 reducer 안에서 사이드 이펙트를 일으키면 어떻게 되는지가 관심사 이므로 src/reducers/todo.js
파일을 열어 보도록 하겠습니다.
const todos = (state = [], action) => {
switch (action.type) {
case "ADD_TODO":
return [
...state,
{
id: action.id,
text: action.text,
completed: false,
},
];
case "TOGGLE_TODO":
// action 으로 받은 id 와 일치하는 todo의 completed 를 반전시키고
// 이를 담은 새로운 배열을 반환합니다
return state.map((todo) =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
);
default:
return state;
}
};
export default todos;
여기서 TOOGLE_TODO
액션을 핸들링할 때 사이드 이펙트를 만들어 보도록 하겠습니다. 지금은 Action 파라미터로 받은 todo
의 id
와 일치한 todo
의 completed
를 반전시키고 새로운 배열을 반환하고 있는데요, 저는 여기서 기존 상태를 mutate 하는 사이드 이펙트를 만들어 보도록 하겠습니다.
const todos = (state = [], action) => {
switch (action.type) {
case "ADD_TODO":
return [
...state,
{
id: action.id,
text: action.text,
completed: false,
},
];
case "TOGGLE_TODO":
// return state.map(todo =>
// todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
// )
// 참조로 전달받은 state 를 변경하는 사이드 이펙트 발생!
state.forEach((todo) =>
todo.id === action.id ? (todo.completed = !todo.completed) : todo
);
return state;
default:
return state;
}
};
export default todos;
저장한 뒤 다시 앱을 실행시키면, todo를 토글하여도 상태가 변하지 않는 것을 확인할 수 있습니다. 왜 이런 일이 나타나게 된 걸까요? 그 이유는 디버깅을 해보면 알 수 있습니다.
4. Debug todo app
Vscode 의 chrome debugger extension
을 설치한 뒤, switch
문에 break point 를 생성하고 F5 번을 누른 후 다시 todo 를 토글 해봅시다. 그러면 Debugger 가 해당 위치에 포커싱을 하게 되는데, 다음과 같이 나타나게 됩니다.
F8 키를 눌러서 step over 해보면, 다음과 같이 Redux 의 combination 함수의 context 에 들어오게 됩니다.
여기서 계속 step over 해보면, 다음과 같이 이전 상태와 현재 상태를 비교하는 로직에 다다르게 됩니다.
여기서 nextStateForKey
는 reducer(previousStateForKey, action)
의 리턴값으로, 특정 action
에 의해 만들어지는 다음 상태를 의미합니다. 다음 상태는 이전 상태와 동등 비교 연산(==
) 을 하게 되어서 변경이 이뤄지면 hasChanged
값이 true
로 변경되게 되는 것이죠. 문제는 여기서 객체의 비교 연산이 shallow
하게 이뤄지고 있다는 것입니다.
무슨 말인가 하면, todos
리듀서에서 TOGGLE_TODO
액션을 핸들링할 때 새로운 state
를 리턴하는 것이 아닌 기존 state
를 변경함으로써 state
는 여전히 같은 메모리 주소에 위치하게 되고, 객체 간의 동등 비교는 메모리 주소를 비교하는 것이기 때문에 Redux 는 이전 상태와 다음 상태가 일치하다고 판단을 하게 됩니다. 그 결과 화면에서 todo 를 토글하더라도 렌더링이 이뤄지지 않는 것이죠.
이처럼 reducer가 순수함수가 아닌 사이드 이펙트를 발생시키는 비순수 함수를 포함한다면 개발자가 예기치 못한 결과를 초래할 수 있습니다. 이번 Todo 예제에서 state
를 직접 수정해서 Redux core function 에 영향을 미치는 극단적인 예를 들었지만, reducer 를 순수함수로 작성해야하는 이유를 충분히 들 수 있었습니다.
Recap
자, 지금까지 Redux의 미들웨어가 무엇인지, 왜 필요한지, 사이드 이펙트는 무엇인지, 그리고 직접 reducer 안에서 사이드 이펙트를 발생시키면서 순수하지 않은 함수가 포함된 reducer 를 사용하는것이 얼마나 위험한 일인지 알아보았습니다.
Redux 미들웨어는 이러한 사이드 이펙트를 효과적으로 처리하기 위해 만들어지게 된 것입니다. 순수하지 않은 로직은 미들웨어가 처리하고, 오직 순수한 로직만을 reducer 가 관장하도록 하여 클린한 상태 트리를 유지할 수 있도록 만들어주는 것이죠. 다음 포스팅에서는 Redux Middleware 를 어떻게 사용하는지 알아보도록 하겠습니다.