Redux 기초, 파트 4: 스토어
이 페이지는 PageTurner AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다. 오류를 발견하셨나요? 문제 신고 →
- Redux 스토어 생성 방법
- 상태 업데이트 및 업데이트 감지를 위해 스토어 사용 방법
- 스토어 확장 기능 구성 방법
- 앱 디버깅을 위한 Redux DevTools Extension 설정 방법
소개
파트 3: 상태, 액션, 리듀서에서는 예제 할 일 앱 작성을 시작했습니다. 앱 작동에 필요한 비즈니스 요구사항을 나열하고 상태 구조를 정의했으며, 사용자가 앱과 상호작용할 때 발생할 수 있는 이벤트 유형과 일치하는 "무슨 일이 발생했는지"를 설명하는 일련의 액션 타입을 생성했습니다. 또한 state.todos 및 state.filters 섹션 업데이트를 처리할 수 있는 리듀서 함수를 작성하고, 앱의 각 기능에 대한 "슬라이스 리듀서"를 기반으로 "루트 리듀서"를 생성하기 위해 Redux combineReducers 함수를 사용하는 방법을 확인했습니다.
이제 Redux 앱의 핵심인 스토어를 통해 이러한 조각들을 통합할 차례입니다.
이 페이지는 PageTurner AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다. 오류를 발견하셨나요? 문제 신고 →
참고: 이 튜토리얼은 Redux의 원리와 개념을 설명하기 위해 의도적으로 오래된 스타일의 Redux 로직 패턴을 보여줍니다. 이 패턴은 오늘날 우리가 Redux 앱 구축을 위한 올바른 접근법으로 가르치는 '현대적인 Redux(modern Redux)' 패턴(Redux Toolkit 사용)보다 더 많은 코드가 필요합니다. 이 튜토리얼은 프로덕션 환경에서 바로 사용할 수 있는 프로젝트가 아닙니다.
'현대적인 Redux'와 Redux Toolkit 사용법을 배우려면 다음 페이지를 참조하세요:
- 전체 "Redux Essentials" 튜토리얼: 실제 애플리케이션을 위한 Redux Toolkit을 사용한 "올바른 Redux 사용법"을 가르칩니다. 모든 Redux 학습자는 'Essentials' 튜토리얼을 필독할 것을 권장합니다!
- Redux Fundamentals, Part 8: Redux Toolkit을 사용한 현대적인 Redux: 이전 섹션의 저수준 예제를 현대적인 Redux Toolkit 방식으로 변환하는 방법을 보여줍니다
Redux 스토어
Redux 스토어는 앱을 구성하는 상태, 액션, 리듀서를 하나로 통합합니다. 스토어는 다음과 같은 여러 책임을 가집니다:
-
현재 애플리케이션 상태를 내부에 보관
-
store.getState()를 통해 현재 상태에 접근 허용 -
store.dispatch(action)를 통해 상태 업데이트 허용 -
store.subscribe(listener)를 통해 리스너 콜백 등록 -
store.subscribe(listener)에서 반환된unsubscribe함수를 통해 리스너 등록 해제 처리
Redux 애플리케이션에는 단일 스토어만 존재한다는 점이 중요합니다. 데이터 처리 로직을 분할하려면 별도의 스토어를 생성하는 대신 리듀서 구성을 사용하고 결합 가능한 여러 리듀서를 생성하게 됩니다.
스토어 생성하기
모든 Redux 스토어는 단일 루트 리듀서 함수를 가집니다. 이전 섹션에서 combineReducers를 사용해 루트 리듀서 함수를 생성했습니다. 이 루트 리듀서는 예제 앱의 src/reducer.js에 정의되어 있습니다. 이 루트 리듀서를 가져와 첫 번째 스토어를 생성해 보겠습니다.
Redux 코어 라이브러리는 스토어를 생성할 수 있는 createStore API를 제공합니다. store.js라는 새 파일을 추가하고 createStore와 루트 리듀서를 가져옵니다. 그런 다음 createStore를 호출하고 루트 리듀서를 전달하세요:
import { createStore } from 'redux'
import rootReducer from './reducer'
const store = createStore(rootReducer)
export default store
초기 상태 로딩
createStore는 두 번째 인수로 preloadedState 값을 받을 수도 있습니다. 이를 사용하면 서버에서 전송된 HTML 페이지에 포함된 값이나 사용자가 페이지를 다시 방문할 때 읽어오는 localStorage에 지속된 값처럼 스토어 생성 시 초기 데이터를 추가할 수 있습니다:
import { createStore } from 'redux'
import rootReducer from './reducer'
let preloadedState
const persistedTodosString = localStorage.getItem('todos')
if (persistedTodosString) {
preloadedState = {
todos: JSON.parse(persistedTodosString)
}
}
const store = createStore(rootReducer, preloadedState)
액션 디스패치하기
이제 스토어를 생성했으니 프로그램이 작동하는지 확인해 봅시다! UI가 없어도 업데이트 로직을 테스트할 수 있습니다.
이 코드를 실행하기 전에 src/features/todos/todosSlice.js로 돌아가 initialState에서 예제 할 일 객체를 모두 제거하여 빈 배열로 만들어 보세요. 그러면 이 예제의 출력 결과를 더 쉽게 읽을 수 있습니다.
// Omit existing React imports
import store from './store'
// Log the initial state
console.log('Initial state: ', store.getState())
// {todos: [....], filters: {status, colors}}
// Every time the state changes, log it
// Note that subscribe() returns a function for unregistering the listener
const unsubscribe = store.subscribe(() =>
console.log('State after dispatch: ', store.getState())
)
// Now, dispatch some actions
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' })
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about reducers' })
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about stores' })
store.dispatch({ type: 'todos/todoToggled', payload: 0 })
store.dispatch({ type: 'todos/todoToggled', payload: 1 })
store.dispatch({ type: 'filters/statusFilterChanged', payload: 'Active' })
store.dispatch({
type: 'filters/colorFilterChanged',
payload: { color: 'red', changeType: 'added' }
})
// Stop listening to state updates
unsubscribe()
// Dispatch one more action to see what happens
store.dispatch({ type: 'todos/todoAdded', payload: 'Try creating a store' })
// Omit existing React rendering logic
기억하세요, store.dispatch(action)을 호출할 때마다:
-
스토어는
rootReducer(state, action)를 호출합니다- 해당 루트 리듀서는 내부에서
todosReducer(state.todos, action)와 같은 다른 슬라이스 리듀서들을 호출할 수 있습니다
- 해당 루트 리듀서는 내부에서
-
스토어는 내부에 새로운 상태(state) 값을 저장합니다
-
스토어는 모든 리스너 구독 콜백을 호출합니다
-
리스너가
store에 접근할 수 있다면,store.getState()를 호출해 최신 상태 값을 읽을 수 있습니다
이전 예제의 콘솔 로그 출력을 살펴보면, 각 액션이 디스패치될 때마다 Redux 상태가 어떻게 변경되는지 확인할 수 있습니다:

마지막 액션에서는 앱이 아무것도 로그로 출력하지 않았습니다.
이는 unsubscribe()를 호출해 리스너 콜백을 제거했기 때문으로,
액션 디스패치 이후에는 아무 작업도 실행되지 않았습니다.
UI 작성을 시작하기 전에 이미 앱의 동작을 명시했습니다. 이는 앱이 의도한 대로 작동할 것이라는 확신을 주는 데 도움이 됩니다.
원한다면 이제 리듀서에 대한 테스트 작성을 시도해볼 수 있습니다.
리듀서는 순수 함수이므로 테스트가 간편합니다.
예제 state와 action으로 호출한 후 결과가 예상과 일치하는지 확인하세요:
import todosReducer from './todosSlice'
test('Toggles a todo based on id', () => {
const initialState = [{ id: 0, text: 'Test text', completed: false }]
const action = { type: 'todos/todoToggled', payload: 0 }
const result = todosReducer(initialState, action)
expect(result[0].completed).toBe(true)
})
Redux 스토어 내부
Redux 스토어의 작동 방식을 이해하기 위해 내부를 살짝 들여다보는 것이 도움이 될 수 있습니다. 다음은 약 25줄의 코드로 작성된 실제 작동하는 Redux 스토어의 축소판 예제입니다:
function createStore(reducer, preloadedState) {
let state = preloadedState
const listeners = []
function getState() {
return state
}
function subscribe(listener) {
listeners.push(listener)
return function unsubscribe() {
const index = listeners.indexOf(listener)
listeners.splice(index, 1)
}
}
function dispatch(action) {
state = reducer(state, action)
listeners.forEach(listener => listener())
}
dispatch({ type: '@@redux/INIT' })
return { dispatch, subscribe, getState }
}
이 소규모 Redux 스토어는 지금까지 앱에서 사용해온 실제 Redux createStore 함수를
대체할 수 있을 만큼 충분히 잘 작동합니다.
(직접 시험해 보세요!) 실제 Redux 스토어 구현은 더 길고 약간 더 복잡하지만,
대부분 주석, 경고 메시지 및 일부 엣지 케이스 처리입니다.
보시다시피, 여기서 실제 로직은 상당히 간결합니다:
-
스토어 내부에 현재
state값과reducer함수가 존재합니다 -
getState는 현재 상태 값을 반환합니다 -
subscribe는 리스너 콜백 배열을 유지하고 새 콜백을 제거하는 함수를 반환합니다 -
dispatch는 리듀서를 호출하고 상태를 저장한 후 리스너들을 실행합니다 -
스토어는 시작 시 한 번의 액션을 디스패치해 리듀서들을 해당 상태로 초기화합니다
-
스토어 API는
{dispatch, subscribe, getState}로 구성된 객체입니다
특히 다음 사항을 강조하고 싶습니다:
getState는 현재 state 값 그 자체를 반환할 뿐입니다.
이는 기본적으로 현재 상태 값을 실수로 변이(mutate)하는 것을 아무것도 막지 않는다는 의미입니다!
다음 코드는 오류 없이 실행되지만 잘못된 방법입니다:
const state = store.getState()
// ❌ Don't do this - it mutates the current state!
state.filters.status = 'Active'
다시 말해:
-
Redux 스토어는
getState()를 호출할 때state값의 추가 복사본을 만들지 않습니다. 루트 리듀서 함수에서 반환된 것과 정확히 동일한 참조입니다 -
Redux 스토어는 우발적 변이를 방지하기 위한 다른 조치를 취하지 않습니다. 리듀서 내부 또는 스토어 외부에서 상태를 변이하는 것이 가능하므로, 항상 변이를 피하기 위해 주의해야 합니다.
실수로 발생하는 변이(mutation)의 흔한 원인은 배열 정렬입니다. array.sort()를 호출하면 실제로 기존 배열이 변이됩니다. 만약 const sortedTodos = state.todos.sort()를 호출한다면, 의도치 않게 실제 스토어 상태를 변이시키게 됩니다.
8부: 모던 리덕스(Modern Redux)에서 리덕스 툴킷(Redux Toolkit)이 리듀서 내 변이를 방어하고 리듀서 외부의 실수로 인한 변이를 감지해 경고하는 방법을 살펴보겠습니다.
스토어 구성하기
이미 createStore에 rootReducer와 preloadedState 인자를 전달할 수 있음을 확인했습니다. 하지만 createStore는 스토어의 기능을 커스터마이징하고 새로운 능력을 부여하는 데 사용되는 추가 인자도 받을 수 있습니다.
리덕스 스토어는 스토어 인핸서(store enhancer) 라는 것을 사용해 커스터마이즈됩니다. 스토어 인핸서는 원본 리덕스 스토어를 감싸는 추가 레이어를 도입하는 createStore의 특별한 버전과 같습니다. 인핸스드 스토어(enhanced store)는 원본 대신 자체 버전의 dispatch, getState, subscribe 함수를 제공해 스토어의 동작 방식을 변경할 수 있습니다.
이 튜토리얼에서는 스토어 인핸서의 실제 작동 방식에 대한 세부 사항보다는 사용법에 집중하겠습니다.
인핸서로 스토어 생성하기
프로젝트의 src/exampleAddons/enhancers.js 파일에는 두 가지 간단한 예제 스토어 인핸서가 포함되어 있습니다:
-
sayHiOnDispatch: 액션이 디스패치될 때마다 항상 콘솔에'Hi'!를 출력하는 인핸서 -
includeMeaningOfLife:getState()가 반환하는 값에 항상meaningOfLife: 42필드를 추가하는 인핸서
먼저 sayHiOnDispatch를 사용해보겠습니다. 먼저 이를 임포트한 후 createStore에 전달합니다:
import { createStore } from 'redux'
import rootReducer from './reducer'
import { sayHiOnDispatch } from './exampleAddons/enhancers'
const store = createStore(rootReducer, undefined, sayHiOnDispatch)
export default store
여기서는 preloadedState 값이 없으므로 대신 두 번째 인자로 undefined를 전달합니다.
다음으로 액션을 디스패치해 보겠습니다:
import store from './store'
console.log('Dispatching action')
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' })
console.log('Dispatch complete')
이제 콘솔을 확인하세요. 다른 두 로그 출력 사이에 'Hi!'가 기록된 것을 볼 수 있을 것입니다:
sayHiOnDispatch 인핸서는 원본 store.dispatch 함수를 자체 특화된 dispatch 버전으로 래핑했습니다. store.dispatch()를 호출하면 실제로는 sayHiOnDispatch의 래퍼 함수가 호출되어 원본 함수를 호출한 후 'Hi'를 출력합니다.
이제 두 번째 인핸서를 추가해보겠습니다. 같은 파일에서 includeMeaningOfLife를 임포트할 수 있지만 문제가 있습니다. createStore는 세 번째 인자로 단 하나의 인핸서만 허용합니다! 어떻게 하면 두 개의 인핸서를 동시에 전달할 수 있을까요?
실제로 필요한 것은 sayHiOnDispatch 인핸서와 includeMeaningOfLife 인핸서를 단일 통합 인핸서로 병합한 후 이를 전달하는 방법입니다.
다행히 리덕스 코어에는 compose 함수가 포함되어 있어 여러 인핸서를 병합하는 데 사용할 수 있습니다. 이를 사용해 보겠습니다:
import { createStore, compose } from 'redux'
import rootReducer from './reducer'
import {
sayHiOnDispatch,
includeMeaningOfLife
} from './exampleAddons/enhancers'
const composedEnhancer = compose(sayHiOnDispatch, includeMeaningOfLife)
const store = createStore(rootReducer, undefined, composedEnhancer)
export default store
이제 스토어를 사용할 때 어떤 일이 발생하는지 확인할 수 있습니다:
import store from './store'
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' })
// log: 'Hi!'
console.log('State after dispatch: ', store.getState())
// log: {todos: [...], filters: {status, colors}, meaningOfLife: 42}
로그 출력 결과는 다음과 같습니다:

이제 두 인핸서가 동시에 스토어의 동작을 수정하고 있음을 확인할 수 있습니다. sayHiOnDispatch는 dispatch의 작동 방식을 변경했고, includeMeaningOfLife는 getState의 작동 방식을 변경했습니다.
스토어 인핸서(Store enhancer)는 스토어를 수정하는 매우 강력한 방법이며, 거의 모든 Redux 앱은 스토어 설정 시 최소 하나의 인핸서를 포함합니다.
전달할 preloadedState가 없다면, enhancer를 두 번째 인자로 직접 전달할 수 있습니다:
const store = createStore(rootReducer, storeEnhancer)
미들웨어(Middleware)
인핸서는 스토어의 모든 메서드(dispatch, getState, subscribe)를 재정의하거나 교체할 수 있기 때문에 매우 강력합니다.
하지만 대부분의 경우 dispatch 동작만 커스터마이징하면 충분합니다. dispatch가 실행될 때 사용자 정의 동작을 추가할 수 있는 방법이 있다면 유용할 것입니다.
Redux는 미들웨어(middleware) 라는 특별한 애드온을 사용해 dispatch 함수를 커스터마이징합니다.
Express나 Koa 같은 라이브러리를 사용해본 적이 있다면, 동작을 커스터마이징하기 위해 미들웨어를 추가하는 개념에 이미 익숙할 수 있습니다. 이러한 프레임워크에서 미들웨어는 프레임워크가 요청을 수신한 후 응답을 생성하기 전에 배치할 수 있는 코드입니다. 예를 들어 Express나 Koa 미들웨어는 CORS 헤더 추가, 로깅, 압축 등을 수행할 수 있습니다. 미들웨어의 가장 큰 장점은 체인으로 조합 가능하다는 점입니다. 하나의 프로젝트에서 여러 개의 독립적인 서드파티 미들웨어를 사용할 수 있습니다.
Redux 미들웨어는 Express나 Koa 미들웨어와 다른 문제를 해결하지만, 개념적으로 유사한 방식으로 동작합니다. Redux 미들웨어는 액션을 디스패치하는 순간과 리듀서에 도달하는 순간 사이에 서드파티 확장 포인트를 제공합니다. 개발자들은 로깅, 크래시 리포트, 비동기 API 통신, 라우팅 등에 Redux 미들웨어를 사용합니다.
먼저 스토어에 미들웨어를 추가하는 방법을 살펴본 후, 직접 미들웨어를 작성하는 방법을 설명하겠습니다.
미들웨어 사용하기
스토어 인핸서를 사용해 Redux 스토어를 커스터마이징할 수 있다는 것을 이미 확인했습니다. Redux 미들웨어는 실제로 Redux에 내장된 매우 특별한 스토어 인핸서인 applyMiddleware 위에서 구현됩니다.
스토어에 인핸서를 추가하는 방법을 이미 알고 있으므로, 지금 바로 적용할 수 있습니다. applyMiddleware 단독으로 시작하고, 이 프로젝트에 포함된 세 가지 예제 미들웨어를 추가해 보겠습니다.
import { createStore, applyMiddleware } from 'redux'
import rootReducer from './reducer'
import { print1, print2, print3 } from './exampleAddons/middleware'
const middlewareEnhancer = applyMiddleware(print1, print2, print3)
// Pass enhancer as the second arg, since there's no preloadedState
const store = createStore(rootReducer, middlewareEnhancer)
export default store
이름에서 알 수 있듯이, 각 미들웨어는 액션이 디스패치될 때 숫자를 출력합니다.
이제 디스패치하면 어떤 일이 발생할까요?
import store from './store'
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' })
// log: '1'
// log: '2'
// log: '3'
콘솔에서 다음과 같은 출력을 확인할 수 있습니다:
어떻게 이런 결과가 나올까요?
미들웨어는 스토어의 dispatch 메서드를 둘러싼 파이프라인을 형성합니다. store.dispatch(action)을 호출하면 실제로는 파이프라인의 첫 번째 미들웨어를 호출하는 것입니다. 미들웨어는 액션을 확인한 후 원하는 모든 작업을 수행할 수 있습니다. 일반적으로 미들웨어는 리듀서와 마찬가지로 특정 타입의 액션인지 확인합니다. 해당 타입이 맞다면 미들웨어는 사용자 정의 로직을 실행할 수 있습니다. 그렇지 않으면 다음 미들웨어로 액션을 전달합니다.
리듀서와 달리 미들웨어는 타임아웃이나 기타 비동기 로직을 포함한 사이드 이펙트를 가질 수 있습니다.
이 경우 액션은 다음과 같이 전달됩니다:
-
print1미들웨어 (store.dispatch로 표시됨) -
print2미들웨어 -
print3미들웨어 -
원본
store.dispatch -
store내부의 루트 리듀서
이것들은 모두 함수 호출이므로 호출 스택에서 반환됩니다. 따라서 print1 미들웨어가 가장 먼저 실행되고 가장 마지막에 완료됩니다.
커스텀 미들웨어 작성하기
또한 직접 미들웨어를 작성할 수도 있습니다. 항상 필요한 것은 아니지만, 커스텀 미들웨어는 Redux 애플리케이션에 특정 동작을 추가하는 훌륭한 방법입니다.
Redux 미들웨어는 세 개의 중첩된 함수로 작성됩니다. 이 패턴을 살펴보겠습니다. 동작 방식을 더 명확히 이해하기 위해 function 키워드를 사용해 작성해 보겠습니다:
// Middleware written as ES5 functions
// Outer function:
function exampleMiddleware(storeAPI) {
return function wrapDispatch(next) {
return function handleAction(action) {
// Do anything here: pass the action onwards with next(action),
// or restart the pipeline with storeAPI.dispatch(action)
// Can also use storeAPI.getState() here
return next(action)
}
}
}
이 세 함수의 역할과 인자를 분석해 보겠습니다.
-
exampleMiddleware: 가장 바깥쪽 함수가 실제 "미들웨어"입니다.applyMiddleware에 의해 호출되며 스토어의{dispatch, getState}함수를 포함한storeAPI객체를 받습니다. 이는 실제 스토어의dispatch및getState와 동일합니다. 이dispatch함수를 호출하면 액션이 미들웨어 파이프라인의 _시작점_으로 전달됩니다. 단 한 번만 호출됩니다. -
wrapDispatch: 중간 함수는 인자로next라는 함수를 받습니다. 이 함수는 실제로 파이프라인의 _다음 미들웨어_입니다. 이 미들웨어가 마지막이라면next는 원본store.dispatch함수가 됩니다.next(action)을 호출하면 액션이 파이프라인의 다음 미들웨어로 전달됩니다. 이 역시 단 한 번만 호출됩니다. -
handleAction: 가장 안쪽 함수는 현재action을 인자로 받으며, 액션이 디스패치될 때마다 호출됩니다.
이 미들웨어 함수들에 어떤 이름이든 붙일 수 있지만, 각각의 역할을 기억하기 위해 다음 이름을 사용하는 것이 도움이 될 수 있습니다:
- 바깥쪽:
someCustomMiddleware(또는 여러분의 미들웨어 이름) - 중간:
wrapDispatch - 안쪽:
handleAction
이들은 일반 함수이므로 ES2015 화살표 함수로도 작성할 수 있습니다. 화살표 함수는 return 문이 필요 없어 코드를 더 짧게 만들 수 있지만, 화살표 함수와 암시적 반환에 익숙하지 않다면 가독성이 떨어질 수 있습니다.
위 예제를 화살표 함수로 동일하게 작성한 모습:
const anotherExampleMiddleware = storeAPI => next => action => {
// Do something in here, when each action is dispatched
return next(action)
}
세 함수를 여전히 중첩하고 각 함수를 반환하지만, 암시적 반환으로 더 간결해졌습니다.
첫 번째 커스텀 미들웨어
애플리케이션에 로깅 기능을 추가한다고 가정해 보겠습니다. 액션이 디스패치될 때마다 콘솔에 액션 내용을 출력하고, 리듀서가 액션을 처리한 후의 상태를 확인하고 싶습니다.
이 예제 미들웨어들은 실제 할 일 앱의 특정 부분은 아니지만, 프로젝트에 추가하여 사용 시 어떤 일이 발생하는지 확인해 볼 수 있습니다.
해당 정보를 콘솔에 출력하는 간단한 미들웨어를 작성할 수 있습니다:
const loggerMiddleware = storeAPI => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', storeAPI.getState())
return result
}
액션이 디스패치될 때마다:
-
handleAction함수의 첫 부분이 실행되어'dispatching'출력 -
액션을
next섹션으로 전달 (이는 다른 미들웨어 또는 실제store.dispatch일 수 있음) -
결국 리듀서가 실행되어 상태가 업데이트되고
next함수 반환 -
storeAPI.getState()를 호출하여 새 상태 확인 가능 -
next미들웨어에서 반환된result값을 최종적으로 반환
모든 미들웨어는 어떤 값이든 반환할 수 있으며, 파이프라인의 첫 번째 미들웨어 반환 값이 실제 store.dispatch() 호출 시 반환됩니다. 예시:
const alwaysReturnHelloMiddleware = storeAPI => next => action => {
const originalResult = next(action)
// Ignore the original result, return something else
return 'Hello!'
}
const middlewareEnhancer = applyMiddleware(alwaysReturnHelloMiddleware)
const store = createStore(rootReducer, middlewareEnhancer)
const dispatchResult = store.dispatch({ type: 'some/action' })
console.log(dispatchResult)
// log: 'Hello!'
다른 예제를 살펴보겠습니다. 미들웨어는 종종 특정 액션을 감지한 후 해당 액션이 디스패치될 때 작업을 수행합니다. 또한 미들웨어 내부에서 비동기 로직을 실행할 수 있습니다. 특정 액션을 감지하면 지연 후 내용을 출력하는 미들웨어를 작성해 보겠습니다:
const delayedMessageMiddleware = storeAPI => next => action => {
if (action.type === 'todos/todoAdded') {
setTimeout(() => {
console.log('Added a new todo: ', action.payload)
}, 1000)
}
return next(action)
}
이 미들웨어는 "todo added" 액션을 감지합니다. 해당 액션을 발견할 때마다 1초 타이머를 설정한 후 액션의 페이로드를 콘솔에 출력합니다.
미들웨어 사용 사례
그렇다면 미들웨어로 무엇을 할 수 있을까요? 정말 다양한 일들이 가능합니다!
미들웨어는 디스패치된 액션을 확인했을 때 원하는 어떤 작업이든 수행할 수 있습니다:
-
콘솔에 로그 기록하기
-
타임아웃 설정하기
-
비동기 API 호출하기
-
액션 수정하기
-
액션 일시 중지하거나 완전히 차단하기
그 외에도 상상할 수 있는 모든 작업이 가능합니다.
특히 미들웨어는 사이드 이펙트가 있는 로직을 처리하도록 설계되었습니다. 또한 미들웨어는 dispatch를 수정하여 평범한 액션 객체가 아닌 것들도 처리할 수 있게 합니다. 이 두 가지 주제는 6부: 비동기 로직에서 더 자세히 다루겠습니다.
Redux DevTools
마지막으로 스토어 설정과 관련해 다뤄야 할 아주 중요한 주제가 하나 더 있습니다.
Redux는 상태가 언제, 어디서, 왜, 어떻게 변경되었는지 쉽게 이해할 수 있도록 특별히 설계되었습니다. 이를 위해 Redux는 Redux DevTools 사용을 지원합니다. 이 확장 기능은 어떤 액션이 디스패치되었는지, 해당 액션에 무엇이 포함되었는지, 그리고 각 액션 디스패치 후 상태가 어떻게 변했는지에 대한 기록을 보여줍니다.
Redux DevTools UI는 Chrome과 Firefox용 브라우저 확장 프로그램으로 제공됩니다. 아직 설치하지 않으셨다면 지금 바로 추가해 보세요.
설치가 완료되면 브라우저의 개발자 도구 창을 열어보세요. 이제 새로운 "Redux" 탭이 보일 것입니다. 아직은 아무 기능도 작동하지 않습니다. 먼저 Redux 스토어와 연결해주어야 합니다.
스토어에 DevTools 추가하기
확장 프로그램 설치 후에는 DevTools가 내부 동작을 확인할 수 있도록 스토어를 설정해야 합니다. 이를 위해 특별한 스토어 인핸서를 추가해야 합니다.
Redux DevTools Extension 문서에 스토어 설정 방법이 안내되어 있지만, 단계가 다소 복잡합니다. 다행히 redux-devtools-extension이라는 NPM 패키지가 이 복잡한 과정을 간소화해줍니다. 이 패키지는 Redux의 기본 compose 함수 대신 사용할 수 있는 전용 composeWithDevTools 함수를 제공합니다.
구현 방법은 다음과 같습니다:
import { createStore, applyMiddleware } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import rootReducer from './reducer'
import { print1, print2, print3 } from './exampleAddons/middleware'
const composedEnhancer = composeWithDevTools(
// EXAMPLE: Add whatever middleware you actually want to use here
applyMiddleware(print1, print2, print3)
// other store enhancers if any
)
const store = createStore(rootReducer, composedEnhancer)
export default store
스토어를 임포트한 후에도 index.js가 여전히 액션을 디스패치하는지 확인하세요. 이제 브라우저 개발자 도구에서 Redux DevTools 탭을 열면 다음과 같은 화면이 보일 것입니다:

왼쪽에는 디스패치된 액션 목록이 표시됩니다. 하나를 클릭하면 오른쪽 패널에 여러 탭이 나타납니다:
-
해당 액션 객체의 내용
-
리듀서 실행 후의 전체 Redux 상태
-
이전 상태와 현재 상태의 차이점
-
활성화된 경우,
store.dispatch()를 처음 호출한 코드 위치까지의 함수 호출 스택
다음은 "할 일 추가" 액션을 디스패치한 후 "State" 탭과 "Diff" 탭의 모습입니다:


이 도구들은 앱을 디버깅하고 내부에서 정확히 무슨 일이 일어나는지 이해하는 데 매우 강력한 기능을 제공합니다.
학습 내용 요약
지금까지 살펴본 것처럼 스토어(store)는 모든 Redux 애플리케이션의 핵심입니다. 스토어는 상태를 보유하고 리듀서를 실행하여 액션을 처리하며, 추가 기능을 구현하기 위해 커스터마이즈될 수 있습니다.
이제 예제 애플리케이션이 어떻게 구성되는지 살펴봅시다:
다시 한번 정리해보면, 이번 섹션에서 다룬 내용은 다음과 같습니다:
- Redux 앱은 항상 단일 스토어를 가집니다
- 스토어는 Redux
createStoreAPI로 생성됩니다 - 모든 스토어는 단일 루트 리듀서 함수를 가집니다
- 스토어는 Redux
- 스토어의 세 가지 주요 메서드
getState: 현재 상태를 반환합니다dispatch: 상태 업데이트를 위해 리듀서에 액션을 전달합니다subscribe: 액션이 디스패치될 때마다 실행될 리스너 콜백을 등록합니다
- 스토어 인핸서(enhancer)로 생성 시 커스터마이징 가능
- 인핸서는 스토어를 감싸 메서드를 오버라이드할 수 있습니다
createStore는 하나의 인핸서를 인자로 받습니다- 여러 인핸서는
composeAPI로 병합할 수 있습니다
- 미들웨어가 스토어 커스터마이징의 주요 방법
- 미들웨어는
applyMiddleware인핸서로 추가됩니다 - 미들웨어는 서로 중첩된 세 개의 함수로 작성됩니다
- 액션이 디스패치될 때마다 미들웨어가 실행됩니다
- 미들웨어 내부에서 사이드 이펙트를 처리할 수 있습니다
- 미들웨어는
- Redux DevTools로 시간에 따른 앱 변화 확인
- DevTools 확장을 브라우저에 설치할 수 있습니다
- 스토어에
composeWithDevTools로 DevTools 인핸서를 추가해야 합니다 - DevTools는 디스패치된 액션과 시간별 상태 변화를 보여줍니다
다음 단계
이제 리듀서를 실행하고 액션을 디스패치할 때 상태를 업데이트할 수 있는 동작하는 Redux 스토어를 갖추게 되었습니다.
그러나 모든 애플리케이션은 데이터를 표시하고 사용자가 유용한 작업을 수행할 수 있는 사용자 인터페이스가 필요합니다. 파트 5: UI와 React에서는 스토어가 UI와 어떻게 연동되는지, 특히 Redux가 React와 함께 어떻게 작동하는지 살펴보겠습니다.