Redux 컨셉 정리

pengooseDev·2023년 2월 17일
1
post-thumbnail
post-custom-banner

1. rootReducer 만들기

  • redux/modules에 index.ts 생성.

  • modules에서 만든 reducer들을 모두 import해 rootReducer에 DI해주면 된다.
    (reducer은 하단에서 다룸)

  • 작성이 완료된 rootReducer은 export default

import { combineReducers } from 'redux';
import toDoReducer from './toDoDatas';

const rootReducer = combineReducers({
  toDoReducer,
});

export default rootReducer;

export type RootState = ReturnType<typeof rootReducer>;

2. store 생성 및 rootReducer DI

  1. store를 생성하고, import한 rootReducer를 DI해준다.
  2. 이후 Provider를 import해 방금 만든 store를 DI해준다.
import rootReducer from './redux/modules/index';
import { Provider } from 'react-redux';
import { createStore } from 'redux';

const store = createStore(rootReducer);

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);

root.render(
  <React.StrictMode>
    <Provider store={store}>
      <RecoilRoot>
        <App />
      </RecoilRoot>
    </Provider>
  </React.StrictMode>
);

action함수 작성 (ducks pattern)

/* Action types 선언 */
const DONE = 'toDo/DONE' as const;
const DISCARD = 'toDo/DISCARD' as const;
const REMOVE = 'toDo/REMOVE' as const;
const ADD = 'toDo/ADD' as const;

/* dispatch로 넘어오는 payload Interface 선언 */
interface addPayload {
  addTitle: string;
  addDescription: string;
}

interface donePayload {
  idDone: number;
}

interface discardPayload {
  idDiscard: number;
}

interface removePayload {
  idRemove: number;
  removeBoardKey: string;
}

/* Action 함수 작성 */
export const done = ({ idDone }: donePayload) => ({
  type: DONE,
  payload: { idDone },
});

export const discard = ({ idDiscard }: discardPayload) => ({
  type: DISCARD,
  payload: { idDiscard },
});

export const remove = ({ idRemove, removeBoardKey }: removePayload) => ({
  type: REMOVE,
  payload: { idRemove, removeBoardKey },
});

export const add = ({ addTitle, addDescription }: addPayload) => ({
  type: ADD,
  payload: { addTitle, addDescription },
});

/* Types */
type ToDosAction =
  | ReturnType<typeof done>
  | ReturnType<typeof discard>
  | ReturnType<typeof remove>
  | ReturnType<typeof add>;

export interface task {
  title: string;
  description?: string;
  id: number;
}

export interface toDoState {
  [key: string]: task[] | [];
}

/* 1. Initial State */
const initialToDos: toDoState = {
  toDo: [{ title: 'todo1', description: 'des1', id: new Date().getTime() }],
  done: [],
};

/* 2. Reducer : state를 action의 type에 따라 바꾸는 함수 */
const toDoReducer = (state: toDoState = initialToDos, action: ToDosAction) => {
  switch (action.type) {
    case DONE:
      const {
        payload: { idDone },
      } = action;
      const oldDoneData = { ...state };
      const oldDoneArr = [...oldDoneData['toDo']];
      let targetDoneIndex;

      oldDoneArr.map((v, i) => {
        if (v.id === idDone) return (targetDoneIndex = i);
      });

      if (!targetDoneIndex && targetDoneIndex !== 0) return state;

      const targetDoneTask = oldDoneArr.splice(targetDoneIndex, 1)[0];
      const doneArr = [...state['done'], targetDoneTask];
      return {
        toDo: oldDoneArr,
        done: doneArr,
      };

    case DISCARD:
      const {
        payload: { idDiscard },
      } = action;
      const oldDiscardData = { ...state };
      const oldDiscardArr = [...oldDiscardData['done']];
      let targetDiscardIndex;

      oldDiscardArr.map((v, i) => {
        if (v.id === idDiscard) return (targetDiscardIndex = i);
      });

      if (!targetDiscardIndex && targetDiscardIndex !== 0) return state;

      const targetTask = oldDiscardArr.splice(targetDiscardIndex, 1)[0];
      const toDoArr = [...state['toDo'], targetTask];
      return {
        toDo: toDoArr,
        done: oldDiscardArr,
      };

    case REMOVE:
      const {
        payload: { idRemove, removeBoardKey },
      } = action;
      const oldRemoveData = { ...state };
      const oldRemoveArr = [...oldRemoveData[removeBoardKey]];
      let targetIndex;

      oldRemoveArr.map((v, i) => {
        if (v.id === idRemove) return (targetIndex = i);
      });

      if (!targetIndex && targetIndex !== 0) return state;
      oldRemoveArr.splice(targetIndex, 1);

      return {
        ...state,
        [removeBoardKey]: oldRemoveArr,
      };

    case ADD:
      const {
        payload: { addTitle, addDescription },
      } = action;

      const oldToDoArr = state['toDo'];
      const newToDoArr = [
        ...oldToDoArr,
        {
          title: addTitle,
          description: addDescription,
          id: new Date().getTime(),
        },
      ];

      const newToDosData = {
        ...state,
        toDo: newToDoArr,
      };

      return newToDosData;

    default:
      return state;
  }
};

export default toDoReducer;

HTTPS 통신 생각하면 간단하다. req.method에 따라 분기처리를 해주듯, FE에서 요청한 action.type에 맞게 분기처리를 해준다.


dispatch 날리기

import { useDispatch } from 'react-redux';
import { add } from './redux/modules/toDoDatas';

const AddTaskToggle = () => {
  const dispatch = useDispatch();// 1. dispatch 생성.
	
  const submitHandler = (event: React.FormEvent) => {
    event.preventDefault();
    dispatch(add({ addTitle: title, addDescription: description })); //add action함수로 dispatch 날리기.

    setTitle((prev) => '');
    setDescription((prev) => '');
  };
}

useDispatch를 이용해 원하는 action type으로 dispatch를 날린다.

dispatch(add({ addTitle: title, addDescription: description })); //add action함수로 dispatch 날리기.

위 코드는 결국 아래와 같은 의미이다.

dispatch({
  type: ADD,
  payload: { addTitle, addDescription },
});

dispatch는 store에 DI한 rootReducer들을 순회하며 action에 맞는 Type을 확인하고, 분기에 따라 함수를 작동한다.


selector

import { useSelector } from 'react-redux';
import { RootState } from '../redux/modules';
import { toDoState } from '../redux/modules/toDoDatas';

const Board = ({ boardKey ) => {
  const toDoDatas = useSelector(
    (state) => state.toDoReducer
  );

  return (
    <Wrapper>
      <Title>{boardKey}</Title>
      {toDoDatas[boardKey].map((v, i) => (
        <Card data={v} boardKey={boardKey} key={v.id} />
      ))}
    </Wrapper>
  );
};

useSelector를 이용해, rootReducer에서 원하는 데이터를 가져온다.


생각보다 간단할지도?

redux의 러닝커브가 높다는 이야기가 많아서 걱정했지만, 결국 recoil의 컨셉과 큰 차이가 없는 것처럼 느껴졌다. recoil로 작성했던 코드를 redux로 migration하는 과정에서 큰 이질감을 느끼지는 못하였다.

또한, NextJS에서 해주던 HTTP 통신 분기처리와 비슷하다고 느끼며, redux의 컨셉 자체가 굉장히 납득이 가는 타당성을 가지고있다는 느낌이었다.

물론, 계속 사용하면서 어떤 문제점이 있는지, 다른 Atomic기반 상태관리 도구랑 어떤 차별점이 있는지 알아가야할 것이다.

추후 프로젝트의 상태관리는 redux 위주로!

post-custom-banner

0개의 댓글