[React] Redux Tookit(RTK)

OnStar·2022년 1월 4일
0

React

목록 보기
11/11
post-thumbnail

Redux란?

https://velog.io/@cks3066/ReactRedux

Redux ToolKit

Redux Toolkit  공식 문서

리덕스 툴킷은 Redux 팀이 제공하는 라이브러리로써 간편하고 효율적인 리덕스 개발을 할 수 있게끔 도와준다.

프로젝트를 진행하며 실제 리덕스를 적용하여 사용하게 됨에 따라 필요한 모든 코드를 작성해야하는 naïve한 리덕스는 프로젝트에 리덕스 도입을 꺼리게 만들었으며 그에 따라 좀 더 쉬운 리덕스의 사용을 위해 RTK가 개발되었다.

RTK 설치

CRA 로 리액트 프로젝트를 생성하는 과정에서 RTK 템플릿 코드를 가져올 수 있다.

npx create-react-app my-app --template redux

npx create-react-app my-app --template redux-typescript

이미 개발이 진행된 프로젝트라면 다음 파일들을 설치하면 된다.

redux, react-redux, @reduxjs/toolkit(툴킷), redux-devtools(개발 툴)


yarn add -D redux react-redux @reduxjs/toolkit
yarn add -D redux-devtools

RTK 함수

configureStore

리덕스의 createStore 을 사용하기위한 툴킷이다.기존의 리덕스의 경우 loggersagathunk 등 미들웨어를 적용하려면 꽤나 복잡한 코드를 작성했어야했다.하지만 thunk는 기본적으로 configureStore에 적용되어있으며 다른 미들웨어도 비교적 간단하게 선언만으로 적용가능하다.

// 루트리듀서
const rootReducer = {
  test,
};

// 스토어 생성
const store = configureStore({
  reducer: rootReducer,
  middleware: [logger],
});
// (기존) const store = createStore(reducer);

createAction

리덕스에서 action을 만들기 위해서는 액션을 선언하고 액션 생성자 함수를 분리하여 선언해야한다. 이때 createAction를 사용하여 인자로 액션을 넣어주기만 하면 간편하게 액션 생성자 함수를 생성할 수 있다.

// 액션
const ADD = "ADD";

// 액션 크리에이터
const addToDo = createAction(ADD);
// (기존)
// const addToDo = (text) => {
//   return {
//     type: ADD,
//     text,
//   };
// };

createReducer

상태변화를 일으키는 리듀서 함수를 생성하는 함수이다. 기존에 불변성유지를 위해 하나하나의 불변성을 위한 코드작성에 주의를 기울이든, immer 라이브러리를 사용하든 불변성을 위한 코드작성이 불편했지만 createReducer는 자동적으로 불변성 관리를 해주기 때문에 좀 더 쉽게 Reducer 함수를 작성할 수 있게 해준다.

// 리듀서
export default createReducer(initialState, {
  [PLUS]: (state, action) => {
    state.value++;
  },
  [MINUS]: (state, action) => {
    state.value--;
  },
  [ADD]: (state, action) => {
    state.array.push(action.payload.value);
  },
  [REMOVE]: (state, action) => {
    state.array.filter((item) => item.key !== action.payload.key);
  },
});
//(기존) 리듀서 함수의 작성법이 다양하기때문에 생략

createSlice (웬만하면 이거 쓰면 됨)

앞서 설명한 createAction, createReducer 가 내부적으로 모두 포함되어 있는 createSlice 는 가장 쉽게 액션과 리듀서 함수를 만들 수 있는 함수이다.

// 슬라이스
const todosSlice = createSlice({
  name: 'todos',
  initialState: [],
  reducers: {
    addTodo(state, action) {
      const { id, text } = action.payload
      state.push({ id, text, completed: false })
    },
    toggleTodo(state, action) {
      const todo = state.find(todo => todo.id === action.payload)
      if (todo) {
        todo.completed = !todo.completed
      }
    }
  }

// 디스패치용 액션크리에이터
export const { addTodo, toggleTodo } = todosSlice.actions

// 리듀서 내보내기
export default todosSlice.reducer

createSlice가 반환하는 객체

{
  name: "todos",
  reducer: (state, action) => newState,
  actions: {
    addTodo: (payload) => ({type: "todos/addTodo", payload}),
    toggleTodo: (payload) => ({type: "todos/toggleTodo", payload})
  },
  caseReducers: {
    addTodo: (state, action) => newState,
    toggleTodo: (state, action) => newState,
  }
}

Redux 공식 문서 : slice 사용 예시

cra-template-redux/template at master · reduxjs/cra-template-redux

// counterSlice.js

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

// createAsyncThunk API 비동기 샘플 함수
function fetchCount(amount = 1) {
  return new Promise((resolve) =>
    setTimeout(() => resolve({ data: amount }), 500)
  );
}

// state 초기 값
const initialState = {
  value: 0,
  status: 'idle',
};

// 아래 함수를 thunk라고 하며 비동기 로직을 수행할 수 있다.
// 일반적인 동작처럼 dispatch(incrementAsync(10))할 수 있다. 
// 이는 첫 번째 인수로 디스패치 기능을 가진 thunk를 부를 것이다. 
// 그런 다음 비동기 코드를 실행하고 다른 액션을 디스패치할 수 있다. 
// 씽크는 일반적으로 비동기 요청을 만드는 데 사용된다.

// createAsyncThunk(액션, 비동기 콜백);
export const incrementAsync = createAsyncThunk(
  'counter/fetchCount',
  async (amount) => {
    const response = await fetchCount(amount);

    return response.data;
  }
);

// slice 객체 생성
export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {
      // 직접 state를 변경하는 것처럼 보이지만,
      // 함수 내부에서 Immer 라이브러리를 사용하기때문에 불변성이 유지된다.
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    // 사용처: dispatch(incrementByAmount(payload));
    incrementByAmount: (state, action) => {
      state.value += action.payload;
			// state.value += action.payload.value;
			// (개인적으로는 payload에 직접적으로 값을 넣기보다는 객체로 넣어주는 것이 더 가독성이 좋아보임)		
    },
  },
  // 'extraReducers' 필드를 사용하여 slice는 다른 곳에서 정의된 action들을 처리할 수 있다.
  // (createAsyncThunk 또는 다른 slice에서 생성된 action)
  extraReducers: (builder) => {
    builder
      .addCase(incrementAsync.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(incrementAsync.fulfilled, (state, action) => {
        state.status = 'idle';
        state.value += action.payload;
      });
  },
}); 

// dispatch의 인자로 들어갈 actionCreator
export const { increment, decrement, incrementByAmount } = counterSlice.actions;

// useSelector를 통해 값을 가져올 때 value만 가져오도록 하는 편의성을 위한 함수
export const selectCount = (state) => state.counter.value;

// 직접 thunk를 쓸 수 있는데, 이때 동기 로직과 비동기 로직을 모두 포함할 수 있다.
// 다음은 현재 state를 기준으로 action을 조건부로 디스패치하는 예시이다. (비동기 아님)
export const incrementIfOdd = (amount) => (dispatch, getState) => {
	// store.getState() === store.state // 현재의 state를 반환함
	// const currentValue = getState().counter.value;
  const currentValue = selectCount(getState()); 
  if (currentValue % 2 === 1) {
    dispatch(incrementByAmount(amount));
  }
};

export default counterSlice.reducer;
// Counter.js

import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
  decrement,
  increment,
  incrementByAmount,
  incrementAsync,
  incrementIfOdd,
  selectCount,
} from './counterSlice';

export function Counter() {
	// const count = useSelector((state) => state.couter.value);
  const count = useSelector(selectCount);
  const dispatch = useDispatch();

	dispatch(increment()) // store.state.counter.value 1 증가
	dispatch(decrement()) // store.state.counter.value 1 감소
	// payload를 객체로 넣고싶다면 : dispatch(incrementByAmount({ value: 2 })); 
	dispatch(incrementByAmount(2)) //  store.state.counter.value 2 증가
	dispatch(incrementAsync(2)) // 5초 뒤 스토어의 state.counter.value  증가
	dispatch(incrementIfOdd(2)) // 아무일도 일어나지 않음
	dispatch(incrementIfOdd(3)) // store.state.counter.value 3 증가

  return <></>;
}
// store.js

import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});

0개의 댓글