웹/React

[React] Context를 사용해서 Prop Drilling 없이 TodoList 만들기

paran21 2023. 8. 21. 10:33

최근 리액트를 공부하기 위해 드림코딩에서 리액트 강의를 듣고 있다.

https://academy.dream-coding.com/courses/react

 

강의 과정 중 간단한 TodoList를 만드는 챌린지가 있어서 만들어보게 되었다.

결과물! 디자인은 강의를 참고하고 컬러만 바꿔보았다.

상태관리 library는 따로 사용하지 않고 React에서 기본적으로 제공하는 hook을 이용해 상태를 관리하였다.

Component를 분리하는 과정에서 2개 이상의 Component가 동일한 State를 공유해야하는 상황이 생겼고, Context를 사용해서 해결해 보았다.

 

1.  전체 구조

서비스의 전체적인 구조는 다음과 같다.

function App() {
  return (
    <section className="todo">
      <Header />
      <TodoList />
      <Footer />
    </section>
  );
}

Header에서는 다크/라이트 모드를 처리하는 ThemeMode와 메뉴바인 Nav를,

TodoListTodoItem을 list로,

Footer는 Todo를 추가하기 위한 Input을 가지고 있다.

 

이러한 구조에서

1. Todo와 관련된 비즈니스 로직TodoList(조회, 삭제)Footer(추가)에서

2. 메뉴와 관련된 로직Header(조회, 선택)TodoList(필터를 위해 조회)에서

3. 다크모드와 관련된 로직앱 전체에서 알고 있어야 한다.

 

2-1. 해결방법1: Prop으로 전달하기

이를 해결하기 위해 할 수 있는 방법 한 가지는 공통된 상위 Component에서 해당 로직을 처리하고, 하위 Component에는 필요한 로직들을 각각 Prop으로 내려주는 방법이 있다.

<TodoList
 todoList = {todoList},
 deleteTodo = {deleteTodo}
/>

비교적 간단한 구조의 앱이라면 이렇게 Prop을 사용하는 것으로도 충분할 수 있지만, 앱이 복잡해지고 Component가 많아지면 단순히 Prop을 전달하기 위해 Prop을 전달받는 경우도 생길 것이다.

예를 들어, deleteTodo를 사용하는 것은 TodoList가 아니라 Todo이기 때문에 한번 더 전달이 필요하다.

<Todo
 todo = {todo},
 deleteTodo = {deleteTodo}
/>

 

2-2. 해결방법2: Context 사용하기

반면에 Context를 사용하면 Prop으로 전달하지 않아도 필요한 곳에서 사용할 수 있다.

export const TodoListContext = createContext();

export function TodoListProvider({ children }) {
  const [todoList, setTodoList] = useState([]);
  
  //Todo 체크하기
  const checkItem = (id) => {
    setTodoList((prev) =>
      prev.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };
  
  //Todo 삭제하기
  const deleteItem = (id) => {
    setTodoList((prev) => prev.filter((todo) => todo.id !== id));
  };
  
  //Todo 추가하기
  const addItem = (text) => {
    const content = text.trim();
    if (content === '') return;
    const id =
      todoList.length === 0 ? 1 : todoList[todoList.length - 1].id + 1;
    const todo = { id: id, content: content, completed: false };
    setTodoList(prev => [...prev, todo]);
  }
  
  return (
    <TodoListContext.Provider
      value={{ todoList, checkItem, deleteItem, addItem }}
    >
      {children}
    </TodoListContext.Provider>
  );
}

이렇게 Context를 만들고, 필요한 곳에서 사용하면 된다.

export default function Input() {
  ... 
  const { addItem } = useContext(TodoListContext);
  ...
}

결과적으로 App 코드는 다음과 같이 정리되었다.

function App() {
  return (
    <section className="todo">
      <DarkModeProvider>
        <TodoListProvider>
          <NavProvider>
            <Header />
            <TodoList />
          </NavProvider>
          <Footer />
        </TodoListProvider>
      </DarkModeProvider>
    </section>
  );
}

 

3. 생각해볼 점

Context를 사용하면서, Prop으로 전달하지 않고 필요한 곳에서 필요한 상태나 로직을 사용하면 되서 굉장히 편하다고 느꼈다.

또, 비즈니스 로직을 Contex에서 관리하기 때문에 ui와 비즈니스 로직이 분리되고, 기능 수정하기에도 편리했다.

 

반면에 여러 Provider를 사용하면서 앱 구조가 더 복잡해지는 것 같다고 느껴지기도 했다.

 

React 공식문서에 참고할 만한 자료로는 https://react.dev/learn/scaling-up-with-reducer-and-context가 있다.

이 예제에서는 Reducer를 사용하고 있어 Context 안에서 상태비즈니스 로직도 구분하고 있다.

 

전체 코드는 여기에!

https://github.com/paran22/simple_todo

 

GitHub - paran22/simple_todo: simple todo app by React

simple todo app by React. Contribute to paran22/simple_todo development by creating an account on GitHub.

github.com