프론트엔드 개발/상태관리 라이브러리

React Redux Tookit 사용 방법

snowman95 2022. 1. 25. 01:36
728x90
반응형

본 게시물에서는 Redux Toolkit 을 사용한 버전으로 예시를 들어보겠다.

리덕스를 훨씬 편한 방법으로 사용할 수 있는 확장팩이라고 생각하면 된다.

 

Redux Toolkit | Redux Toolkit

The official, opinionated, batteries-included toolset for efficient Redux development

redux-toolkit.js.org

리덕스에 관해서는 아래 게시물을 보고 이해하고 오면 된다.

 

React Redux (리액트 리덕스)

리덕스란? 리액트 뿐만 아니라 다양한 프레임워크에서 사용되는 상태관리 라이브러리이다. 리액트는 MVC (Model View Controller) 중에 View만 담당하게 되는데 나머지 Model, Controller에 해당하는 역할을

11001.tistory.com

 

동작 방식

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도 수행 가능하다.

 

반응형