원티드 프리온보딩 챌린지 - 리팩토링 (3)

KINA KIM·2022년 8월 15일
0
post-thumbnail

1. Redux 적용

🎯프론트엔드 구현 포인트 정리하기 - 1. 상태 관리에서 회고했던 내용을 바탕으로 덕스 패턴을 사용해서 redux를 적용해보고자 한다. redux 설치는 아래 명령어를 통해 가능하다. react-redux는 typescript를 자동으로 지원하지 않기 때문에 @types/를 앞에 붙여서 설치한다.

yarn add redux react-redux @types/react-redux

client/src/module 폴더 안에 세팅할 예정이다. 덕스 패턴을 사용할 예정이기 때문에 파일 하나에서 액션 타입, 액션 생성 함수, 초기 상태, 리듀서를 모두 관리한다.

(1) 필요한 Action 함수 파악하기

현재 큰 기능은 로그인/회원가입/투두 관리이고, 상태 관리가 필요한 부분은 투두 관리이다. 그리고 투두 관리에는 전체 투두를 관리하는 TodoList State와 개별 투두를 관리하는 Todo State 두 개의 상태가 존재한다.

TodoList state는 투두 리스트 불러오기(TODOS_GET), 새로운 투두의 추가(CREATE), 기존 투두의 수정(UPDATE), 기존 투두의 삭제(DELETE)에 영향을 받는다.
Todo state는 개별 투두의 상세보기(TODO_GET), 현재 보고 있는 투두의 수정(UPDATE), 현재 보고 있는 투두의 삭제(DELETE)에 영향을 받는다.

1) 투두 관리와 관련된 코드
투두 관리의 메인 view를 담당하고 있는 TodoList.tsx에서 개별 투두와 전체 투두가 모두 영향을 받는 CREATE, UPDATE, DELETE를 한 번에 선언해주고 props로 하위 컴포넌트로 내려보내고 있다.

  • todoList.tsx

  • todoInfo.tsx

  • todoTitle.tsx

  • todoFooter.tsx

(2) Ducks Pattern으로 만들기

1) Todo Reducer 만들기

(1) TypeScript 환경에서 만들기

덕스 패턴을 사용하려면 아래와 같은 규칙을 따라야 한다.

  1. reducer는 export default로 내보낸다.
  2. action 함수는 export로 내보낸다.
  3. 액션타입은 reducer/ACTION_TYPE 형태를 따른다. 서로 다른 리듀서에서 액션 이름이 중복되지 않도록 하기 위해서이다.

한 가지 고민이 됐던 건 GET action 함수가 두 개가 필요하다는 점이었다. 전체 투두 리스트를 불러오는 TODOS_GET과 개별 투두를 조회하는 TODO_GET이다.
하나의 파일에서 하나의 state에 대한 GET, UPDATE, CREATE, DELETE만 다룰 수 있게 정의하고 싶었는데 이렇게 하려면 todos와 todo를 별도의 파일로 분리해야 한다. 근데 복수와 단수의 개념에서만 다를 뿐이지 둘 다 투두 관리와 관련된 건데 파일을 분리하는 건 너무 이상해 보이는데다 todo에는 GET 액션만 존재하게 된다(;;)
client의 api를 관리하는 todoService.ts에서도 둘을 묶어서 한 파일에서 모두 정의하고 있고, server 쪽에서도 한꺼번에 묶어서 사용하고 있다. 또 분리하는게 사실 파일 개수만 하나 더 늘어나고 의미도 없어보인다. 그래서 그냥 하나의 모듈에 TODOS_GETTODO_GET을 모두 넣기로 했다.

/* ------------ 액션 타입 --------------- */
const GET_TODOS = "todo/GET_TODOS";
const GET_TODO = "todo/GET_TODO";
const CREATE = "todo/CREATE";
const UPDATE = "todo/UPDATE";
const DELETE = "todo/DELETE";

그리고 액션 상수 함수와 초기 state값을 설정해주고 리듀서 부분을 작성하려고 하는데 난관이 찾아왔다. action의 타입 설정이었다. 타입을 어떤 식으로 알려줘야 하지...?🤔

typescript 환경에서 redux를 사용하는 건 처음이었기 때문에 공식 문서, Stack Overflow, 기술 블로그 등을 검색해봤다. typescript에서 사용하기 위해 참고해야 할 점은 크게 두 가지 정도였다.

  1. 액션 타입 선언에서 as const로 타입을 좁혀준다. type assertion이 없으면 컴파일 시 string 티입으로 읽힌다고 한다. as const로 액션 타입이라는 것을 명확히 알려준다.
const GET_TODOS = "todo/GET_TODOS" // 'todo/GET_TODOS'의 타입인 'string'으로 읽힘 (문자열)
const GET_TODOS = "todo/GET_TODOS" as const // 'todo/GET_TODOS'로 읽힘
//(문자열이 아닌 액션 타입이라는 것을 확실하게 알려주는 역할.)


2. 액션 타입을 결정하기 위해서 ReturnType을 사용한다. ReturnType은 함수가 반환하는 타입을 가져올 수 있게 해준다.

1번처럼 as const를 사용하지 않았더라면 type이 'string'으로 출력됐을 거다.

(2) Input의 onChange event 리덕스로 관리해야 하나?

두 번째 고민은 todo를 수정할 때 발생하는 onChange 이벤트다. hook으로 컴포넌트 내에서 관리했었는데 이걸 액션 함수로 빼야 할지 말아야 할지 고민이 됐다.

결론은 액션 함수로 빼지 않기로 했다.

todoInfo.tsx 컴포넌트에서 newTodo라는 state를 만들어서 관리하기로 했다. 코드가 좀 길어지긴 했지만 newBook state로 관리하고 수정이 끝난 newBook을 updateTodo 액션 함수의 인자로 넘겨서 업데이트 해주는게 코드를 봤을 때 흐름이 더 이해가 잘 가는 것 같은데.........

수정된 getTodoInfo의 꼴을 보아하니 또 아니고....(불러온 데이터를 store에 있는 todo에 세팅해주는 함순데 newTodo까지 같이 세팅해주고 있다;; 심지어 edit 모드로 들어가지 않았을 때도 newTodo를 미리 세팅해줌) 좀 더 고민해 봐야 할 거 같다😓

🌟8.16 업데이트 내용🌟
수정과 관련한 뷰와 로직을 따로 떼어내서 TodoEdit.tsx으로 분리하기로 했다. 투두 수정과 투두 상세보기가 같은 div css를 공유하고 있어서 TodoInfo.tsx 안에 묶어놨었는데 굳이 그럴 필요가 없다고 생각됐다. 분리하니까 한 컴포넌트에서 한 가지 기능만 담당하게 되니 훨씬 보기 좋다. (onChange와 관련한 부분을 리듀서 안으로 넣을지 말지는 아직도 고민..)

TodoInfo.tsxTodoEdit.tsx

)

아무튼.. 이렇게 해서 완성된 투두 리듀서.
client/src/module/todo.ts

import { Todo } from "./../types/todo";
/* ------------ 액션 타입 --------------- */
const GET_TODOS = "todo/GET_TODOS" as const;
const GET_TODO = "todo/GET_TODO" as const;
const CREATE = "todo/CREATE" as const;
const UPDATE = "todo/UPDATE" as const;
const DELETE = "todo/DELETE" as const;

/* ------------ 액션 생성 함수 ---------------*/
export const getTodos = (todos: Todo[]) => ({ type: GET_TODOS, todos });
export const getTodo = (todo: Todo) => ({ type: GET_TODO, todo });
export const createTodo = (newTodo: Todo) => ({ type: CREATE, newTodo });
export const updateTodo = (todoIndex: number, newTodo: Todo) => ({
  type: UPDATE,
  todoIndex,
  newTodo,
});
export const deleteTodo = (todoId: number) => ({ type: DELETE, todoId });

/* ------------ 액션 타입 설정 ---------------*/
type TodoAction =
  | ReturnType<typeof getTodos>
  | ReturnType<typeof getTodo>
  | ReturnType<typeof createTodo>
  | ReturnType<typeof updateTodo>
  | ReturnType<typeof deleteTodo>;

/* ------------ 초기 상태 ---------------*/
type TodoState = {
  todos: Todo[]
  todo: Todo
}

const initState: TodoState = {
  todos:[{
    title: "",
    content: "",
    createdAt: "",
    updatedAt: "",
    id: "",
  }],

  todo:{
    title: "",
    content: "",
    createdAt: "",
    updatedAt: "",
    id: "",
  }
}

/* ------------ 리듀서 ---------------*/
export const todoReducer = (state: TodoState = initState, action: TodoAction): TodoState => {
  const update = { ...state.todos };

  switch (action.type) {
    case "todo/GET_TODOS":
      return { ...state, todos: action.todos };

    case "todo/GET_TODO":
        return { ...state, todo: action.todo };

    case "todo/CREATE": {
      update[Object.keys(update).length] = action.newTodo;
      return { ...state, todos: update };
    }

    case "todo/UPDATE": {
      update[action.todoIndex] = action.newTodo;
      return { ...state, todos: update };
    }

    case "todo/DELETE": {
      delete update[action.todoId];
      return { ...state, todos: update };
    }
    
    default:
      return state;
  }
};

2) RootReducer 만들기

지금은 리듀서가 todo 하나밖에 없지만 추후 생겨날 수도 있는 리듀서들을 위해 root reducer를 만들어서 하나로 묶어주기로 한다. 또 이곳에서 자바스크립트랑 달라 유의할 점이 있다.

RootReducer를 export할 때는 타입을 만들어서 내보내주어야 한다. 추후 스토어의 상태를 조회하기 위해 useSelector를 사용할 때 타입을 필요로 한다.

client/src/module/index.ts

import { combineReducers } from 'redux';
import counter from './counter';

const rootReducer = combineReducers({
  todoReducer
});

export default rootReducer;

export type RootState = ReturnType<typeof rootReducer>;

3) Provider로 App 묶어주기

client/src/index.ts

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { createStore } from "redux";
import rootReducer from "./modules";
import { Provider } from "react-redux";
const store = createStore(rootReducer);
const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);

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

4) 기존 코드에 적용하기

이제 메인 컴포넌트인 TodoList.tsx에서 함수를 내려보낼 필요 없이 필요한 컴포넌트에서 직접 접근해 상태를 변경할 수 있게 됐다.

  • 스토어에 접근해서 todos 가져오기
const todoList = useSelector((state: RootState) => state.todoReducer.todos)
  • 투두의 CRUD (예시로 CREATE만 가져왔다.)
const dispatch = useDispatch()
const onTodoAddClick = async () => {
  const response = await callCreateTodoApi(newTodo);
  dispatch(createTodo(response!.data.data));
  formRef.current?.reset();
};

0개의 댓글