(Redux) Toolkit

Mirrer·2022년 11월 20일
0

Redux

목록 보기
6/7
post-thumbnail

Redux의 문제점

Redux는 현 시점 가장 높은 시장 점유율을 차지하고 있는 대표적인 JavaScript 상태 관리 라이브러리이다.

하지만 Redux를 사용하면서 발생되는 다음과 같은 단점들은 이전부터 많은 Front-end 개발자의 고민이었다.

  • 스토어 구성의 높은 복잡성
  • 많은 양의 관련 패키지 및 사용 방법
  • 보일러플레이트 (많은 상용구 코드의 필요성)

그래서 Redux 공식 개발팀에서는 위와 같은 문제를 해결하기 위해 Redux-Toolkit이라는 도구를 개발했다.


Redux Toolkit

Redux 로직을 효율적으로 사용할 수 있는 표준 방식

공식 문서에 따르면 Redux-Toolkit기존 Redux의 문제점을 보완하기 위해 개발된 도구이다.

즉, 효율적인 Redux 개발을 위해 Redux 공식 팀에서 개발한 공식적이고 독단적인 배터리 포함 도구 세트이다.

Redux Toolkit의 특징은 다음과 같다.

  • Simple : 스토어 설정, 리듀서 생성, 변경 불가능한 업데이트 로직 등과 같은 일반적인 사용 사례를 단순화하는 유틸리티가 포함되어 있다.

  • Opinionated : 스토어 설정을 위한 좋은 기본값을 제공하며 가장 일반적으로 사용되는 Redux addon이 내장되어 있다.

  • Powerful : ImmerAutodux와 같은 라이브러리에서 영감을 얻어 "변형" 로직으로 불변성 업데이트 논리를 작성하고 state 전체를 slice로 자동으로 생성 할 수도 있다.

  • Effective : 적은 코드많은 작업이 가능하다.


사용 방법

본 포스팅에서는 Redux ToolkitNext.js에 적용하는 방식으로 설명

본격적인 사용에 앞서 현재 프로젝트 구조는 다음과 같다.

.
├── components
├── next.config.js
├── node_modules
├── package-lock.json
├── package.json
├── pages
├── public
├── reducers
│ └── title.js (리듀서 모듈)
│ └── desc.js (리듀서 모듈)
│ └── index.js (리듀서 모듈 통합)
├── store
│ └── configureStore.js (store 생성 && wrapper 생성)


Package Install

아래 npm명령어를 통해 Redux Toolkit 관련 패키지를 설치한다.

npm i @reduxjs/toolkit
npm i react-redux
npm i next-redux-wrapper
npm i redux-logger --save-dev # (선택사항)

createSlice

Redux-ToolkitcreateSlice를 사용하여 Reducer 모듈을 작성한다.

createSlicecreateAction, createReducer 함수가 내부적으로 사용된다.

createSlice: actionreducer를 함께 정의
=> createSlice = createAction + createReducer

또한 선언된 슬라이스 name을 따라 Reducer와 그에 상응하는 액션 생성자 및 타입을 자동으로 생성한다.

정의를 마치면 Reducer와 액션 생성함수다른 컴포넌트, 모듈에서 사용하기 위해 export한다.

// reducers/title.js
import { createSlice } from '@reduxjs/toolkit';

const initialState = {
  titleText: '',
};

const titleSlice = createSlice({
  name: 'title',
  initialState,
  reducers: {
    editTitle: (state, action) => { // title/editTitle
      state.titleText = action.payload;
    },
  },
});

export const { editTitle } = titleSlice.actions;
export default titleSlice.reducer;
// reducers/desc.js
import { createSlice } from '@reduxjs/toolkit';

const initialState = {
  descText: '',
};

const descSlice = createSlice({
  name: 'desc',
  initialState,
  reducers: {
    editDesc: (state, action) => { // desc/editDesc
      state.descText = action.payload;
    },    
  },
});

export const { editDesc } = descSlice.actions;
export default descSlice.reducer;

만약 액션의 내용을 Reducer 실행 이전에 편집하기를 원한다면 prepare를 사용한다.

// reducers/title.js
import { createSlice } from '@reduxjs/toolkit';

const initialState = {
  titleText: '',
};

const titleSlice = createSlice({
  name: 'title',
  initialState,
  reducers: {
    editTitle: (state, action) => { 
      state.titleText = action.payload;
    },
    // Reducer 실행 전 액션의 내용 편집
    prepare: (text) => {
      const name = '홍길동'
      return { 
        payload: { name, text } 
      }
    },
  },
});

export const { editTitle } = titleSlice.actions;
export default titleSlice.reducer;

combineReducers

reducers/index.js파일에서 combineReducers를 사용하여 앞서 생성한 Reducer 모듈을 통합한다.

해당 과정에서 Server Side Rendering작업HYDRATE라는 액션을 통해 서버의 Store와 클라이언트의 Store를 통합한다.

combineReducers는 정의한 리듀서 모듈들을 결합
=> 추가되는 리듀서 모듈은 combineReducers함수의 인자로 전달

// reducers/index.js
import { combineReducers } from "@reduxjs/toolkit";
import { HYDRATE } from "next-redux-wrapper";

import title from './title';
import desc from './desc';

const reducer = (state, action) => {
  if (action.type === HYDRATE) {
    return {
        ...state,
        ...action.payload
    };
  }
  return combineReducers({ // Reducer 모듈 통합
    title,
    desc,      
  })(state, action);
}

export default reducer;

configureStore

configureStoreReduxcreateStore를 추상화한 스토어 구성 함수이다.

configureStore에서는 Redux-Toolkit에서 기본으로 제공하는 기능들을 추가로 정의할 수 있으며, 종류는 다음과 같다.

  • reducer: 단일 함수를 전달하여 스토어의 루트 리듀서(root reducer)로 바로 사용
  • middleware: 리덕스 미들웨어를 담는 배열
  • devTools: Redux DevTools 활성화 여부
  • preloadedState: 스토어의 초기값 설정
  • enchaners: 미들웨어 보다 먼저 추가될 store enhancer

store/configureStore.js파일에서 configureStore, createWrapper함수를 사용하여 Store, Wrapper를 생성한다.

// store/configureStore.js
import { configureStore } from '@reduxjs/toolkit';
import { createWrapper } from "next-redux-wrapper";
import logger from 'redux-logger';

import reducer from '../reducers';

// Store 생성
const makeStore = (context) => configureStore({ 
    reducer,
    // logger 미들웨어 추가
    middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger),
    // 개발모드일 때 Redux DevTools 활성화
    devTools: process.env.NODE_ENV !== 'production',
});

// Wrapper를 생성하여 생성된 Store를 바인딩
export const wrapper = createWrapper(makeStore, { 
    debug: process.env.NODE_ENV !== 'production',
});

Higher Order Component

위에서 생성한 WrapperwithRedux 고차 컴포넌트(HOC, Higher Order Component)_app.js를 감싼다.

해당 과정을 통해 각 페이지에서 getStaticProps, getServerSideProps...등등 함수 내에서 스토어 접근을 가능하게 한다.

// pages/_app.js
import React from 'react';
import { ThemeProvider } from 'styled-components';

import "../styles/variables.less";
import GlobalStyles from '../styles/GlobalStyles';
import Theme from '../styles/Theme';
import { wrapper } from '../store/configureStore'; // wrapper 불러오기

const App = ({ Component, pageProps }) => {
  return (
    <>
      <GlobalStyles />
      <ThemeProvider theme={Theme}>
        <Component {...pageProps} />
      </ThemeProvider>
    </>
  );
};

export default wrapper.withRedux(App); // Wrapper의 withRedux HOC

Check Result

위의 과정을 모두 마친 뒤 해당 컴포넌트에서 확인해보면 결과는 다음과 같다.

import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Typography, Input } from 'antd';

import AppLayout from '../components/AppLayout';
import { editTitle } from '../reducers/title'; // 액션 생성함수 export
import { editDesc } from '../reducers/desc'; // 액션 생성함수 export

const Home = () => {    
  const dispatch = useDispatch();
  const { titleText } = useSelector((state) => state.title); // state 불러오기
  const { descText } = useSelector((state) => state.desc); // state 불러오기

  const onChangeTitle = useCallback((e) => {
    dispatch(editTitle(e)); // 액션 생성함수를 Dispatch    
  }, []);
  
  const onChangeDesc = useCallback((e) => {
    dispatch(editDesc(e)); // 액션 생성함수를 Dispatch   
  }, []);

  return (
    <AppLayout>        
      <Typography.Title>Title</Typography.Title>       
      <Typography.Text strong>{titleText}</Typography.Text>
      <Input.Search enterButton="변경" type='text' onSearch={onChangeTitle} />      
        
      <Typography.Title>DESC</Typography.Title>       
      <Typography.Text strong>{descText}</Typography.Text>
      <Input.Search enterButton="변경" type='text' onSearch={onChangeDesc} />              
    </AppLayout>
  )
};

export default Home;


Asynchronous Operations

Redux-Toolkit의 비동기 작업

Redux-Toolkit은 내부적으로 redux-thunk 라이브러리를 사용하는 createAsyncThunk를 통해 비동기 작업을 수행한다.

단, createAsyncThunkcreateSlicereducers와 달리 자동으로 action creator가 생성되지 않아 수동으로 생성해야 한다.


extraReducers

extraReducerscreateSlice가 생성한 액션 타입 외 다른 액션 타입에 응답할 수 있도록 한다.

즉, 슬라이스 리듀서에 맵핑된 내부 액션 타입이 아닌, 외부의 액션을 참조하는 용도로 사용한다.

import { createSlice } from '@reduxjs/toolkit';
import { loadSales } from '../actions/sales'; // action creator

const initialState = {
  value: null,
  loadSalesLoading: false,
  loadSalesDone: false,
  loadSalesError: null,
};

const salesSlice = createSlice({
  name: 'sales',
  initialState,
  reducers: {},
  extraReducers: (builder) => builder
    .addCase(loadSales.pending, (state) => { // Loading
      state.loadSalesLoading = true;
      state.loadSalesDone = false;
      state.loadSalesError = null;
    })
    .addCase(loadSales.fulfilled, (state, action) => { // Success
      state.value = action.payload.value;
      state.loadSalesLoading = false;
      state.loadSalesDone = true;
    })
    .addCase(loadSales.rejected, (state, action) => { // Error
      state.loadSalesLoading = false;
      state.loadSalesError = action.error.message;
    })
});

export default salesSlice.reducer;

또한 extraReducers에 추가된 케이스 리듀서는 Promise의 진행 상태에 따라 아래와 같은 3가지 상태로 리듀서를 실행할 수 있다.

pending: 로딩
fulfilled: 성공
rejected: 실패


createAsyncThunk

createAsyncThunk액션 타입 문자열Promisereturn하는 콜백 함수를 인자로 받는다.

그리고 주어진 액션 타입을 접두어로 사용하여 Promise 생명 주기 기반의 액션 타입을 생성한다.

import axios from 'axios';
import { createAsyncThunk } from '@reduxjs/toolkit';

export const loadSales = createAsyncThunk('sales/loadSales', async (data, { rejectWithValue }) => {
  try {
    // response는 총 판매량을 조회하여 return
    // {"value":총 판매량}
    const response = await axios.get('https://api.countapi.xyz/hit/opesaljkdfslkjfsadf.com/visits');
    return response.data;
  } catch (error) {
    // 오류가 발생하면 error를 return
    return rejectWithValue(error.response.data);
  }
});

위의 과정을 통해 작성된 비동기 작업은 필요한 시점에 컴포넌트에서 Dispatch하여 사용한다.

import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Typography, Input, Button } from 'antd';

import AppLayout from '../components/AppLayout';
import { editTitle } from '../reducers/title';
import { editDesc } from '../reducers/desc';
import { loadSales } from '../actions/sales'; // action 불러오기

const Home = () => {    
  const dispatch = useDispatch();
  const { titleText } = useSelector((state) => state.title);
  const { descText } = useSelector((state) => state.desc);
  // 비동기 작업의 state
  const { value, loadSalesLoading } = useSelector((state) => state.sales); 

  const onChangeTitle = useCallback((e) => {
    dispatch(editTitle(e));    
  }, []);
  
  const onChangeDesc = useCallback((e) => {
    dispatch(editDesc(e));    
  }, []);

  const onClickSales = useCallback(() => {
    // 비동기 action dispatch
    dispatch(loadSales());    
  }, []);  

  return (
    <AppLayout>        
      <Typography.Title>Title</Typography.Title>       
      <Typography.Text strong>{titleText}</Typography.Text>
      <Input.Search enterButton="변경" type='text' onSearch={onChangeTitle} />      
        
      <Typography.Title>DESC</Typography.Title>       
      <Typography.Text strong>{descText}</Typography.Text>
      <Input.Search enterButton="변경" type='text' onSearch={onChangeDesc}/>        
      
      // 총 판매량 조회
      <Typography.Title>TOTAL SALES</Typography.Title>
        {
          loadSalesLoading 
          ? <Typography.Text strong>로딩 중...</Typography.Text>  
          : <Typography.Text strong>{value}</Typography.Text>
        }        
      <Button type='primary' onClick={onClickSales}>조회</Button>      
    </AppLayout>
  )
};

export default Home;


참고 자료

Redux toolkit - 생활코딩
Redux Toolkit | Redux Toolkit
Redux Toolkit Tutorial - Codevolution

profile
memories Of A front-end web developer

0개의 댓글