본문으로 건너뛰기
비공식 베타 번역

이 페이지는 PageTurner AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다. 오류를 발견하셨나요? 문제 신고 →

스토어 설정하기

"Redux 핵심" 튜토리얼에서는 Todo 리스트 예제 앱을 만들며 Redux의 핵심 개념을 소개했습니다. 그 과정에서 Redux 스토어 생성 및 설정 방법도 다뤘습니다.

이제 추가 기능을 구현하기 위해 스토어를 커스터마이징하는 방법을 살펴보겠습니다. "Redux 핵심" 파트 5: UI 및 React의 소스 코드를 시작점으로 사용할 것입니다. 튜토리얼 이 단계의 소스 코드는 Github 예제 앱 저장소 또는 CodeSandbox 브라우저 환경에서 확인할 수 있습니다.

스토어 생성하기

먼저 스토어를 생성한 원본 index.js 파일을 살펴봅시다:

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import rootReducer from './reducers'
import App from './components/App'

const store = createStore(rootReducer)

render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)

이 코드에서는 리듀서를 Redux createStore 함수에 전달하여 store 객체를 반환받습니다. 이 객체를 react-reduxProvider 컴포넌트에 전달하면 컴포넌트 트리의 최상위에 렌더링됩니다.

이를 통해 react-reduxconnect를 통해 앱에서 Redux에 연결할 때마다 컴포넌트가 스토어를 사용할 수 있게 됩니다.

Redux 기능 확장하기

대부분의 앱은 미들웨어나 스토어 인핸서를 추가하여 Redux 스토어 기능을 확장합니다 (참고: 미들웨어는 흔하지만 인핸서는 덜 일반적입니다). 미들웨어는 Redux dispatch 함수에 추가 기능을 제공하고, 인핸서는 Redux 스토어 자체에 추가 기능을 제공합니다.

두 가지 미들웨어와 하나의 인핸서를 추가해 보겠습니다:

  • redux-thunk 미들웨어: 디스패치의 간단한 비동기 사용을 가능하게 합니다.

  • 디스패치된 액션과 결과로 생성된 새 상태를 기록하는 미들웨어

  • 리듀서가 각 액션을 처리하는 데 걸린 시간을 기록하는 인핸서

redux-thunk 설치하기

npm install redux-thunk

middleware/logger.js

const logger = store => next => action => {
console.group(action.type)
console.info('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
console.groupEnd()
return result
}

export default logger

enhancers/monitorReducer.js

const round = number => Math.round(number * 100) / 100

const monitorReducerEnhancer =
createStore => (reducer, initialState, enhancer) => {
const monitoredReducer = (state, action) => {
const start = performance.now()
const newState = reducer(state, action)
const end = performance.now()
const diff = round(end - start)

console.log('reducer process time:', diff)

return newState
}

return createStore(monitoredReducer, initialState, enhancer)
}

export default monitorReducerEnhancer

이제 기존 index.js에 추가해 봅시다.

  • 먼저 redux-thunk와 우리가 만든 loggerMiddleware, monitorReducerEnhancer를 임포트해야 합니다. 추가로 Redux가 제공하는 applyMiddlewarecompose 함수도 임포트합니다.

  • applyMiddleware를 사용해 loggerMiddlewarethunk 미들웨어를 스토어의 디스패치 함수에 적용하는 스토어 인핸서를 생성합니다.

  • 다음으로 compose를 사용해 새로 만든 middlewareEnhancermonitorReducerEnhancer를 하나의 함수로 조합합니다.

    createStore에는 하나의 인핸서만 전달할 수 있기 때문입니다. 여러 인핸서를 사용하려면 이 예시처럼 먼저 하나의 더 큰 인핸서로 조합해야 합니다.

  • 마지막으로 이 새로운 composedEnhancers 함수를 createStore의 세 번째 인자로 전달합니다. 참고: 두 번째 인자는 스토어에 초기 상태를 미리 로드할 때 사용하며 여기서는 다루지 않습니다.

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { applyMiddleware, createStore, compose } from 'redux'
import { thunk } from 'redux-thunk'
import rootReducer from './reducers'
import loggerMiddleware from './middleware/logger'
import monitorReducerEnhancer from './enhancers/monitorReducer'
import App from './components/App'

const middlewareEnhancer = applyMiddleware(loggerMiddleware, thunk)
const composedEnhancers = compose(middlewareEnhancer, monitorReducerEnhancer)

const store = createStore(rootReducer, undefined, composedEnhancers)

render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)

이 접근 방식의 문제점

이 코드는 동작하지만 일반적인 앱에는 이상적이지 않습니다.

대부분의 앱은 여러 미들웨어를 사용하며, 각 미들웨어는 초기 설정이 필요한 경우가 많습니다. index.js에 추가되는 복잡성은 로직이 깔끔하게 정리되지 않아 유지보수를 어렵게 만들 수 있습니다.

해결책: configureStore

이 문제에 대한 해결책은 스토어 생성 로직을 캡슐화하는 새로운 configureStore 함수를 만드는 것입니다. 이 함수는 별도의 파일에 위치시켜 확장성을 높일 수 있습니다.

최종 목표는 index.js가 다음과 같이 보이도록 하는 것입니다:

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import App from './components/App'
import configureStore from './configureStore'

const store = configureStore()

render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)

스토어 구성과 관련된 모든 로직(리듀서, 미들웨어, 확장 기능 임포트 포함)은 전용 파일에서 처리됩니다.

이를 달성하기 위해 configureStore 함수는 다음과 같이 구현됩니다:

import { applyMiddleware, compose, createStore } from 'redux'
import { thunk } from 'redux-thunk'

import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'

export default function configureStore(preloadedState) {
const middlewares = [loggerMiddleware, thunk]
const middlewareEnhancer = applyMiddleware(...middlewares)

const enhancers = [middlewareEnhancer, monitorReducersEnhancer]
const composedEnhancers = compose(...enhancers)

const store = createStore(rootReducer, preloadedState, composedEnhancers)

return store
}

이 함수는 앞서 설명한 단계를 따르면서, 향후 확장을 위해 일부 로직을 분리하여 추가 기능을 더 쉽게 통합할 수 있게 합니다:

  • middlewaresenhancers 모두 이를 소비하는 함수와 별도로 배열로 정의됩니다.

    이는 다양한 조건에 따라 미들웨어나 확장 기능을 쉽게 추가할 수 있게 해줍니다.

    예를 들어, 개발 모드에서만 특정 미들웨어를 추가하는 것은 if 문 내에서 미들웨어 배열에 푸시하기만 하면 간단히 해결됩니다:

    if (process.env.NODE_ENV === 'development') {
    middlewares.push(secretMiddleware)
    }
  • preloadedState 변수는 나중에 추가할 경우를 대비해 createStore에 전달됩니다.

이 접근 방식은 createStore 함수를 더 직관적으로 만들며, 각 단계가 명확히 분리되어 실제 동작 방식을 쉽게 파악할 수 있습니다.

개발자 도구 확장 기능 통합

앱에 추가할 수 있는 또 다른 유용한 기능은 redux-devtools-extension 통합입니다.

이 확장 기능은 Redux 스토어를 완벽하게 제어할 수 있는 도구 세트로, 액션 검사 및 재실행, 다양한 시점의 상태 탐색, 스토어에 직접 액션 디스패치 등의 기능을 제공합니다. 사용 가능한 기능에 대한 자세한 내용은 여기를 참조하세요.

통합 방법은 여러 가지가 있지만, 가장 편리한 옵션을 사용하겠습니다.

먼저 npm을 통해 패키지를 설치합니다:

npm install --save-dev redux-devtools-extension

다음으로 redux에서 임포트했던 compose 함수를 제거하고, redux-devtools-extension에서 임포트한 새로운 composeWithDevTools 함수로 대체합니다.

최종 코드는 다음과 같습니다:

import { applyMiddleware, createStore } from 'redux'
import { thunk } from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'

import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'

export default function configureStore(preloadedState) {
const middlewares = [loggerMiddleware, thunk]
const middlewareEnhancer = applyMiddleware(...middlewares)

const enhancers = [middlewareEnhancer, monitorReducersEnhancer]
const composedEnhancers = composeWithDevTools(...enhancers)

const store = createStore(rootReducer, preloadedState, composedEnhancers)

return store
}

이것으로 완료되었습니다!

이제 개발자 도구 확장 기능이 설치된 브라우저로 앱에 접속하면, 이 강력한 새 도구를 사용해 탐색 및 디버깅할 수 있습니다.

핫 리로딩

개발 과정을 훨씬 직관적으로 만들어주는 또 다른 강력한 도구는 핫 리로딩입니다. 이 기능은 전체 앱을 재시작하지 않고 코드 조각을 교체할 수 있게 해줍니다.

예를 들어, 앱을 실행하고 상호작용한 후 리듀서 중 하나를 수정하려는 상황을 가정해 보세요. 일반적으로 코드를 변경하면 앱이 재시작되며 Redux 상태가 초기 값으로 돌아갑니다.

핫 모듈 리로딩을 활성화하면 변경된 리듀서만 리로드되므로, 매번 상태를 초기화하지 않고 코드를 수정할 수 있습니다. 이는 개발 프로세스를 훨씬 빠르게 만들어 줍니다.

Redux 리듀서와 React 컴포넌트 모두에 핫 리로딩을 추가하겠습니다.

먼저 configureStore 함수에 추가합니다:

import { applyMiddleware, compose, createStore } from 'redux'
import { thunk } from 'redux-thunk'

import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'

export default function configureStore(preloadedState) {
const middlewares = [loggerMiddleware, thunk]
const middlewareEnhancer = applyMiddleware(...middlewares)

const enhancers = [middlewareEnhancer, monitorReducersEnhancer]
const composedEnhancers = compose(...enhancers)

const store = createStore(rootReducer, preloadedState, composedEnhancers)

if (process.env.NODE_ENV !== 'production' && module.hot) {
module.hot.accept('./reducers', () => store.replaceReducer(rootReducer))
}

return store
}

새 코드는 if 문으로 감싸져 있어 프로덕션 모드가 아니고 module.hot 기능을 사용할 수 있을 때만 실행됩니다.

Webpack이나 Parcel 같은 번들러는 module.hot.accept 메서드를 지원하여 핫 리로딩할 모듈을 지정하고 모듈이 변경될 때의 동작을 정의할 수 있게 합니다. 여기서는 ./reducers 모듈을 감시하고 변경 시 업데이트된 rootReducerstore.replaceReducer 메서드에 전달합니다.

React 컴포넌트 변경 사항을 핫 리로드하기 위해 index.js에서도 동일한 패턴을 사용합니다:

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import App from './components/App'
import configureStore from './configureStore'

const store = configureStore()

const renderApp = () =>
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)

if (process.env.NODE_ENV !== 'production' && module.hot) {
module.hot.accept('./components/App', renderApp)
}

renderApp()

여기서 추가된 변경 사항은 앱 렌더링 로직을 새로운 renderApp 함수로 캡슐화하여 이제 앱을 다시 렌더링할 때 호출한다는 점입니다.

Redux Toolkit으로 설정 간소화

Redux 코어 라이브러리는 의도적으로 제약이 없습니다. 스토어 설정, 상태 구성, 리듀서 작성 방법 등 모든 것을 직접 결정할 수 있도록 유연성을 제공합니다.

이는 경우에 따라 유연성이 장점이 될 수 있지만, 항상 필요한 것은 아닙니다. 때로는 기본적으로 제공되는 적절한 동작을 통해 가능한 한 간단하게 시작하는 것이 좋습니다.

Redux Toolkit 패키지는 스토어 설정을 포함한 여러 일반적인 Redux 사용 사례를 단순화하도록 설계되었습니다. 스토어 설정 과정을 어떻게 개선하는지 살펴보겠습니다.

Redux Toolkit에는 이전 예제에서 본 것과 유사한 사전 구성된 configureStore 함수가 포함되어 있습니다.

가장 빠른 사용법은 루트 리듀서 함수를 전달하는 것입니다:

import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './reducers'

const store = configureStore({
reducer: rootReducer
})

export default store

명명된 매개변수를 사용하는 객체를 받아 전달하는 내용이 명확하도록 합니다.

Redux Toolkit의 configureStore는 기본적으로 다음을 수행합니다:

다음은 Redux Toolkit을 사용한 핫 리로딩 예제입니다:

import { configureStore } from '@reduxjs/toolkit'

import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'

export default function configureAppStore(preloadedState) {
const store = configureStore({
reducer: rootReducer,
middleware: getDefaultMiddleware =>
getDefaultMiddleware().prepend(loggerMiddleware),
preloadedState,
enhancers: [monitorReducersEnhancer]
})

if (process.env.NODE_ENV !== 'production' && module.hot) {
module.hot.accept('./reducers', () => store.replaceReducer(rootReducer))
}

return store
}

이를 통해 설정 과정이 확실히 단순화되었습니다.

다음 단계

이제 스토어 구성을 캡슐화하여 유지 관리를 용이하게 하는 방법을 알았으니, Redux Toolkit의 configureStore API를 살펴보거나 Redux 생태계의 확장 기능을 자세히 알아볼 수 있습니다.