그럼에도 불구하고

👨‍💻

[React with TS] Context API를 이용한 todoList 만들기 본문

React/React basics

[React with TS] Context API를 이용한 todoList 만들기

zenghyun 2023. 7. 5. 17:50

Context API를 이용하여 todoList를 만들어봅시다!!

 

목차

     

    [ Context API란? ]

    자세한 설명은 아래를 참고해 주세요

     

    https://despiteallthat.tistory.com/184

     

    [React] Context API란?

    오늘은 Context API에 대해 알아보겠습니다. [ Context ] Context는 리액트 컴포넌트 간에 어떠한 값을 공유할 수 있게 해주는 기능입니다. 주로 Context는 전역적으로 필요한 값을 다룰 때 사용하는데, 꼭

    despiteallthat.tistory.com

     

    간단하게나마 다시 언급하자면, Context API는 컴포넌트 트리에서 속성을 전달하지 않고 필요한 데이터를 컴포넌트에 전달하는 방법을 제공하는 API입니다. 

     

     

    [ todolist-app에 Context API 적용하기 ]

     

    📌 TodoContext.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
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    import React, { useState } from 'react';
    import { produce } from 'immer';
     
    export type TodoListItemType = {
        no: number;
        todo: string;
        done: boolean;
    };
     
    // Provider로 전달한 데이터(value)의 타입 정의 
    export type TodoListContextValueType = {
        state : { todoList : Array<TodoListItemType> };
        actions : {
            addTodo : (todo:string) => void;
            deleteTodo : (no:number) => void;
            toggleDone : (no:number) => void;
        };
    }; 
     
    // 앞에서 정의한 타입 또는 null 타입을 이용해 Context 객체 생성 
    const TodoContext = React.createContext<TodoListContextValueType | null>(null); 
     
    // TodoProvider 컴포넌트의 자식 컴포넌트 타입을 정의 
    // <TodoProvider> 자식 컴포넌트 </TodoProvider> 
    type PropsType = {
        children : JSX.Element | JSX.Element[];
    }
     
    // 상태와 상태 변경 함수를 가지는 컴포넌트
    // 상태와 상태 변경 함수를 데이터 타입 형식으로 구성한 후 value 속성으로 전달 
    // <TodoContext.Provider value={value}>{children}</TodoContext.Provider>
    export const TodoProvider = (props: PropsType) => {
        const [todoList, setTodoList] = useState<Array<TodoListItemType>>([
            {no: 1, todo: "React 학습 1", done: false},
            {no: 2, todo: "React 학습 2", done: false},
            {no: 3, todo: "React 학습 3", done: true},
            {no: 4, todo: "React 학습 4", done: false},
        ]);
     
        const addTodo = (todo: string) => {
            const newTodoList = produce(todoList, (draft: Array<TodoListItemType>=> {
                draft.push({no: new Date().getTime(), todo: todo, done: false});
            });
            setTodoList(newTodoList);
        };
     
        const deleteTodo = (no: number) => {
            const index = todoList.findIndex(item => item.no === no);
            const newTodoList = produce(todoList, (draft: Array<TodoListItemType>=> {
                draft.splice(index, 1);
            });
            setTodoList(newTodoList);
        };
     
        const toggleDone = (no :number) => {
            const index = todoList.findIndex(item => item.no === no);
            const newTodoList = produce(todoList, (draft: Array<TodoListItemType>=> {
                draft[index].done = !draft[index].done;
            });
            setTodoList(newTodoList);
        };
     
        // <TodoContext.Provider />의 value로 전달할 객체 생성 
        const values : TodoListContextValueType = {
            state: { todoList },
            actions: { addTodo, deleteTodo, toggleDone},
        };
     
        return (
            <TodoContext.Provider value={values}>
                {props.children}
            </TodoContext.Provider>
        );
    };
     
    export default TodoContext;
    cs

     

     

    첫 번째 단계로 Provider로 전달할 데이터 (value)의 타입을 정의했습니다. 상태는 state 속성으로, 모든 상태 변경 함수는 actions 속성에 포함되도록 타입을 정의했습니다.

     

    두 번째 단계로 createContext 함수를 호출해 TodoContext 객체를 생성합니다.

     

    ⭐️ React.createContext()

     

    React.createContext() 메서드를 호출하여 TodoContext 객체를 생성합니다

    // 앞에서 정의한 타입 또는 null 타입을 이용해 Context 객체 생성 
    const TodoContext = React.createContext<TodoListContextValueType | null>(null);

    위의 예시처럼 미리 정의한 데이터(value) 타입 또는 null 허용하도록 제네릭으로 지정하여 createContext 함수를 호출하고 Context 객체를 생성합니다.

     

    null을 허용하는 이유는 Context를 생성할 때 null로 초기화하기 때문입니다.

     

    세 번째 단계로 JSX.Element를 전달할 수 있도록 PropType의 속성을 전달받을 수 있는 TodoProvider 컴포넌트를 작성합니다. TodoProvider 컴포넌트에는 상태와 상태 변경 함수를 작성하고, TodoContext.Provier 컴포넌트의 value 속성으로 전달할 데이터 형식으로 객체를 구성합니다. 그러면 TodoProvider 컴포넌트를 렌더링 할 때 구성한 객체를 Context 객체의 Provider에서 value 속성으로 전달합니다.

     

    마지막으로 {props.children} TodoProvider 컴포넌트를 다음과 같이 렌더링 <App /> 컴포넌트와 같은 자식 컴포넌트가 됩니다

     

    <TodoProvider>
      <App />
    <TodoProvider>

     

    📌 main.tsx

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import React from 'react'
    import ReactDOM from 'react-dom/client'
    import { TodoProvider } from './TodoContext.tsx'
    import App from './App.tsx'
    import 'bootstrap/dist/css/bootstrap.css'
     
    ReactDOM.createRoot(document.getElementById('root'as HTMLElement).render(
      <React.StrictMode>
        <TodoProvider>
        <App />
        </TodoProvider>
      </React.StrictMode>,
    )
     
    cs

     

    📌 App.tsx

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    import InputTodo from './components/InputTodo';
    import TodoList from './components/TodoList';
     
    const App = () => {
      return (
        <div className='container'>
          <div className='card card-body bg-light'>
            <div className='title'>
              :: TodoList App
            </div>
            <div className='card card-default card-borderless'>
              <div className='card-body'>
                <InputTodo />
                <TodoList />
              </div>
            </div>
          </div>
        </div>
      );
    };
     
    export default App; 
    cs

     

    📌 InputTodo.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
    49
    50
    51
    52
    import React, { useContext, useState } from "react";
    import TodoContext from "../TodoContext";
     
    const InputTodo = () => {
      const [todo, setTodo] = useState<string>("");
      // useContext 훅으로 TodoContext의 value 값을 받아냅니다.
      const value = useContext(TodoContext);
     
      // value의 속성의 actions의 addTodo 함수를 호출합니다.
      const addHandler = () => {
        value?.actions.addTodo(todo);
        setTodo("");
      };
     
      const enterInput = (e: React.KeyboardEvent) => {
        if (e.key === "Enter") {
          addHandler();
        }
      };
     
      const changeTodo = (e: React.ChangeEvent<HTMLInputElement>=> {
        setTodo(e.target.value);
      };
     
      return (
        <div className="row">
          <div className="col">
            <div className="input-group">
              <input
                id="msg"
                type="text"
                className="form-control"
                name="msg"
                placeholder="할 일을 여기에 입력"
                value={todo}
                onChange={changeTodo}
                onKeyUp={enterInput}
              /> &nbsp;
              <span
                className="btn btn-primary input-group-addon"
                onClick={addHandler}
              >
                추가
              </span>
            </div>
          </div>
        </div>
      );
    };
     
    export default InputTodo;
     
    cs

     

     // value의 속성의 actions의 addTodo 함수를 호출합니다.
      const addHandler = () => {
        value?.actions.addTodo(todo);
        setTodo("");
      };

     

    한 가지 주의할 점은 value?. actions.addTodo(todo);와 같이 value에? 식별자를 사용해 선택적 속성으로 작성해야 합니다. 

    TodoContext.tsx에서 Context 객체를 만들 때 사용했던 제네릭 타입이 <TodoListContextValueType | null>이고, 초기화할 때 null 값을 부여했으므로? 기호를 사용해 선택적 속성으로 사용하는 것입니다.

     

    📌 TodoList.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
    import React, { useContext, useState } from "react";
    import TodoContext from "../TodoContext";
    import TodoListItem from "./TodoListItem";
     
    const TodoList = () => {
      const value = useContext(TodoContext);
     
      const items = value?.state.todoList.map((item) => {
        return (
          <TodoListItem
            key={item.no}
            todoItem={item}
            deleteTodo={value?.actions.deleteTodo}
            toggleDone={value?.actions.toggleDone}
          />
        );
      });
     
      return (
        <div className="row">
          {" "}
          <div className="col">
            <ul className="list-group">{items}</ul>
          </div>
        </div>
      );
    };
     
    export default TodoList;
     
    cs

    TodoList 컴포넌트 코드에서 눈여겨볼 부분은 TodoList 컴포넌트에서 TodoListItem 컴포넌트로 속성을 전달하는 방법을 그대로 사용하고 있다는 점입니다. TodoList 컴포넌트에서 이용하는 todoList 상태는 배열 값이며 TodoListItem 컴포넌트로 전달되는 속성은 todoList 배열의 각 항목입니다. 이런 경우 Context를 이용해 전달하는 것이 더 복잡하고 어렵기 때문에 기존과 같이 속성을 전달하는 것이 더 바람직합니다.

     

    📌 TodoListItem.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
    import { TodoListItemType } from "../AppContainer";
     
    type TodoListItemProps = {
      todoItem: TodoListItemType;
      deleteTodo: (no: number) => void;
      toggleDone: (no: number) => void;
    };
     
    const TodoListItem = (props: TodoListItemProps) => {
      let itemClassName = "list-group-item";
      props.todoItem.done
        ? (itemClassName += " list-group-item-success")
        : itemClassName;
      return (
        <li className={itemClassName}>
          <span
            className={props.todoItem.done ? "todo-done pointer" : "pointer"}
            onClick={() => props.toggleDone(props.todoItem.no)}
          >
            {props.todoItem.todo}
            {props.todoItem.done ? " (완료)" : ""}
          </span>
          <span
            className="float-end badge bg-secondary pointer"
            onClick={() => props.deleteTodo(props.todoItem.no)}
          >
            삭제
          </span>
        </li>
      );
    };
     
    export default TodoListItem;
     
    cs

     

    🏷️ 마무리 

    Context API 이용하면 자식 컴포넌트로 반복해서 속성을 전달하지 않아도 됩니다. React.createContext() 함수를 이용해 Context 객체를 생성하고, Context.Provider 컴포넌트의 value 속성에 상태와 상태를 변경하는 함수를 객체로 구성하여 지정합니다. 컴포넌트 트리에 있는 자식 컴포넌트에서는 useContext 훅을 이용해 value 객체를 받아내어 상태와 상태 변경 함수를 이용할 있습니다.

     

    하지만 속성을 전혀 사용하지 않고 Context API만 이용해서 컴포넌트와 애플리케이션을 작성하는 것은 바람직하지 않습니다.

     

    작성했던 예제 중 TodoLIstItem 컴포넌트와 같이

     

    상태 데이터 중 배열의 각 항목을 이용하는 컴포넌트는 useContext를 이용해 상태를 접근할 수는 있지만, 배열 데이터 중 몇 번째 항목인지를 확인하기 힘들기 때문에 기존처럼 속성을 사용하는 것이 바람직합니다.

     

    또한 Context API를 이용하는 컴포넌트는 Context API에 종속되기 때문에 Context API를 사용하도록 개발된 애플리케이션에서만 재사용할 수 있습니다.  따라서 주요 거점의 컴포넌트에서만 useContext 훅을 사용하고, 그 하위 짧은 단계는 자식 컴포넌트로 속성을 전달하는 것이 바람직합니다.

     

     

     

    Comments