Redux toolkit 튜토리얼

코딩덕·2024년 4월 18일

💡 Redux toolkit

Redux Toolkit은 Redux 개발에서 실수를 줄여주고, 더 간단한 코드를 작성하게 함으로써 좋은 Redux 앱을 쉽고 빠르게 개발할 수 있게 해주는 도구입니다.


상태관리의 필요성

클라이언트에서 전역 상태 관리가 필요한 이유는 애플리케이션의 규모가 커지고 복잡해질수록 컴포넌트간의 데이터 공유와 상태 관리를 보다 효율적으로 처리하기 위해서입니다.

✅ 컴포넌트간 데이터 공유
여러 컴포넌트에서 공통적으로 사용되는 상태가 있을 수 있습니다. 예를 들면 사용자 인증 정보, 로그인 상태, 언어, 테마, 설정 등 애플리케이션의 여러 곳에서 공유해야하는 데이터가 있을 때입니다.

✅ 상태의 일관성 유지
중첩된 컴포넌트가 많은 경우, 상태를 상위로 전달하는 것이 번거로울 수 있습니다. 전역 상태 관리를 통해서 여러 컴포넌트에서 동일한 상태에 접근하고 업데이트할 수 있으며 이를 통해 상태의 일관성을 유지할 수 있습니다.

✅ 비동기 상태 관리
비동기 작업 (API 호출, 데이터 로딩 등)을 처리하는 경우, 전역 상태 관리를 사용하면 여러 컴포넌트에서 해당 상태에 접근하고 업데이트할 수 있습니다. 또한 비동기 작업이 완료된 후에도 상태를 적절하게 관리할 수 있습니다.


Redux toolkit의 특징


(출처 : 리덕스 홈페이지)

  • Simple: 초기 설정이 간편해짐, 스토어 설정, 리듀서 생성, 불변성 업데이트 로직 사용을 편리하게 하는 기능 포함
  • Opitionated: 스토어 설정에 관한 기본 설정 제공, 리덕스를 사용하면 redux devtool, immer, thunk 등 여러가지 라이브러리를 추가적으로 설치해야 하지만, redux-toolkit은 내부에 이미 설치가 되어 있기에 굳이 설치 할 필요가 없습니다.
  • Powerful : Immer에 영감을 받아 '변경'로직으로 '불변성' 로직 작성 가능
  • Effective : 보일러플레이트 개선

(추가) Redux-toolkit과 Redux의 차이점



Redux toolkit 사용방법


createSlice = Action + Reducer

createSlice는 action과 reducer를 전부 가진 함수
기능별로 slice를 생성해 파일을 분리하여 사용합니다.

기본형태

createSlice({
  name: 'posts', // slice의 이름
  initialState: [], // 초기 상태 값
  reducers: {
    action1(state) {
      //action1 logic
    },
    action2(state) {
      //action2 logic
    },
    action3(state, payload) {
      //action3 logic & payload
    },
  },
})

createAsyncThunk

Thunk 미들웨어를 사용하여 비동기 처리를 진행합니다.

기본형태

export const fetchPosts = createAsyncThunk(
  "posts/fetchPosts",    // slice 이름 + 비동기작업 이름
  async () => {
    const response = await client.get('fakeApi/posts'); // 데이터받아오기
    return response.data;
  }
);

extraReducers

비동기 액션을 생성했던 리듀서에 연결시켜줍니다.

기본형태

extraReducers: (builder) => {
  builder
    .addCase(fetchPosts.pending, (state) => {
      state.status = 'loading';
    })
    .addCase(fetchPosts.fulfilled, (state, action) => {
      state.status = 'succeeded';
      state.posts = action.payload;
    })
    .addCase(fetchPosts.rejected, (state, action) => {
      state.status = 'failed';
      state.error = action.error.message;
    })
}

위와 같이 extraReducer의 파리미터인 builder를 통해 addCase를 사용하여
pending(대기), fullfilled(완료), rejected(실패) 상태를 처리합니다.

configureStore

리듀서에서 반환한 새로운 상태를 스토어로 정리하고 관리합니다.
별도의 combineReducers로 리듀서를 묶어줄 필요 없이 reducer 필드를 필수적으로 넣고 그안에 createSlice에서 생성한 리듀서 함수를 넣으면 됩니다.

또한 thunk외에 추가로 필요한 middleware를 등록할 수 있습니다.

일반적으로 React 앱 하나에 하나의 store만 존재해야 합니다.

기본형태

export const store = configureStore({
  reducer: {
    ex1: ex1Reducer,
    ex2: ex2Reducer,
    // 추후 필요한 리듀서 추가...
  },
  // 선택사항
  middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger), // 추후 필요한 middleware 추가...
});

Demo Redux-Toolkit App 구조

(Redux Toolkit App Structure - 리덕스 공식 홈페이지)

1. store.ts

상태 데이터를 넣어둘 저장소인 store를 생성한다.

import { configureStore } from "@reduxjs/toolkit";
import countReducer from "../features/counter/counterSlice";  // counter 컴포넌트에 필요한 리듀서
import { logger } from 'redux-logger';

export const store = configureStore({
  reducer: {
    counter: countReducer,   
    // 추후 필요한 리듀서 추가...
  },
  middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger),
});

export type RootState = ReturnType<typeof store.getState>; // Redux 상태의 타입정의
export type AppDispatch = typeof store.dispatch; // 비동기 dispatch작업에 필요한 타입

2. App.tsx

전체 어플리케이션을 provider을 통해 redux환경으로 감싼다.

import { Provider } from "react-redux";
import { store } from "./store/store";
import Counter from "./features/counter/Counter";

function App() {
  return (
    <Provider store={store}>
      <Counter />
    </Provider>
  );
}

export default App;

3. features/counter/counterSlice.ts

Counter 컴포넌트에 필요한 모든 redux 액션 선언

// counter에 필요한 모든 action, reducer 생성

import { PayloadAction, createAsyncThunk, createSlice } from "@reduxjs/toolkit";

interface CounterState {
  value: number;
}

const initialState: CounterState = {
  value: 0,
};

const counterSlice = createSlice({
  name: "counter",
  initialState,
  // 동기
  reducers: {
    increment: (state) => {
      state.value += 1;       // count값 증가작업
    },
    decrement: (state) => {
      state.value -= 1;       // count값 감소작업
    },
    incrementByAmount: (state, actions: PayloadAction<number>) => {
      state.value += actions.payload;   // count값 인자 값만큼 증가작업
    },
  },
  // 비동기
  extraReducers: (builder) => {
    builder
      .addCase(incrementAsync.pending, () => {
        console.log("just wait");  // 비동기작업 대기상태일 때 진행
      })
      .addCase(
        incrementAsync.fulfilled,
        (state, action: PayloadAction<number>) => {
          state.value += action.payload;   // 비동기작업 완료일때 진행
        }
      );
  },
});

// 비동기작업 선언
export const incrementAsync = createAsyncThunk(
  "counter/incrementAsync",
  async (amount: number) => {
    await new Promise((resolve) => setTimeout(resolve, 1000));
    return amount;
  }
);

export const { increment, decrement, incrementByAmount } = counterSlice.actions;

export default counterSlice.reducer;

4. features/counter/counter.tsx

useSelector로 store의 상태값 counter를 반환합니다.
useDispatch로 counterslice의 액션을 수행합니다.

import { useDispatch, useSelector } from "react-redux";
import { AppDispatch, RootState } from "../../store/store";
import {
  decrement,
  increment,
  incrementAsync,
  incrementByAmount,
} from "./counterSlice";

const Counter = () => {
  const count = useSelector((state: RootState) => state.counter.value); // counterslice의 전역변수 counter 값 불러오기
  const dispatch = useDispatch<AppDispatch>(); // counterslice의 액션 수행
  return (
    <>
      <div>Redux Tutorial</div>
      <br />
      <div>{count}</div>
      <div>
        <button onClick={() => dispatch(increment())}>+</button>
        <button onClick={() => dispatch(incrementByAmount(10))}>+10</button>
        <button onClick={() => dispatch(incrementAsync(100))}>
          Async +100
        </button>
        <button onClick={() => dispatch(decrement())}>-</button>
      </div>
    </>
  );
};

export default Counter;

0개의 댓글