ABOUT ME

-

오늘
-
어제
-
-
  • [React] Context API + useReducer 조합 살펴보기 feat. Typescript
    Front-end/React 2021. 12. 2. 00:55

    리액트 개발을 할 때 상태관리 라이브러리를 사용하지 않으면 무수히 많은 props들과 싸우며 개발을 하게 됩니다.. 😨

    그러한 문제를 해결하기 위해 많은 라이브러리(Redux, Recoil, Mobx 등)들이 등장했습니다.

     

    그래서 이번 기회에 간단한 Todo를 구현하며 Context API와 useReducer 조합을 통해 번거로운 prop drilling 없이 상태관리를 할 수 있는 방법에 대해 알아보겠습니다. 

     

    ❗️ 이 글은 개인적으로 학습한 정보와 지식으로 작성된 글입니다. 혹시 잘못된 부분이 있거나 수정사항이 있다면 말씀해주시면 반영하겠습니다. 🙏

    Context API

    먼저 Context API 의 개념에 대해 알아보겠습니다!

    일단, 결론부터 말씀드리면 Context API는 상태관리를 하는 목적이 아닙니다. 🙅🏻

     

    Context API가 Redux와 같은 상태관리 라이브러리 대용으로 사용하는 경우와 예시의 한 경우로 자리매김 하여 상태관리의 역할을 하는 API로 이해를 하고 있을텐데요 (저도 그랬습니다. 😅)

    리액트 공식문서에 따르면 생각과는 다르게 전혀 다른 말이 적혀있습니다.

    React Context API 설명

    위 말에서 그 어디에도 상태관리라는 말은 찾아볼 수가 없죠

    그리고 말하고자 하는 결론은 일일이 props를 넘겨주지 않고 곧바로 제공하는 역할을 하는 친구라고 볼 수 있습니다.

    즉, 굳이 상태가 아니더라도 데이터(값)라면 무엇이는 props를 넘기지 않고도 제공한다는 것이죠

     

    리액트 개발을 할 때 Context API를 상태관리의 명목으로 많이 사용되고 있기 때문에 이런 오해에 의한 워딩이 생겨나 혼동을 일으키는 것 같습니다. 🤔

     

    그렇다면 Context API로 상태관리를 한다는건 무슨 의미냐?!

    리액트에서 제공하는 훅인 useState, useReducer를 통해 직접 상태관리를 하는 상태함수들을 Context API로 prop drilling 없이 전달할 때 상태관리의 바지사장이라는 자리를 수여하게 됩니다.

    그 이유는 개념적으로는 앞서 말씀 드렸지만 상태관리의 역할이 아니나, useState와 useReducer의 상태와 함수들을 처리하니 강제로 상태관리의 역할을 하는 자리에 앉혀놓았다고 생각하면 됩니다.

    진짜 상태관리는 현재 예제 기준으로는 useReducer가 하고 있는 셈이죠

     

    코드를 보면서 어떻게 활용되는지 살펴보겠습니다.

    import React, { useReducer, createContext } from 'react';
    
    interface DefaultValueState {
      id: number;
      text: string;
    }
    
    const defaultValue: DefaultValueState[] = [];
    
    type ActionType =
      | {
          type: 'ADD_TODO';
          text: string;
        }
      | { type: 'DELETE_TODO'; id: number };
    
    function reducer(
      state: DefaultValueState[],
      action: ActionType
    ): DefaultValueState[] {
      switch (action.type) {
        case 'ADD_TODO': {
          return state.concat({
            id: new Date().getTime(),
            text: action.text,
          });
        }
        case 'DELETE_TODO': {
          return state.filter((todo) => todo.id !== action.id);
        }
      }
    }
    
    interface ContextType {
      todos: DefaultValueState[];
      addTodo: (text: string) => void;
      deleteTodo: (todoId: number) => void;
    }
    
    export const TodoContext = createContext<ContextType>({
      todos: defaultValue,
      addTodo: () => {},
      deleteTodo: () => {},
    });
    
    function TodoProvider({ children }: { children: React.ReactNode }) {
      const [state, dispatch] = useReducer(reducer, defaultValue);
    
      const addTodo = (text: string): void => {
        dispatch({ type: 'ADD_TODO', text });
      };
    
      const deleteTodo = (id: number): void => {
        dispatch({ type: 'DELETE_TODO', id });
      };
    
      return (
        <TodoContext.Provider
          value={{
            todos: state,
            addTodo,
            deleteTodo,
          }}
        >
          {children}
        </TodoContext.Provider>
      );
    }
    
    export default TodoProvider;

     

    컨텍스트를 생성할 때 가장 기본값을 넣게 되는데, 이때 useReducer에서 관리할 상태함수를 가지고 생성하게 됩니다.

    그리고 이렇게 생성된 컨텍스트를 useReducer에서 본격적으로 상태를 핸들링 하게 되고, 그 함수와 갱신될 상태를 children에게 넘겨주게 됩니다.

     

    이렇게 되면 상태관리는 TodoProvider 컴포넌트에서 진행이 되지만, 이는 Context API를 통해 전역으로 drilling 해줄 수 있게 되는 것이죠

     

    그렇다면 이걸 사용하는 컴포넌트의 모습은 어떨까요?

     

    App.tsx

    import React from 'react';
    import ReactDOM from 'react-dom';
    import './index.css';
    import App from './App';
    import reportWebVitals from './reportWebVitals';
    
    import TodoProvider from './components/todo/provider';
    
    ReactDOM.render(
      <React.StrictMode>
        <TodoProvider>
          <App />
        </TodoProvider>
      </React.StrictMode>,
      document.getElementById('root')
    );
    
    reportWebVitals();

    Todo를 그려낼 컴포넌트에 대해서 상위 컴포넌트로 감싸줍니다.

    Input.tsx

    import React, { useContext, useState } from 'react';
    import { TodoContext } from './provider';
    
    function Input() {
      const { addTodo } = useContext(TodoContext);
      const [value, setValue] = useState<string>('');
    
      const handleChangeInput = (e: React.ChangeEvent<HTMLInputElement>): void => {
        setValue(e.target.value);
      };
    
      const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
        e.preventDefault();
      };
    
      const handleClickButton = (): void => {
        addTodo(value);
        setValue('');
      };
    
      return (
        <form
          onSubmit={handleFormSubmit}
          style={{
            display: 'flex',
            justifyContent: 'space-between',
            width: 'inherit',
          }}
        >
          <input
            value={value}
            onChange={handleChangeInput}
            style={{ width: '80%' }}
          />
          <button type='button' onClick={handleClickButton}>
            등록
          </button>
        </form>
      );
    }
    
    export default Input;

    그리고 자식 컴포넌트에선 useContext hook을 통해 TodoProvider에서 drilling한 props를 바로 접근해서 사용이 가능하게 됩니다.

    Input 컴포넌트에서는 등록을 해주는 역할이 있기 때문에 reducer 함수에서 'ADD_TODO' 타입인 dispatch 함수를 사용했습니다.

     Todos.tsx

    import React, { useContext } from 'react';
    
    import Item from './item';
    import { TodoContext } from './provider';
    
    function Todo() {
      const { todos } = useContext(TodoContext);
    
      return (
        <ul style={{ width: '100%', listStyleType: 'none', padding: 0 }}>
          {todos.map((todo) => (
            <Item key={todo.id} id={todo.id} text={todo.text} />
          ))}
        </ul>
      );
    }
    
    export default Todo;

    이제 추가한 Todo를 그려내야 할 때 todos 라는 명으로 상태를 주었으니 해당 상태를 가져와 렌더링을 시켜줍니다.

    위와 마찬가지로 'DELETE_TODO'의 dispatch도 마찬가지겠죠?

    Item.tsx

    import React, { useContext } from 'react';
    
    import { TodoContext } from './provider';
    
    function Item({ id, text }: { id: number; text: string }) {
      const { deleteTodo } = useContext(TodoContext);
    
      const handleDeleteTodo = () => {
        deleteTodo(id);
      };
    
      return (
        <li>
          <div style={{ display: 'flex', justifyContent: 'space-between' }}>
            <span>{text}</span>
            <button onClick={handleDeleteTodo}>삭제</button>
          </div>
        </li>
      );
    }
    
    export default Item;

     

    이처럼 Context API와 useReducer 조합을 통해 props drilling 없이 상태관리 하는 법을 학습해보았습니다.

    댓글