Language/React.js

[React] TodoList 프로젝트 : React Context

JJcoding 2024. 10. 21. 10:04

Context

  • 컴포넌트 간의 데이터를 전달하는 또 다른 방법으로 기존 Props가 가지고 있던 단점을 해결할 수 있다.
  • Props의 단점 (Props Drilling) : Props는 부모 → 자식으로만 데이터를 전달 할 수 있다. 컴포넌트의 계층 구조가 두 단계 이상이 되면, 즉 손자 컴포넌트에게는 직접적으로 데이터를 전달 할 수 없다. 중간 다리의 역할을 하는 컴포넌트가 필요하다.
  • context는 데이터보관소 역할을 하는 객체이다.

 

props 사용할 경우

 

context 사용할 경우

  • Context를 사용하기 전 고려할 점 
    • 컴포넌트와 컨텍스트가 연동되면 재사용성이 떨어진다.
      다른 레벨의 많은 컴포넌트가 데이터를 필요로 하는게 아니라면 props를 사용하는게 더 적합하다.
    • context를 사용하는 대표적인 예 : 현재 지역 정보, UI 테마, 캐싱된 데이터 등


App.jsx

import "./App.css";
import Header from "./components/Header";
import Editor from "./components/Editor";
import List from "./components/List";
import { useRef, useReducer, useCallback, createContext } from "react";

const mockDate = [
  ...생략
];

function reducer(state, action) {
	...생략
}

export const TodoContext = createContext();

function App() {
  const [todos, dispatch] = useReducer(reducer, mockDate);
  const idRef = useRef(3);

 const onCreate = useCallback((content) => {
		...생략
  }, []);

  const onUpdate = useCallback((targetId) => {
    ...생략
  }, []);

  const onDelete = useCallback((targetId) => {
    ...생략
  }, []);

  return (
    <div className="App">
      <Header />
      <TodoContext.Provider
        value={{
          todos,
          onCreate,
          onDelete,
          onUpdate,
        }}
      >
        <Editor />
        <List />
      </TodoContext.Provider>
    </div>
  );
}

export default App;
  • context는 대부분 컴포넌트 밖에 선언한다. 컴포넌트가 리렌더링 될때마다 같이 리렌더링 될 필요가 없기 때문에 컴포넌트 밖에 선언한다.
  • <TodoContext.Provider> value로 보낼 객체를 명시한다.


Editor.jsx

import "./Editor.css";
import { useState, useRef, useContext } from "react";
import { TodoContext } from "../App";

const Editor = () => {
 const { onCreate } = useContext(TodoContext);
  const [content, setContent] = useState("");
  const contentRef = useRef();

  const onChangeContent = (e) => {
    setContent(e.target.value);
  };

  const onSubmit = () => {
    if (content === "") {
      contentRef.current.focus();
      return;
    }
    onCreate(content);
    setContent("");
  };

  const onKeydown = (e) => {
    if (e.keyCode === 13) {
      onSubmit();
    }
  };

  return (
    <div className="Editor">
      <input
        ref={contentRef}
        value={content}
        onKeyDown={onKeydown}
        onChange={onChangeContent}
        placeholder="새로운 Todo를 입력하세요."
      />
      <button onClick={onSubmit}>추가</button>
    </div>
  );
};

export default Editor;
  • 구조 분해 할당으로 onCreate를 꺼내와서 사용한다.


List.jsx

import "./List.css";
import TodoItem from "./TodoItem";
import { useState, useMemo, useContext } from "react";
import { TodoContext } from "../App";

const List = () => {
  const [search, setSearch] = useState("");
  const { todos } = useContext(TodoContext);

  const onChangeSearch = (e) => {
    setSearch(e.target.value);
  };

  const getFilteredData = () => {
    if (search === "") {
      return todos;
    }
    return todos.filter((todo) =>
      todo.content.toLowerCase().includes(search.toLowerCase())
    );
  };

  const filteredTodos = getFilteredData();

  const { totalCount, doneCount, notDoneCount } = useMemo(() => {
    //console.log("getAnalyzedData 호출");
    const totalCount = todos.length;
    const doneCount = todos.filter((todo) => todo.isDone).length;
    const notDoneCount = totalCount - doneCount;

    return {
      totalCount,
      doneCount,
      notDoneCount,
    };
  }, [todos]);
  // 의존성배열 : deps

  //const { totalCount, doneCount, notDoneCount } = getAnalyzedData();

  return (
    <div className="List">
      <h4>Todo List📝</h4>
      <div>
        <div>전체 : {totalCount}</div>
        <div>완료 : {doneCount}</div>
        <div>진행 : {notDoneCount}</div>
      </div>
      <input
        value={search}
        onChange={onChangeSearch}
        placeholder="검색어를 입력하세요."
      />
      <div className="todos_wrapper">
        {filteredTodos.map((todo) => {
          return <TodoItem key={todo.id} {...todo} />;
        })}
      </div>
    </div>
  );
};

export default List;
  • 구조 분해 할당으로 todos를 꺼내와서 사용한다.


TodoItem.jsx

import "./TodoItem.css";
import { memo, useContext } from "react";
import { TodoContext } from "../App";

const TodoItem = ({ id, isDone, content, date }) => {
  const { onDelete, onUpdate } = useContext(TodoContext);

  const onChangeCheckbox = () => {
    onUpdate(id);
  };

  const onClickDeleteButton = () => {
    onDelete(id);
  };

  return (
    <div className="TodoItem">
      <input onChange={onChangeCheckbox} checked={isDone} type="checkbox" />
      <div className="content">{content}</div>
      <div className="date">{new Date(date).toLocaleDateString()}</div>
      <button onClick={onClickDeleteButton}>삭제</button>
    </div>
  );
};

export default memo(TodoItem);
  • 구조 분해 할당으로 onDelete, onUpdate를 꺼내와서 사용한다.
  • 그런데 context를 적용하자, 지난 시간에 했던 최적화가 적용이 안된 모습을 확인할 수 있다. 왜 ?
    • <TodoContext.Provider>에서 value props로 보낸 객체가 그 중의 하나의 요소인 todos가 업데이트 되면서 새로운 객체를 생성해서 보내기 때문이다. 그럼 어떻게 해결 할 수 있을까?
    • TodoContext를 두 개로 분리하여 공급한다.

App.jsx

import "./App.css";
import Header from "./components/Header";
import Editor from "./components/Editor";
import List from "./components/List";
import { useRef, useReducer, useCallback, createContext, useMemo } from "react";

const mockDate = [
	...생략
];

function reducer(state, action) {
	...생략
}

export const TodoStateContext = createContext();
export const TodoDispatchContext = createContext();

function App() {
  const [todos, dispatch] = useReducer(reducer, mockDate);
  const idRef = useRef(3);

  const onCreate = useCallback((content) => {
		...생략
  }, []);

  const onUpdate = useCallback((targetId) => {
    ...생략
  }, []);

  const onDelete = useCallback((targetId) => {
    ...생략
  }, []);

  const memoizedDispatch = useMemo(() => {
    return { onCreate, onDelete, onUpdate };
  }, []);

  return (
    <div className="App">
      <Header />
      <TodoStateContext.Provider value={todos}>
        <TodoDispatchContext.Provider value={memoizedDispatch}>
          <Editor />
          <List />
        </TodoDispatchContext.Provider>
      </TodoStateContext.Provider>
    </div>
  );
}

export default App;
  • TodoStateContext 와 TodoDispatchContext 로 나누어서 변경되는 값, 변경되지 않는 값 구분하여 전달한다.
  • useMemo를 쓴 이유 ? value props 객체로 함수들을 묶게 되면, 컴포넌트가 리렌더링 될 때마다 묶은 객체가 계속 재생성 될 것이기 때문에 useMemo로 한번만 생성되도록 감싸주었다.


Editor.jsx

import "./Editor.css";
import { useState, useRef, useContext } from "react";
import { TodoDispatchContext } from "../App";

const Editor = () => {
  const { onCreate } = useContext(TodoDispatchContext);
  const [content, setContent] = useState("");
  const contentRef = useRef();

  ... 생략


List.jsx

import "./List.css";
import TodoItem from "./TodoItem";
import { useState, useMemo, useContext } from "react";
import { TodoStateContext } from "../App";

const List = () => {
  const [search, setSearch] = useState("");
  const todos = useContext(TodoStateContext);

... 생략
  • todos는 값 하나만 그대로 전달 받았기 때문에(객체가 아닌 배열 구조를 그대로 받았다고 보면 됨), 구조분해할당 문법이 아닌 todos로 받으면 된다.


TodoItem.jsx

import "./TodoItem.css";
import { memo, useContext } from "react";
import { TodoDispatchContext } from "../App";

const TodoItem = ({ id, isDone, content, date }) => {
  const { onDelete, onUpdate } = useContext(TodoDispatchContext);

  ... 생략
  • 자식 컴포넌트도 자신이 사용하는 context에 따라 새롭게 import 해주면 된다.

 

출처 : 한입 크기로 잘라먹는 리액트,  처음 만난 리액트