이번 포스팅에서는 React에서 Redux를 사용해 전역 상태 관리를 하는 방법에 대해 알아보고자 한다.
< 목차 >
- Redux
- react-redux로 상태 관리하기
1. Redux
Redux는 JavaScipt 상태 관리 라이브러리로 React의 context API와 비슷한 기능을 수행한다. Redux를 사용하면 store라는 데이터 공간을 만든 후 해당 공간 안에서 데이터의 상태를 관리할 수 있다. Redux에는 store, action, reducer라는 세가지 개념이 존재한다.
👉 store (스토어)
store는 상태가 관리되는 오직 하나의 공간이다. 컴포넌트와 별개로 store라는 공간이 있어서 그 store 안에 앱에서 필요한 상태를 담는다. 특정 컴포넌트에서 상태 정보가 필요하다면 store에 접근해서 데이터를 가져오게 된다. store 객체 안에는 상태를 가져올 수 있는 getState( ) 함수 와 reducer를 실행하는 dispatch( ) 함수가 존재한다. 또한 store.subscribe( ) 메소드를 이용해 reducer 함수 실행 이후 호출할 함수를 등록할 수 있다.
- store.getState( ) : store에 있는 상태값을 가져올 수 있다.
- store.dispatch( action ) : 인자값으로 action을 전달해 reducer 함수를 실행시킨다.
- store.subscribe( listener ) : 인자값으로 listener 함수를 전달하면 reducer 함수 실행 이후 listener로 등록된 함수가 실행된다.
👉 action (액션)
action은 store에 운반할 데이터를 지칭한다. 즉, action을 사용해서 store로 데이터를 보내게 된다. action은 JavaScript 객체 형식으로 되어있다. dispatch 함수의 인자값으로 action을 전달하면 reducer 함수가 호출되면서 인자값으로 action을 전달 받게 된다. 요약하면, dispatch(action) 을 통해 reducer 함수를 호출함과 동시에 reducer 함수에게 action을 전달해준다.
👉 reducer (리듀서)
dispatch 함수를 통해 action을 reducer 함수에 전달한다. reducer 함수는 인자값으로 전달받은 action을 보고 store 안의 상태를 갱신할지 여부를 결정한다.
추가로 상태를 나눠서 관리하고 싶은 경우 Redux에서 제공하는 combineReducers 함수를 사용할 수 있다. 상태 관리에 사용하고 싶은 reducer 함수들을 객체에 담아 combineReducers( ) 함수의 인자값으로 전달한다. 주의할 점은 dispatch 함수를 이용해 reducer 함수를 호출할 때 combineReducers( ) 함수의 인자값으로 들어간 모든 reducer 함수가 실행된다. 따라서 combineReducers 함수를 사용할 때에는 action 값을 이용해 각각의 reducer 함수 안에서 특정 action 값일 경우에만 상태를 변경하도록 조건을 걸어준다.
2. react-redux로 상태 관리하기
React에서 Redux를 사용해 상태를 관리하기 위해서는 Redux 뿐만 아니라 React와 Redux를 연결해주는 라이브러리 역시 추가적으로 설치해줘야 한다. VSCode 에디터에서 npm install을 통해 redux 와 react-redux 패키지를 설치해주도록 하자.
npm install redux
npm install react-redux
그리고 브라우저에서 Redux DevTools를 사용하기 위해 다음의 패키지를 설치해준다.
npm install redux-devtools-extension
react-redux를 사용해 어떤식으로 전역 상태 관리를 하는지 알아보기 위해 카운터를 만들어 보았다.
// store를 만드는 useStore.jsx 파일
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import rootReducer from '../reducers/reducer.js'
const store = createStore(rootReducer, composeWithDevTools())
const Store = ({children}) => {
return (
<Provider store={store}>
{children}
</Provider>
)
}
export default Store;
우선 전역 상태를 관리할 store라는 객체를 만들어주었다. store를 만들기 위해 createStore( ) 함수를 사용했으며 인자값으로 rootReducer 함수와 composeWithDevTools( )를 넣어주었다. rootReducer는 위에서 언급했던 reducer 함수이고 composeWithDevTools( )를 인자값으로 넣어줌으로써 브라우저에서 Redux DevTools를 사용할 수 있게 된다.
store를 이용해 전역 상태 관리를 하기 위해서는 <Provider />라는 component가 필요하다. <Provider /> component의 props로 store를 전달하면 <Provider /> component의 하위 컴포넌트에서 store에 접근하는 것이 가능해진다.
다음은 App.js 파일과 index.js 파일이다. store에 담은 상태를 전역에서 관리하기 위해 <Store /> component가 최상위 컴포넌트가 되도록 하였다.
// App.js 파일
import './App.css';
import Counter from './counter/counter.jsx'
function App() {
return (
<div className="App">
<Counter />
</div>
);
}
export default App;
// index.js 파일
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import Store from './store/useStore.jsx'
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Store>
<App />
</Store>
</React.StrictMode>
);
이제 reducer인 rootReducer 함수를 살펴보자.
// rootReducer 함수가 들어있는 reducer.js 파일
const initialState = {
number: 0
}
const UP = 'COUNTER/UP'
const DOWN = 'COUNTER/DOWN'
// actions
export const up = () => ({type: UP})
export const down = () => ({type: DOWN})
const rootReducer = (state=initialState, action) => {
switch (action.type) {
case UP :
return {
...state,
number: state.number + 1
}
case DOWN :
return {
...state,
number: state.number - 1
}
default :
return state
}
}
export default rootReducer;
rootReducer 함수는 Redux에서 등장하는 reducer 함수이며 인자값으로 state 와 action을 받는다. dispatch(action)에 의해 rootReducer 함수가 호출되면서 action값에 따라 상태를 변경하게 되는데 이때 state 매개변수의 default 값으로 initialState를 지정해 둠으로써 상태 변경 조건에 해당하지 않는 경우 기존의 상태값을 return 해주도록 하였다.
마지막으로 <Counter /> component의 내용이 작성된 파일을 살펴보도록 하자.
// 카운터 버튼이 구현된 counter.jsx
import { useSelector, useDispatch } from 'react-redux'
import { up, down } from '../reducers/index.js'
const Counter = () => {
const count = useSelector((state)=>{
// 전역 상태에 있는 state를 가져와서 count 변수에 담은 것.
return state
})
const dispatch = useDispatch()
const downClick = e => {
dispatch(down())
}
const upClick = e => {
dispatch(up())
}
return (
<>
<h1>Hello React-Redux!!</h1>
<h2>Counter : {count.number}</h2>
<button onClick={downClick}>-1</button>
<button onClick={upClick}>+1</button>
</>
)
}
export default Counter;
주의깊게 살펴보아야 할 것은 useSelector Hook 과 useDispatch Hook 이다. 특정 컴포넌트에서 store에 저장된 state를 가져오거나 state를 변경하고 싶을 때 useSelector Hook 과 useDispatch Hook을 사용한다. useSelector Hook을 사용하면 store에 저장된 state를 가져올 수 있다. 그리고 useDispatch Hook을 사용해서 dispatch 함수를 실행시킬 수 있는데 보통 dispatch 변수에 useDispatch( )의 반환 값을 저장해서 const dispatch = useDispatch( ) 와 같은 형태로 사용한다.
- useSelector( (state) => state ) : store에 있는 state를 가져올 수 있다.
- useDispatch( ) : dispatch 함수를 반환한다.
<button>엘리먼트에 onClick 이벤트를 부여해서 클릭이 발생했을 때 upClick 함수와 downClick 함수가 실행되도록 하였다. upClick과 downClick 함수 안에서는 dispatch( )를 실행시켜 reducer.js 파일 안에서 만들어 놓은 action 값을 반환하는 함수인 up( ) 과 down( )을 인자값으로 전달하였다. 결과적으로 버튼을 클릭했을 때 dispatch 함수가 호출되면서 rootReducer 함수를 실행시키고 rootReducer 함수 안에서 switch문을 통해 action 값에 따라 상태를 변경하게 된다.
'React' 카테고리의 다른 글
Redux - redux-saga (1) | 2022.05.10 |
---|---|
Redux - redux middleware (0) | 2022.05.07 |
React - react-router-dom (0) | 2022.05.02 |
React - Custom Hook (0) | 2022.05.01 |
React - useCallback & useMemo (0) | 2022.04.30 |