본 게시물에서는 Redux Toolkit 을 사용한 버전으로 예시를 들어보겠다.
리덕스를 훨씬 편한 방법으로 사용할 수 있는 확장팩이라고 생각하면 된다.
리덕스에 관해서는 아래 게시물을 보고 이해하고 오면 된다.
동작 방식
View에서 action 수행 → Dispatch 을 반드시 거쳐서 Store로 전달 → action에 해당되는 Callback 수행 → state 업데이트 → 업데이트 된 State를 View에서 받아서 출력
사용 방법
1. 설치
npm i react-redux @reduxjs/toolkit
2. 리덕스 스토어 구성 파일 생성
src 디렉토리 아래에 store.js 파일을 만든다.
src
ㄴ store.js // redux
[store.js]
import logger from "redux-logger";
import { configureStore } from "@reduxjs/toolkit";
export const store = () => {
const store = configureStore({
reducer: {users : usersReducer, posts : postsReducer},
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger),
devTools: process.env.NODE_ENV !== "production", // 개발자 도구 on/off
// preloadedState: {}, // 스토어의 초기값
// 개발자가 원하는 store enhancer를 미들웨어가 적용되는 순서보다 앞서서 추가 가능
// enhancers: [reduxBatch],
});
};
reducer: {users : usersReducer, posts : postsReducer},
위와 같이 작성하면 자동으로 combineReducers 를 호출해서 하나의 리듀서로 묶어준다.
그러나 관리 차원에서 아래와 같이 별도의 파일로 빼도 상관없다.
[rootReducer.js]
import board from "./modules/board";
import review from "./modules/review";
import { combineReducers } from "@reduxjs/toolkit";
export const rootReducer = combineReducers({
board,
review,
});
[store.js]
import { configureStore } from "@reduxjs/toolkit";
import { rootReducer } from "./rootReducer";
export const store = () => {
const store = configureStore({
reducer: rootReducer,
});
};
middleware :
나는 클라이언트와 서버 중간에 위치해서 중간 기능을 넣는 역할을 한다라고 이해했다.
getDefaultMiddleware() 는 기본 미들웨어를 적용하기 위해 사용한다.
아래와 같이 작성하면 기본 미들웨어에 사용자 미들웨어를 추가하여 함께 사용한다는 뜻이다.
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger),
기본 미들웨어는 아래와 같다.
- 불변성 검사 미들웨어 : 값을 직접 수정한 경우 (mutation) error를 발생시키는 역할 (redux-immutable-state-invariant)
- 직렬화 가능성 검사 미들웨어 : 직렬화 할 수 없는 Promise와 같은 객체를 받았을때 error를 발생시키는 역할
만약 기본 미들웨어를 무시하고 사용자 정의 미들웨어만 적용하고자 한다면 아래와 같이 쓰면 된다.
middleware: [thunk, logger], // thunk, logger는 별도로 import한 미들웨어임
middleware는 아래와 같이 액션 수행 직전에 개입해서 무언가 처리를 하는 녀석이라고 이해하면 된다.
3. 스토어 사용 등록
store.js 를 import 하여 Provider 에 store를 전달한다. 최상단에 위치시켜야 한다.
import React from "react";
import ReactDOM from "react-dom";
import App from "./components/App";
import { Provider } from "react-redux";
import { store } from "./store";
import { ThemeProvider } from "styled-components";
import theme from "styles/theme";
ReactDOM.render(
<Provider store={store}>
<React.StrictMode>
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
</React.StrictMode>
</Provider>,
document.getElementById("root")
);
4. 리듀서 만들기
createSlice는 createAction과 createReducer() 를 한번에 사용한 것이다.
"상태의 일부분" 이라는 뜻에서 Slice라고 이름지은 것 같다.
파일 경로는 사람마다 다른것 같은데 권장 경로인 services아래로 만들자.
→ /src/services/counterSlice.js
[counterSlice.js]
import { createSlice } from '@reduxjs/toolkit'
const initialState = { value: 0 }
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment(state) {
state.value++
},
decrement(state) {
state.value--
},
incrementByAmount(state, action) {
state.value += action.payload
},
},
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
createSlice 는 아래 내용을 반환한다.
{
name : string,
reducer : ReducerFunction,
actions : Record<string, ActionCreator>,
caseReducers: Record<string, CaseReducer>.
getInitialState: () => State
}
이렇게 만들어진 리듀서는 처음에 만든 store.js 에서 아래와 같이 등록되는 것이다.
counterSlice.js 에서 default로 반환하는 슬라이스이름.reducer를 받아온 것이다.
import { configureStore } from "@reduxjs/toolkit";
import { counterSlice } from "./services/counter";
export const store = () => {
const store = configureStore({
reducer: counterSlice,
});
};
리듀서 내부 변수들을 조금 살펴보자
name은 각 액션 타입의 접두사로 사용된다.
- increment 는 counter/increment
- decrement 는 counter/decrement
reducer에는 여러 액션을 정의할 수 있다.
액션명 : (state, action) => ...
initialState : { value : 0 } 라고 정의된 경우
state.value 이런 식으로 스토어에 접근할 수 있다.
넘겨받은 매개변수에는 action.payload 로 접근할 수 있다
보통 const { id, name } = action.payload 와 같이 풀어서 사용한다.
최종적으로 아래와 같이 넘겨받은 매개변수를 스토어 값에 넣어줄 수 있다.
참고로 immer을 기반으로 동작하기 때문에 아래와 같이 덮어씌우는 듯 작성해도 실제론 immutable 하게 동작한다.
state.value = action.payload
extraReducer는 action 타입에서 정의되지 않은 (외부/비동기)action을 사용할 수 있게 해준다.
그냥 reducer는 (내부/동기)action을 다룬다.
비동기 호출시 사용되는 Thunk를 외부에 작성해놓고 바로 이 extraReducer로 스토어에 연결시켜줄 수 있는데
이것은 게시물을 따로 포스팅하여 다루겠다.
빌더 표기법을 사용할 수 있다.
builder.addCase(...).addCase(...).addCase(...)
이런 식으로 주렁주렁 Case를 달아놓고 해당되는 액션을 수행하는 방식이다.
addCase는 정확히 일치하는 1개
addMatcher는 여러 케이스에 매치시킬 수 있다.
.addDefaualtCase는 매치되지 않았을 때 선택되는 기본 케이스
4. 액션 호출
먼저 아주 옛날에 클래스 컴포넌트 쓰던 리액트 방식 부터 보자
connect 함수를 통해 아래의 두 함수를 스토어에 연결시킨다. (필요한 것만 작성해도 된다.)
- mapStateToProps() : 상태 가져오기
- mapDispatchToProps() : 상태 변경 액션 수행
import { connect } from "react-redux";
import { add } from "../store";
function 컴포넌트({ 새변수명, 새함수명 }) {
새함수명(값); → 이 값은 store.js의 add함수의 action.payload로 들어가게된다.
}
function mapStateToProps(state, ownProps) { → 여기서 ownProps는 빼도된다.
return { 새변수명: state }; → 이 return 값이 컴포넌트의 props로 추가된다.
}
function mapDispatchToProps(dispatch) {
return { 새함수명: (text) => dispatch(add(text))}; → add는 store.js에서 가져온 함수
}
export default connect(mapStateToProps, mapDispatchToProps)(컴포넌트명);
mapDispathToProps를 생략할 경우는 아래와 같이 사용한다.
connect(mapStateToProps);
mapStateToProps 를 생략할 경우에는 null로 채워줘야한다.
connect(null, mapDispathToProps);
function mapDispatchToProps(dispatch, ownProps) {
return {dispatch};
}
다음으로는 최신 함수형 컴포넌트 + hook 방식에서 사용 방식을 알아보자
사용하기 간편하도록 아래와 같이 Hook을 제공한다.
- useDispatch() : Acton 실행용 Hook
- useSelector() : State 조회용 Hook
import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { boardSelector, fetchBoard } from "redux/modules/board";
export const useFetchBoard = () => {
const dispatch = useDispatch();
const boards = useSelector(boardSelector());
const [loading, setLoading] = useState(true);
const fetch = () => !boards && dispatch(fetchBoard());
useEffect(() => fetch(), []);
useEffect(() => boards && setLoading(false), [boards]);
return { loading, boards, fetch };
};
참고로 boardSelector()는 reducer 내부에 작성하는 State 조회용 변수이다.
[/src/services/board.js]
const board = createSlice({
name: "boardReducer",
initialState: {
boards: [],
}
...
export const boardSelector = (state) => state.board.boards;
fetchBoard()위치에는 아래와 같이 동기/비동기 Action 함수가 올 수 있다.
1. reducer 내부에 작성된 동기적으로 데이터를 호출 함수
Slice 내부의 reducers에 작성한 Action 함수를 호출할 수 있다.
(단순히 참고를 위해 다른 예시를 들었음. 이름 다르다고 당황말기를)
[/src/services/user.js]
const user = createSlice({
name: "UserReducer",
reducers: {
setUserName: (state, action) => {
state.name = action.payload
},
},
...
export const { setUserName } = user.actions;
2. reducer 외부에 작성된 비동기적으로 데이터를 호출하는 함수
(Redux Toolkit의 createAsyncThunk + Axios를 이용하여 비동기 호출을 한 예시)
이 부분은 별도 게시물로 다룰 예정이다.
[/src/services/board.js]
export const fetchBoard = createAsyncThunk(
"board/fetchBoard",
async ({ rejectWithValue }) => {
return axios
.create({ baseURL })
.get(`/board`)
.then((res) => {
if (!res.data.content) {
return rejectWithValue("No Board Data");
}
return res.data.content;
})
.catch((error) => rejectWithValue(error.res.data));
}
)
const board = createSlice({
name: "boardReducer",
initialState: {
...
extraReducers: (builder) => {
builder
.addCase(fetchBoard.pending, (state, action) => {
if (state.loading === "idle") {
state.loading = "pending";
state.currentRequestId = action.meta.requestId;
}
})
.addCase(fetchBoard.fulfilled, (state, action) => {
const { requestId } = action.meta;
if (
state.loading === "pending" &&
state.currentRequestId === requestId
) {
state.loading = "idle";
state.boards = action.payload;
state.currentRequestId = undefined;
}
})
.addCase(fetchBoard.rejected, (state, action) => {
const { requestId } = action.meta;
if (
state.loading === "pending" &&
state.currentRequestId === requestId
) {
state.loading = "idle";
state.error = action.error;
state.currentRequestId = undefined;
}
});
Redux DevTools
1. createStore() 대신에 configureStore() 로 변경해준다.
※ Redux Toolkit과는 관계없음
const store = configureStore({reducer});
import { createStore } from "redux"; → 이거말고
import { configureStore } from "@reduxjs/toolkit"; → 이것
2. 구글 확장 프로그램에서 Redux DevTools 설치한다.
https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=ko
3, F12 눌려서 맨오른쪽 Redux 들어간다.
모든 변경사항을 추적 가능하고 Dispatcher도 수행 가능하다.
'프론트엔드 개발 > 상태관리 라이브러리' 카테고리의 다른 글
redux persist : 새로 고침 시에도 상태 유지 (0) | 2022.01.29 |
---|---|
React RTK Query 캐시 (0) | 2022.01.27 |
React Redux (리액트 리덕스) (0) | 2021.11.18 |