그럼에도 불구하고

👨‍💻

[React with TS] useReducer를 이용한 todoList 만들기 ( with TypeScript ) 본문

React/React basics

[React with TS] useReducer를 이용한 todoList 만들기 ( with TypeScript )

zenghyun 2023. 7. 3. 13:15

오늘은 useReducer를 이용한 todoList를 만들어보겠습니다.

 

목차

     

     

    [ useReducer ]

    useReducer 훅을 사용하는 방법은 다음과 같습니다. 

     

    // state : 상태
    // dispatch : 상태를 변경하는 메서드
    // reducer : 새로운 상태를 리턴하는 리듀서 함수 
    /// initialState : 초기 상태로 지정할 객체 
    const [state, dispatch] = useReducer(reducer, initialState);

     

    useReducer 훅에 리듀서 함수와 초기 상태를 인자로 전달하여 호출하면 상태와 상태 변경을 위해 메시지를 전달할 수 있는 dispatch 함수가 리턴됩니다. dispatch 함수가 미리 정의한 형식의 메시지를 전달하면 상태를 변경하도록 리듀서 함수를 작성해야 합니다. 

     

    이제 간단한 할 일 목록 (TodoList) 예제를 작성하면서 사용 방법을 구체적으로 익히도록 하겠습니다. 

     

    📌 TodoReducer.ts

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    import { produce } from "immer";
     
    export type TodoItemType = {
      id: number;
      todo: string;
    };
     
    export const TODO_ACTION = {
      ADD: "addTodo" as const,
      DELETE: "deleteTodo" as const,
    };
     
    export const TodoActionCreator = {
      addTodo: (todo: string) => ({
        type: TODO_ACTION.ADD,
        payload: { todo: todo },
      }),
      deleteTodo: (id: number) => ({
        type: TODO_ACTION.DELETE,
        payload: { id: id },
      }),
    };
     
    export type TodoActionType =
      | ReturnType<typeof TodoActionCreator.addTodo>
      | ReturnType<typeof TodoActionCreator.deleteTodo>;
     
      export const TodoReducer = (state: Array<TodoItemType>, action: TodoActionType) => {
        switch(action.type) {
            case TODO_ACTION.ADD : 
            return produce(state, (draft: Array<TodoItemType>=> {
                draft.push({id: new Date().getTime(), todo: action.payload.todo});
            });
            case TODO_ACTION.DELETE :
                // eslint-disable-next-line no-case-declarations
                const index = state.findIndex(item => item.id === action.payload.id);
                return produce(state, (draft: Array<TodoItemType>)=> {
                    draft.splice(index, 1);
                });
        }
      };
      
    cs

     

    우선, 상태를 초기화하고 dispatch(action)로 상태를 변경하는 작업을 정적 타입으로 수행하려면 타입을 정의해야 합니다.

     

    TodoItemType과 같이 기본적인 상태를 정해주겠습니다.

     

    export type TodoItemType = {
      id: number;
      todo: string;
    };

     

    그리고 TodoReducer 함수를 살펴보면 dispatch(action) 함수를 호출하여 전달되는 action 객체는 어떤 작업을 수행할지(type)와 작업에 필요한 인자(payload)를 포함해야 합니다. 그렇기 때문에 미리 action 객체의 형식을 지정하기 위해 TodoActionCreator 객체와 이 객체의 메서드 리턴값을 이용하여 생성한 TodoActionType를 정의했습니다.

    action 객체는 TodoActionType 형식의 객체가 되도록 작성했습니다.

     

    export const TODO_ACTION = {
      ADD: "addTodo" as const,
      DELETE: "deleteTodo" as const,
    };
    
    export const TodoActionCreator = {
      addTodo: (todo: string) => ({
        type: TODO_ACTION.ADD,
        payload: { todo: todo },
      }),
      deleteTodo: (id: number) => ({
        type: TODO_ACTION.DELETE,
        payload: { id: id },
      }),
    };

     

    export type TodoActionType =
      | ReturnType<typeof TodoActionCreator.addTodo>
      | ReturnType<typeof TodoActionCreator.deleteTodo>;

     

    TodoActionType에서 type 필드는 string이 아니라 addTodo, deleteTodo와 같이 상수로써 사용해야 합니다.

     

    type이 addTodo일 때는 payload가 { todo : string } 타입이고, type이 deleteTodo 일 때는 payload가 { id : number } 형식이어야 하기 때문입니다. 

     

    따라서 TODO_ACTION을 작성할 때 as const를 추가하여 상수로 정의합니다.

     

    TodoActionType에서 TodoActionCreator의 각 메서드가 리턴하는 값들을 이용해야 하므로 ReturnType이라는 유틸리티 타입을 이용해 리턴값의 타입을 추출해서 사용합니다.

     

    그다음은 작성된 타입과 액션 생성자 메서드들을 이용해 TodoReducer를 작성합니다. TodoReducer에서는 첫 번째 인자(state)가 두 번째 인자(action)를 이용해 상태를 연산하여 새로운 상태를 리턴해야 합니다. 이때 action의 type에 따라 서로 다른 작업을 수행해야 하므로 switch문으로 분기해서 처리합니다. ( if 문을 사용해도 무방 합니다 ) 그리고 기존 상태는 불변성을 가져야 하므로 immer 라이브러리를 이용해 새로운 상태를 생성하여 리턴합니다. 

     

    📌 App.tsx

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    import { useState, useReducer } from "react";
    import { TodoItemType, TodoActionCreator, TodoReducer } from "./TodoReducer";
     
    const idNow = new Date().getTime();
     
    const initialTodoList: Array<TodoItemType> = [
      { id: idNow, todo: "React" },
      { id: idNow + 1, todo: "TypeScript" },
      { id: idNow + 2, todo: "NextJS" },
    ];
     
    const App = () => {
      const [todoList, dispatchTodoList] = useReducer(TodoReducer, initialTodoList);
      const [todo, setTodo] = useState<string>("");
     
      const addTodo = () => {
        dispatchTodoList(TodoActionCreator.addTodo(todo));
        setTodo("");
      };
     
      const deleteTodo = (id: number) => {
        dispatchTodoList(TodoActionCreator.deleteTodo(id));
      };
     
      return (
        <div style={{ padding: "20px" }}>
          <input
            type="text"
            value={todo}
            onChange={(e) => setTodo(e.target.value)}
          />
     
          <button onClick={addTodo}>할 일 추가</button>
     
          <ul>
            {todoList.map((item) => (
              <li key={item.id}>
                {item.todo} &nbsp;&nbsp;
                <button onClick={() => deleteTodo(item.id)}>삭제</button>
              </li>
            ))}
          </ul>
        </div>
      );
    };
     
    export default App;
     
    cs

     

     

    ※ 할 일 추가 전

     

     

    추가

    Comments