React Redux

김대은·2022년 8월 7일
1

Redux의 특징

  • 상태를 단방향으로만 바꿀 수 있다. Flux 패턴
    기존의 어플리케이션에서 보편적으로 사용되는 패턴은 MVC였습니다.

    Model 에 데이터를 정의해 두고, Controller를 이용해
    Model 데이터를 생성 / 조회 / 수정 / 삭제 하고, 변경된 데이터는 View에 출력되면서 사용자에게 전달됩니다.

    이 패턴의 문제점은 규모가 커질수록 데이터 흐름의 복잡도가 무지막지하게 늘어난다는 점입니다.

    또한 MVC 패턴은 데이터의 변경 사항을 신속하게 전파하기가 어렵습니다.
    모델이 늘어날수록 전파해야 할 대상도 함께 늘어나기 때문인데요.

    페이스북은 이 문제를 해결하기 위해 flux 라는 패턴을 만들었습니다.
    Model이 View를 반영하고, View가 Model을 변경하는 양방향 데이터 흐름에서 벗어나 단방향으로만 데이터를 변경할 수 있도록 만든 겁니다.

  • action이 dispatch 되면 기록이 남기 때문에 history기능을 통해 에러가 발생했을때 디버깅 하기 쉽고, 기록을 통해 action 이전의 상태로 되돌릴 수 있다.

  • reducer에서 action을 받아서 해당 action 에 맞게 state의 새로운 객체를 만들어서 기존의 상태를 업데이트한다.

redux의 사용 목적

보통 redux를 global한 상태 관리 개념으로 생각하고 사용하곤 하는듯 하다.

하지만, 예를 들어 게시판의 리스트 정보의 경우 redux에 저장해서 게시물 상세페이지에 이동했다가 다시 게시판 리스트로 돌아왔을때 서버로 데이터를 요청하지 않고, redux에 있는 리스트 정보를 가져오게 할 수 있다.
게시물 상세페이지에 대한 정보도 redux에 담아서 caching해서 사용할수 있다.

리덕스의 폴더 구조

리덕스에서 액션과 리듀서의 코드가 길어질 수 있기 때문에 별도의 폴더와 파일로써 분리를 해주는것이 좋다.
(action과 reducer는 매개변수와 함수 내부의 변수만 참조하는 순수함수이기 때문에 분리하기 용이하다)
action 객체를 나누는 기준은 initialState의 항목을 기준으로 한다.
(공통화 시킬 수 있는 부분은 최대한 공통화 처리)

리덕스 -미들웨어

액션은 객체로, 기본적으로 동기방식으로 처리되고 dispatch함수는 액션을 받아서 처리 하기 때문에 중간단계에서 비동기를 처리할 틈이 없다.
하지만 dispatch 와 reducer사이에 동작하는 middleware를 사용하면
사이에서 비동기 처리를 해 줄수있다.
이때 사용되는 middleware 에는 redux-thunk 와 redux-saga가 있다.

middlware는 비동기 처리로 많이 사용되긴 하지만, 반드시 비동기 처리를 위해 사용되는것이 아닌, dispatch와 reducer 사이에 특정 처리를 하기 위해서 사용됨을 알아두자.

react-redux

React 와 Redux를 연결 시키기 위해서는 react-redux를 사용해야한다.

import { Provider } from 'react-redux';
import store from '스토어 경로';
...
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>
)

최상위 컴포넌트를 Provider로 감싸서 store를 넣어주면
이제 하위 컴포넌트들에서도 dispatch의 접근이 가능하다.

import { useDispatch, useSelector } from 'react-redux';

// state : initial state
const user = useSelector((state) => {
  state.user.data;
});
const posts = useSelector((state) => {
  state.posts;
});
const dispatch = useDispatch();

immer

리덕스에서는 불변성을 지키기 위해서 스프레드 연산자를 사용하는 경우가 많다.
하지만 스프레드 연산자를 많이 사용하게 되면 코드가 복잡해지고 가독성이 떨어진다.
따라서 immer 라는 라이브러리를 사용해서 직관적으로 불변성을 지키면서 상태 데이터를 업데이트 할 수있다.
기본적으로 RTK에는 immer가 내장되어있다.

// immer의 기본 형태
// draft 부분을 prevState의 복사본으로 보면 된다.으로 보면 된다.
nextState = produce(prevState, (draft) => {});

기존 코드

const reducer = (prevState, action) => {
  switch (action.type) {
    case 'LOG_IN':
      return {
        ...prevState,
        user: action.data
      };
    case 'LOG_OUT':
      return {
        ...prevState,
        user: null
      };
    case 'ADD_POST':
      return {
        ...prevState,
        posts: [...prevState.posts, action.data]
      };
    default:
      return prevState;
  }
};

적용 코드

import produce from 'immer';

const reducer = (prevState, action) => {
  return produce(prevState, (draft) => {
    switch (action.type) {
      case 'LOG_IN':
        draft.data = action.data;
        draft.isLoggingIn = true;
        break;
      case 'LOG_OUT':
        draft.data = null;
        draft.isLoggingIn = false;
        break;
      case 'ADD_POST':
        // 사본 데이터에 action에서 받은 데이터를 push해준다.
        draft.push(action.data);
        break;
      default:
        break;
    }
  });
};

Redux Toolkit

redux toolkit은 redux에서 자주 쓰이는 기능을 리덕스 팀에서 라이브러리로 만들었는데,
redux toolkit을 사용하면 redux toolkit을 사용하면, 기존의 redux, devtools, redux-thunk, redux-saga, immer를 redux toolkit의 내장된 기능으로 사용할 수 있다.
중요한 메인 기능은 createSlice와 createAsyncThunk이다.

configuerStore

기존의 createStore를 대체해서 configuerStore를 사용하면,
Thunk, devtool을 자동으로 연결해준다.

import reducer from "./modules/reducer";
import { configureStore } from "@reduxjs/toolkit";

const store = configureStore({ reducer });

export default store;
import { combineReducers } from "redux";
import userSlice from './user';
import postSlice from './post';

export default combineReducers({
  // slice 자체가 아닌 slice안의 reducer를 넣어줘야 한다.
  user: userSlice.reducer,
  posts: postSlice.reducer
});

createSlice

reducer와 action이 분리되어 있었는데, 합친 개념이 slice이다.
특정 action은 특정 reducer에 종속되어있다.

createSlice는 하나의 slice 객체를 인자로 받는다.

createSlice({name,initalState,reducers,extreReducers})

  • name : string을 넣어 prefix로 사용
  • initailState : defaultState가 들어간다.
  • reducres : 해당 reducer들을 만들면 자동으로 slice.action에 reducers에서 만든 reducer에 대한 actionCreator 함수가 들어 있다.
    동기적인 작업에 사용한다.
  • extraReducers : 외부에서 만들어진 action을 통해 현재 slice에서 사용하는 state에 변경을 가하는 경우 처리받는 리듀서 이며, 비동기 작업에 주로 사용된다.
const users = createSlice({
  name: "usersReducer",
  initialState,
  reducers: {
  //내부 action ,동기 action
    getUsersStart: (state, action) => ({ ...state, loading: true }),
    getUsersSuccess: (state, action) => ({
      ...state,
      loading: false,
      data: action.payload,
    }),
    getUsersRemove: (state, action) => ({
      ...state,
      loading: false,
      data: [],
    }),
   },
  extraReducers: (builder) => {
  // 외부,비동기 action
    builder
      .addCase(getUsersThunk.pending, (state, action) => {
        state.loading = true;
      })
      .addCase(getUsersThunk.fulfilled, (state, action) => {
        state.loading = false;
        state.data = action.payload;
      })
      .addCase(getUsersThunk.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error; // action.error인 것을 주의
      });
  },
});


export const { getUsersRemove, getUsersSuccess, getUsersStart } = users.actions;

reducer 작성 방식 2가지

// slice reducers 방법01 : 있던 값을 바꾸는 형식 (state에 직접적으로 변경을 가함, 함수방식)
// 직접 변경을 가하기 때문에 기존값을 풀어써주는 전개 연산자가 필요가 없음
const users = createSlice({
  name: "usersReducer",
  initialState,
  reducers: {
    getUsersStart: (state, action) => {
      state.loading = true;
    },
    getUsersSuccess: (state, action) => {
      state.loading = false;
      state.data = action.payload;
    },
    getUsersRemove: (state, action) => {
      state.loading = false;
      state.data = [];
    },
  },
  extraReducers: {},
});
export const { getUsersRemove, getUsersSuccess, getUsersStart } = users.actions;

// slice reducers 방법02 : 기존 state를 복사하여 새로운 값을 만들어서 state에 세팅하는 방식(return 방식)
// 기존 값을 가져와 반영해야 함으로 전개연산자 필요
const users = createSlice({
  name: "usersReducer",
  initialState,
  reducers: {
    getUsersStart: (state, action) => ({ ...state, loading: true }),
    getUsersSuccess: (state, action) => ({
      ...state,
      loading: false,
      data: action.payload,
    }),
    getUsersRemove: (state, action) => ({
      ...state,
      loading: false,
      data: [],
    }),
  },
  extraReducers: {},
});
export const { getUsersRemove, getUsersSuccess, getUsersStart } = users.actions;

extraReducer 작성법 2가지

  1. map Object notation 방식
// 방식1: map Object notation
// 1. return 방식 (전개 연산자 필요)
const users = createSlice({
  name: "usersReducer",
  initialState,
  reducers: {},
  extraReducers: {
    [getUsersThunk.pending]: (state, action) => ({
      ...state,
      loading: true,
    }),
    [getUsersThunk.fulfilled]: (state, action) => ({
      ...state,
      loading: false,
      data: action.payload,
    }),
    [getUsersThunk.rejected]: (state, action) => ({
      ...state,
      loading: false,
      error: action.error,
    }),
  },
});

export default users;


// 방식1: map Object notation
// 2. 함수 및 할당 방식 (전개 연산자 불필요)
const users = createSlice({
    name: 'usersReducer',
    initialState,
    reducers: {},
    extraReducers: {
        [getUsersThunk.pending]: (state, action) => {
            state.loading = true;
        },
        [getUsersThunk.fulfilled]: (state, action) => {
            state.loading = false;
            state.data = action.payload;
        },
        [getUsersThunk.rejected]: (state, action) => {
            state.loading = false;
            state.error = action.error;
        },
    },
});

export default users;
  1. builder callback notation 방식

// 방식2: builder callback notation
// 1. return 방식 (전개 연산자 필요함)
const users = createSlice({
    name: 'usersReducer',
    initialState,
    reducers: {},
    extraReducers: (builder) => {
        builder
            .addCase(getUsersThunk.pending, (state, action) => ({
                ...state,
                loading: true,
            }))
            .addCase(getUsersThunk.fulfilled, (state, action) => ({
                ...state,
                loading: false,
                data: action.payload,
            }))
            .addCase(getUsersThunk.rejected, (state, action) => ({
                ...state,
                loading: false,
                error: action.error,
            }));
    },
});

export default users;

// 방식2: builder callback notation
// 2. 함수 및 할당 방식 (전개 연산자 불필요)

const users = createSlice({
    name: 'usersReducer',
    initialState,
    reducers: {},
    extraReducers: (builder) => {
        builder
            .addCase(getUsersThunk.pending, (state, action) => {
                state.loading = true;
            })
            .addCase(getUsersThunk.fulfilled, (state, action) => {
                state.loading = false;
                state.data = action.payload;
            })
            .addCase(getUsersThunk.rejected, (state, action) => {
                state.loading = false;
                state.error = action.error;
            });
    },
});

export default users;

createAsyncThunk

toolkit에서 제공하는 createAsyncThunk를 활용해서 비동기 작업을 구현할 수 있다.

createAsyncThunk(type,payloadCreator,options)

  • type : 해당 요청의 type명으로, prefix를 포함해서 작성해 주어야한다.
  • payloadCreator : actionCreator로 payload와 함께 보내져 요청되는 비동기 함수 실행 부분으로 2개의 인자를 받는다.
    • arg : 첫번째 파라미터로 지정하면, actionCreator를 사용하면서 보낼 payload를 받아 실행하고자 하는 비동기 함수를 구성하는데 사용될 사용자 입력으로 활용할 수 있다.
    • thunkAPI : dispatch,getState,rejectWithValue,fulfillWithValue 등의 함수를 실행할 수 있는 API 묶음
  • 기본적으로 해당 함수의 return 값은 fulfiiled로 처리하여 payload로 보내지고,
    error는 thunkAPI,rejectWithValue(error)를 통해서 받아 action.error로 보내진다.
export const getUsersThunk = createAsyncThunk(
  "users/getUsersThunk",
  async (thunkAPI) => {
    try {
      const res = await axios.get("https://api.github.com/users");
      return res.data;
    } catch (error) {
      return thunkAPI.rejectWithValue(error);
    }
  }
);

본 게시물은 아래 링크를 참조하여 작성 하였습니다.
참조한 링크

profile
매일 1% 이상 씩 성장하기

0개의 댓글