리덕스 툴킷 ReduxToolKit

에구마·2024년 1월 12일
0

React

목록 보기
3/5

Intro


“상태관리? 할까?”

  1. props drilling을 막자!
  2. 상태의 일관성을 유지하자!

“어떤 상태관리 라이브러리가 있을까?”

  • recoil
  • zustand
  • redux-toolkit
    • connect, useDispatch, useSelector 등을 사용하면서 리덕스 사용 가능
    • configureStore, createSlice , createAsyncThunk 등을 지원하며 더 간편하게 사용할 수 있고, 리덕스의 설정, 미들 웨어, 반복되는 코드 등의 이슈를 해결

redux-toolkit을 선택했나?”

  • 상태관리의 필요성
  • 중앙 상태 관리와 FLUX 패턴을 학습하자


Redux-toolkit


Redux Toolkit



Redux 와 Redux ToolKit


// Redux

const ADD_TODO = 'ADD_TODO'
const TODO_TOGGLED = 'TODO_TOGGLED'

export const addTodo = (text) => ({
  type: ADD_TODO,
  payload: { text, id: nanoid() },
})

export const todoToggled = (id) => ({
  type: TODO_TOGGLED,
  payload: { id },
})

export const todosReducer = (state = [], action) => {
  switch (action.type) {
    case ADD_TODO:
      return state.concat({
        id: action.payload.id,
        text: action.payload.text,
        completed: false,
      })
    case TODO_TOGGLED:
      return state.map((todo) => {
        if (todo.id !== action.payload.id) return todo

        return {
          ...todo,
          completed: !todo.completed,
        }
      })
    default:
      return state
  }
}

// 사용할때
<button onClick={() => { dispatch({type='ADD_TODO', paylod: data }) }
  • 각각의 액션 리듀서를 만들어줘야했음
  • 하나의 store에 모든 상태 저장
// Redux ToolKit

import { createSlice } from '@reduxjs/toolkit'

const todosSlice = createSlice({
  name: 'todos',
  initialState: [],
  reducers: {
    todoAdded(state, action) {
      state.push({
        id: action.payload.id,
        text: action.payload.text,
        completed: false,
      })
    },
    todoToggled(state, action) {
      const todo = state.find((todo) => todo.id === action.payload)
      todo.completed = !todo.completed  ///#3
    },
  },
})

export const { todoAdded, todoToggled } = todosSlice.actions
export default todosSlice.reducer
  • createSlice를 사용하여 작게 쪼갠 슬라이스를 만들고 configureStore를 통해 스토어에 저장할 수 있음
  • action.type 없이 바로 리듀서 등록 가능
  • 불변성을 위해 … 등을 사용하지 않고 바로 할당 가능 (#3)


사용해보자 🫡


1. store 만들기

중앙에서 관리하는 store 안에 작은 하나하나인 slice를 가진다. 즉, store는 작은 slice들의 모임이 된다.

// _redux/store.ts

import heartsSlice from './slices/heartsSlice';
import { configureStore } from '@reduxjs/toolkit';

export const store = configureStore({
  reducer: {
    hearts: heartsSlice,
  },
});

// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootStateType = ReturnType<typeof store.getState>;
//->  useSelector 사용시 state의 타입으로 사용됨 !!!

export type AppDispatchType = typeof store.dispatch;
// useDispatch를 좀 더 명확하게 사용하기 위함

2. slice들 만들기

테스트 용으로, 버튼을 눌러서 특정 메세지를 hearts라는 slice에 저장해보자!

// _redux/slices/heartsSlice.ts

import { createSlice } from '@reduxjs/toolkit';

interface IHeartsSlice {
  hearts: string[];
}

const initialState: IHeartsSlice = {
  hearts: [],
};

const heartsSlice = createSlice({
  name: 'heartsSlice',
  initialState,
  reducers: {
    getHearts: (state, action) => {
      if (typeof action.payload === 'string') {
        state.hearts = [...state.hearts, action.payload];
      }
    },
  },
});

export const { getHearts } = heartsSlice.actions; //action을 export

export default heartsSlice.reducer; //리듀서를 export

3. slice 상태 저장하기, 불러오기

slice의 reducer의 action을 실행시키기 위해선 dispatch !
store의 slice들, 즉 각 상태를 사용하기 위해서 useSelector !
→ state.슬라이스명.이름 으로 접근

// src/Test.tsx

import { useDispatch, useSelector } from 'react-redux';
import { getHearts } from './_redux/slices/heartsSlice';
import { AppDispatchType, RootStateType } from './_redux/stores';

export const Test = () => {
  const useAppDispatch: () => AppDispatchType = useDispatch;
  const dispatch = useAppDispatch();
  const hearts = useSelector((state: RootStateType) => state.hearts.hearts);
	// console.log('hearts state', hearts)

  return (
    <>
      <button
        onClick={() => {
          dispatch(getHearts('hi'));
        }}>
        hi 보내기
      </button>
    </>
  );
};

—> dispatch할때 리덕스에서처럼 type을 보내지 않아도 됨! heartsSlice.ts 에서 export const { getHearts } = heartsSlice.actions; 를 했기 때문에 바로 action에 접근할 수 있음

// App.tsx

import { ThemeProvider } from '@emotion/react';
import { Provider } from 'react-redux';
import { Outlet } from 'react-router-dom';
import { store } from './_redux/stores';
import { GlobalReset } from './style/GlobalReset';
import { theme } from './style/theme';

export const App = () => {
  return (
    <Provider store={store}> // 이 React에 store를 등록해주어야한다.
      <ThemeProvider theme={theme}>
        <GlobalReset />
        <Outlet />
      </ThemeProvider>
    </Provider>
  );
};

버튼 클릭시 결과 →



thunk로 비동기 작업하기 😲


데이터 패칭 등 비동기 처리 작업을 여러번 발생시킬 때, 매번 패칭하고 그 결과를 dispatch 하는 액션 로직을 만드는 것은 불필요하다!

이 때, 패칭하고 dispatch하는 액션 로직을 따로 함수로 만들고 이를 슬라이스에 반영시킬 수 있다!

createAsyncThunk

→ action creator 이다! 액션을 만든다!

createAsyncThunk(type, payloadCreator callback, [options])

  • type은 액션타입을 적어준다. 이후에 .pending, .fulfilled, .rejected를 생성해준다.
  • payloadCreator 는 Promise를 반환하는 콜백함수이다. 리듀서와 동일한 형태 (state,action) ⇒ { }

Promise 반환의 상태(pending, fullfilled, rejeected) 각각에 따라 리듀서가 필요하다.

extraReducers

reducers는 액션 creator를 만들어주는데, 비동기작업에 대해서는 만들어주지 못하기 때문에 extraReducers에 정의한다.

//_redux/slices/channelsSlice.ts

import { IChannel } from '@/api/_types/apiModels';
import { getApi } from '@/api/apis';
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';

interface IChannelData {
  channels: IChannel[];
	isLoading: boolean;
}
const initialState: IChannelData = {
  channels: [],
	iLoading: false,
};

// 채널데이터를 받아오는 패칭 액션로직을 thunk로 만든다.
export const getChannelsData = **createAsyncThunk**('getChannels', async () => {
  const response = await getApi('/channels');
  return response?.data as IChannel[];
});

const channelsSlice = createSlice({
  name: 'channelsSlice',
  initialState,
  reducers: {},
  extraReducers: (builder) => { // 👈
	builder.addCase(getChannelsData.pending, (state, action) => {
      state.isLoading = true;
    });
    builder.addCase(getChannelsData.fulfilled, (state, action) => {
      state.isLoading = false;
      state.channels = action.payload;
    });
	builder.addCase(getChannelsData.fulfilled, (state, action) => {
      state.isLoading = false;
    });
  },
});
export default channelsSlice.reducer;
// src/Test.tsx

import { useDispatch, useSelector } from 'react-redux';
import { AppDispatchType, RootStateType } from './_redux/stores';
import { getChannelsData } from './_redux/slices/channelsSlice';

export const Test = () => {
  const useAppDispatch: () => AppDispatchType = useDispatch;
  const dispatch = useAppDispatch();

  return (
    <>
      <button
        onClick={() => {
						**dispatch(getChannelsData())**
            .then((res) => console.log(res))
            .catch((err) => console.error(err));
        }}>
        GET <CHANNEL
      </button>
  );
};

작동과정을 정리하자면,
button Onclick으로 thunk 액션 함수가 실행
→ 액션 로직내에서 비동기 처리
→ (성공시)fullfilled 상태의 리듀서에 그 반환값이 action.payload로 들어옴
→ 상태값에 반영 state.channelds = action.payload




참고자료
https://ko.redux.js.org/redux-toolkit/overview/
https://redux-toolkit.js.org/introduction/getting-started
https://redux-toolkit.js.org/api/createAsyncThunk

profile
코딩하는 고구마 🍠 Life begins at the end of your comfort zone

0개의 댓글