Redux
리덕스는 오픈 소스 자바스크립트 라이브러리의 일종으로, state를 이용해 웹 사이트 혹은 애플리케이션의 상태 관리를 해줄 목적으로 사용한다.
Categories
- redux-persist
- redux-logger
- Redux Toolkit (redux-toolkit)
- redux-thunk - 리덕스에서 비동기 작업을 처리 할 때 가장 많이 사용하는 미들웨어이다. 이 미들웨어를 사용하면 action객체가 아닌 함수를 dispatch 할 수 있다.
- 장점 : 한 번에 여러가지를 dispatch 할 수 있게 해줌
- 단점 : 그게 다임, 나머지는 본인이 다 구현해야 함
- redux-saga - 비동기 작업을 처리하기 위한 미들웨어이다. Redux-thunk가 함수를 dispatch 할 수 있게 해주는 미들웨어였다면, Saga는 action을 모니터링 하고 있다가 특정 action이 발생했을 때, 미리 정해둔 로직에 따라 특정 작업이 이루어진다. 또한 Sagas라는 순수함수들로 로직을 처리할 수 있는데, 순수함수로 이루어지다보니, side effect도 적고 테스트 코드를 작성하기에도 용이하다.
Redux의 장점
- 상태를 예측 가능하게 만든다. (순수함수를 사용하기 때문)
- 유지보수 (복잡한 상태 관리와 비교)
- 디버깅에 유리 (action과 state log 기록 시) → redux dev tool (크롬 확장)
- 테스트를 붙이기 용의 (순수함수를 사용하기 때문)
Redux의 기본 개념 : 세 가지 원칙
- Single source of truth
- 동일한 데이터는 항상 같은 곳에서 가지고 온다.
- 즉, 스토어라는 하나뿐인 데이터 공간이 있다는 의미이다.
- State is read-only
- 리액트에서는
setState
메소드를 활용해야만 상태 변경이 가능하다. - 리덕스에서도 액션이라는 객체를 통해서만 상태를 변경할 수 있다.
- Changes are made with pure functions
- 변경은 순수함수로만 가능하다.
- 리듀서와 연관되는 개념이다.
- Store(스토어) – Action(액션) – Reducer(리듀서)
Store, Action, Reducer의 의미와 특징
Redux-state-step.png
데이터가 한 방향으로만 흘러야하기 때문이다.
Store (스토어)
- Store(스토어)는 상태가 관리되는 오직 하나의 공간이다.
- 컴포넌트와는 별개로 스토어라는 공간이 있어서 그 스토어 안에 앱에서 필요한 상태를 담는다.
- 컴포넌트에서 상태 정보가 필요할 때 스토어에 접근한다.
Action (액션)
- Action(액션)은 앱에서 스토어에 운반할 데이터를 말한다. (주문서)
- Action(액션)은 자바스크립트 객체 형식으로 되어있다.
Reducer (리듀서)
- Action(액션)을 Store(스토어)에 바로 전달하는 것이 아니다.
- Action(액션)을 Reducer(리듀서)에 전달해야한다.
- Reducer(리듀서)가 주문을 보고 Store(스토어)의 상태를 업데이트하는 것이다.
- Action(액션)을 Reducer(리듀서)에 전달하기 위해서는
dispatch()
메소드를 사용해야한다.
Redux에서 위 개념을 구현하는 두 가지 방법
-
mapStateToProps()
- Redux hooks
- useSelector
- useDispatch
React 에서 사용할 경우 설치 방법
Provider
import React from 'react';
import ReactDOM from 'react-dom';
import App from './containers/App';
import './index.css';
// Redux 관련 불러오기
import { createStore } from 'redux'
import reducers from './reducers';
import { Provider } from 'react-redux';
// 스토어 생성
const store = createStore(reducers);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
Deprecated Example
WARNING |
Deprecated 상태이므로 #@reduxjs/toolkit Example를 사용하자 |
import { createStore } from 'redux'
/**
* 이것이 (state, action) => state 형태의 순수 함수인 리듀서입니다.
* 리듀서는 액션이 어떻게 상태를 다음 상태로 변경하는지 서술합니다.
*
* 상태의 모양은 당신 마음대로입니다: 기본형(primitive)일수도, 배열일수도, 객체일수도,
* 심지어 Immutable.js 자료구조일수도 있습니다. 오직 중요한 점은 상태 객체를 변경해서는 안되며,
* 상태가 바뀐다면 새로운 객체를 반환해야 한다는 것입니다.
*
* 이 예제에서 우리는 `switch` 구문과 문자열을 썼지만,
* 여러분의 프로젝트에 맞게
* (함수 맵 같은) 다른 컨벤션을 따르셔도 좋습니다.
*/
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
// 앱의 상태를 보관하는 Redux 저장소를 만듭니다.
// API로는 { subscribe, dispatch, getState }가 있습니다.
let store = createStore(counter)
// subscribe()를 이용해 상태 변화에 따라 UI가 변경되게 할 수 있습니다.
// 보통은 subscribe()를 직접 사용하기보다는 뷰 바인딩 라이브러리(예를 들어 React Redux)를 사용합니다.
// 하지만 현재 상태를 localStorage에 영속적으로 저장할 때도 편리합니다.
store.subscribe(() => console.log(store.getState())))
// 내부 상태를 변경하는 유일한 방법은 액션을 보내는 것뿐입니다.
// 액션은 직렬화할수도, 로깅할수도, 저장할수도 있으며 나중에 재실행할수도 있습니다.
store.dispatch({ type: 'INCREMENT' })
// 1
store.dispatch({ type: 'INCREMENT' })
// 2
store.dispatch({ type: 'DECREMENT' })
// 1
@reduxjs/toolkit Example
- createSlice(redux-toolkit)를 이용하여 간단하게 action과 reducer 사용하기
- Redux Toolkit의 createAsyncThunk로 비동기 처리하기
- [추천] maruzzing's devlog - Redux-toolkit을 활용한 상태관리 1
- [추천] (6/5) Redux-toolkit세팅하기 + 튜토리얼 번역 (Next.js 13)
다음과 같이 사용한다: app/store.js
:
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
features/counter/counterSlice.js
:
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { fetchCount } from './counterAPI';
const initialState = {
value: 0,
status: 'idle',
};
// The function below is called a thunk and allows us to perform async logic. It
// can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This
// will call the thunk with the `dispatch` function as the first argument. Async
// code can then be executed and other actions can be dispatched. Thunks are
// typically used to make async requests.
export const incrementAsync = createAsyncThunk(
'counter/fetchCount',
async (amount) => {
const response = await fetchCount(amount);
// The value we return becomes the `fulfilled` action payload
return response.data;
}
);
export const counterSlice = createSlice({
name: 'counter',
initialState,
// The `reducers` field lets us define reducers and generate associated actions
reducers: {
increment: (state) => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the Immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
// Use the PayloadAction type to declare the contents of `action.payload`
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
// The `extraReducers` field lets the slice handle actions defined elsewhere,
// including actions generated by createAsyncThunk or in other slices.
extraReducers: (builder) => {
builder
.addCase(incrementAsync.pending, (state) => {
state.status = 'loading';
})
.addCase(incrementAsync.fulfilled, (state, action) => {
state.status = 'idle';
state.value += action.payload;
});
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
// The function below is called a selector and allows us to select a value from
// the state. Selectors can also be defined inline where they're used instead of
// in the slice file. For example: `useSelector((state: RootState) => state.counter.value)`
export const selectCount = (state) => state.counter.value;
// We can also write thunks by hand, which may contain both sync and async logic.
// Here's an example of conditionally dispatching actions based on current state.
export const incrementIfOdd = (amount) => (dispatch, getState) => {
const currentValue = selectCount(getState());
if (currentValue % 2 === 1) {
dispatch(incrementByAmount(amount));
}
};
export default counterSlice.reducer;
features/counter/counterAPI.js
:
// A mock function to mimic making an async request for data
export function fetchCount(amount = 1) {
return new Promise((resolve) =>
setTimeout(() => resolve({ data: amount }), 500)
);
}
index.js
:
import React from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './app/store';
import App from './App';
import reportWebVitals from './reportWebVitals';
import './index.css';
const container = document.getElementById('root');
const root = createRoot(container);
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
사용할 때 예제:
import { useSelector, useDispatch } from 'react-redux';
import {
decrement,
increment,
incrementByAmount,
incrementAsync,
incrementIfOdd,
selectCount,
} from './counterSlice';
export function Counter() {
const count = useSelector(selectCount);
const dispatch = useDispatch();
dispatch(decrement());
dispatch(increment());
dispatch(incrementByAmount(2));
// ...
}
See also
Favorite site
- Redux (JavaScript library) - Wikipedia
- Redux(리덕스)란? (상태 관리 라이브러리) - 하나몬
- 리덕스(Redux) 애플리케이션 설계에 대한 생각
- Making sense of Redux
- Naver D2 - React 적용 가이드 - React와 Redux
- Redux 문서(한글)
상태관리 라이브러리 비교
- React 최신 상태관리 라이브러리 비교하기 (feat. zustand, redux-toolkit, jotai, recoil) :: 개발 메모장 (zustand, redux-toolkit, jotai, recoil)
- 상태관리 라이브러리 비교: Redux vs Recoil vs Zustand vs Jotai (redux, recoil, zustand, jotai)
References
-
WebFrameworks_-_Making_sense_of_Redux.pdf ↩