props-drilling에 이어 context로 props를 전달해서 기존 기능들이 정상적으로 작동되게 만들어보자!!🔥

 

 

 

📌 props-drilling으로 만든 지출 관리 사이트

https://ejunyang.tistory.com/entry/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EA%B0%9C%EC%9D%B8-%EC%A7%80%EC%B6%9C-%EA%B4%80%EB%A6%AC-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EB%A7%B9%EB%93%A4%EA%B8%B0%F0%9F%90%A2

 

[리액트] 개인 지출 관리 사이트 맹들기🐢

세번째 개인 프로젝트이다. 리액트로는 두번째 개인 프로젝트! 간단한 개인 지출 관리 사이트를 만들어보자!!🐢이번 과제는 props-drilling > context > redux 순으로 상태 관리를 변경해가며 구현해야

ejunyang.tistory.com

 

 

컴포넌트

컴포넌트 설명
ExesForm.jsx 지출내역을 입력하는 컴포넌트이다. 메인 페이지 우측 하단 버튼을 누르면 모달처럼 뜨도록 구현했다.
MonthList.jsx 월별 버튼 컴포넌트이다. 해당 월을 누를 때마다 다른 스타일링이 들어간다.
ExesList.jsx 월별 버튼을 눌렀을 때 하단에 해당 월의 지출내역이 나타나는 지출 내역 리스트 컴포넌트이다.
ExesItem.jsx 각 지출 내역의 컴포넌트이다.

 

 


 

 

컨텍스트 생성

📂 context >  📂 Context.jsx

import { createContext } from "react";

export const Context = createContext(null);

 

 

App.jsx

최상위 컴포넌트에서 각 컴포넌트에서 사용할 props를 Provider를 사용해 전달해준다. value로 전달해주기 위해선 App.jsx에 데이터들이 있어야한다.

import { useCallback, useEffect, useState } from "react";
import { Context } from "./context/Context";

return (
    <Context.Provider
      value={{
        exes,
        setExes,
        selectedMonth,
        setSelectedMonth,
        handleMonthSelect,
        onInsert,
        filteredList,
      }}
    >
      <Router />
    </Context.Provider>
  );
};

export default App;

 

 

ExesForm.jsx

 

useContext를 사용해서 사용하고 있던 데이터를 가져온다. 지출내역을 추가하는 컴포넌트로 onInsert 함수를 가져온다.

import React, { useCallback, useContext, useState } from "react";
import { FaPen } from "react-icons/fa6";
import styled from "styled-components";
import { Context } from "../context/Context";

const ExesForm = () => {
  const { onInsert } = useContext(Context);

  const onSubmit = useCallback(
    (e) => {
      ...
    },
    [date, item, amount, desc]
  );

  return (
   ...
  );
};

export default ExesForm;

 

 

 

ExesList.jsx

여기서 살짝 헤맸는데 해당 지출 내역을 가져와서 map메서드로 각 지출 내역을 하나씩 만들어준다. 그래서 ExesItem.jsx에도 useContext를 사용해서 데이터를 가져와야하나? 했는데 사용하기 어려워보였다. 튜터님께 여쭤보니 해당 과제가 context를 사용해보려고 하는것이지 아예 props-drilling을 사용하지 말라는건 아니라고 하셨고, 데이터를 바로 전달할 수 있는 경우에는 props로 하위 컴포넌트에게 넘겨주어도된다고 했다. 

import React, { useContext } from "react";
import ExesItem from "./ExesItem";
import { Context } from "../context/Context";

const ExesList = () => {
  const { filteredList } = useContext(Context);
  return (
    <div>
      <ul>
        {/*
        Uncaught TypeError: Cannot read properties of undefined (reading 'map')
        커밋 된 후에야 모든 효과를 실행
        React는 return에서 map을 반복실행할 때 첫 턴에 데이터가 아직 안들어와도 렌더링이 실행되며 
        당연히 그 데이터는 undefined로 정의되어 오류

        && 사용
        true && expression은 항상 expression으로 실행
        */}
        {filteredList &&
          filteredList.map((exe) => {
            return <ExesItem key={exe.id} exe={exe} />;
          })}
      </ul>
    </div>
  );
};

export default ExesList;

 

 

MonthList.jsx

import React, { useContext } from "react";
import "../App.css";
import styled from "styled-components";
import { Context } from "../context/Context";

const MonthList = () => {
  const { exes, selectedMonth, handleMonthSelect } = useContext(Context);

  const monthArray = [1,2,3,4,5,6,7,8,9,10,11,12];

  const totalAmount =
    exes[selectedMonth] && exes[selectedMonth].length > 0
      ? exes[selectedMonth].reduce((acc, cur) => {
          return (acc += cur.amount);
        }, 0)
      : 0;

  return (
    <>
     ...
    </>
  );
};

export default MonthList;

 

 

 

Detail.jsx

 

props-drilling으로 작업할 때 디테일 페이지에 지출내역의 setState를 가져오지 못해서 상태를 업데이트해주지 못했는데 context를 사용하면 setState 가져올 수 있다!

import React, { useCallback, useContext, useEffect, useRef } from "react";
import { useNavigate, useParams } from "react-router-dom";
import styled from "styled-components";
import { IoIosArrowBack } from "react-icons/io";
import { Context } from "../context/Context";

const Detail = () => {
  const { id } = useParams(); 
  const navigate = useNavigate(); 
  const { filteredList, setExes } = useContext(Context);

  const exe = filteredList.find((item) => item.id === id);

  return (
    <>
      ...
    </>
  );
};

export default Detail;

 

삭제할 지출 내역을 제외한 새로운 배열을 생성하고,  그 배열을 포함한 새로운 객체를 만든다. 불변성 유지를 위해 전체 월 지출내역 복사,  removeExes 안에는 전체 지출 내역 중 삭제된 지출내역을 제외한 나머지 지출내역이 들어가 있다. 로컬스토리지에 해당 데이터를 저장하고 setExes(지출 내역 업데이트)로 상태 업데이트를 해준다.

const onRemove = useCallback(
    (id) => {
      if (confirm("정말 삭제하시겠습니까?") == true) {
        const removeExes = {
          ...data,
          [exe.month]: data[exe.month].filter((ex) => ex.id !== id),
        };
        localStorage.setItem("expenseList", JSON.stringify(removeExes));
        setExes(removeExes);
        navigate("/");
      } else {
        return false;
      }
    },
    [exe]
  );

 

수정 기능도 불변성 유지를 위해 이전 데이터를 복사 후에 수정된 데이터 배열을 월 지출 내역에 업데이트 해준다. 

const onModify = useCallback(
    (id) => {
      const modifiedData = {
        id: exe.id,
        date: dateRef.current.value, //변수 접근
        item: itemRef.current.value,
        amount: +amountRef.current.value, //숫자로만 받아야하기때문에 +연산자 사용(NaN 오류 발생)
        desc: descRef.current.value,
      };
      data[exe.month] = data[exe.month].map((ex) =>
        ex.id === id ? { ...ex, ...modifiedData } : ex
      );
      setExes((prevExe) => ({
        ...prevExe,
        [exe.month]: data[exe.month],
      }));
      localStorage.setItem("expenseList", JSON.stringify(data));
      alert("수정이 완료되었습니다.");
    },
    [exe]
  );

 

 

useContext 

useContext는 컴포넌트에서 context를 읽고 구독할 수 있게 해주는 React Hook입니다.

 

 

기본구문

const value = useContext(SomeContext)

 

매개변수

SomeContext : 이전에 createContext로 생성한 context입니다. context 자체는 정보를 보유하지 않으며, 컴포넌트에서 제공하거나 읽을 수 있는 정보의 종류를 나타낼 뿐입니다.

 

반환값

호출한 컴포넌트에서 트리상 위에 있는 가장 가까운 SomeContext.Provider에 전달된 value입니다. 이러한 provider가 없는 경우 반환되는 값은 해당 context에 대해 createContext에 전달한 defaultValue가 됩니다. 반환된 값은 항상 최신 값입니다. React는 context가 변경되면 context를 읽는 컴포넌트를 자동으로 리렌더링합니다.

 

 


 

 

 

React Context의 필요성

일반적으로 부모컴포넌트에서 자식 컴포넌트에게 데이터를 전달해줄때 props로 전달해준다. 하지만 큰 단점이 있는데 컴포넌트가 깊어질수록 해당 컴포넌트가 어느 컴포넌트에서 왔는지 파악하기 어려워진다. 이것을 props-drilling이라고 한다.

 

prop drilling의 문제점

  1. 깊이가 너무 깊어지면 이 prop이 어떤 컴포넌트로부터 왔는지 파악이 어려워진다.
  2. 어떤 컴포넌트에서 오류가 발생할 경우 추적이 힘들다.

 

 

Props 사용이 적절할 때❓

리스트 컴포넌트에서 아이템 컴포넌트로 상태를 전달하는 경우. 이 경우는 이번 프로젝트를 하면서 많이 생소했다. props drilling으로 만든 코드를 context 로 변경하는 과정에서 리스트 컴포넌트에는 도저히 context를 사용할 수 없을 것 같아서였다. map을 돌린 애들로 아이템을 만드는데 여기서 context를 어떻게 사용할까 했는데 오늘 강의 때 단순한 데이터 전달은 Props 사용이 적절한 때라고 알려주셔서 이해할 수 있었다.

export default function TodoList({ isDone }) {
  const { todos, setTodos } = useContext(TodoContext);
  const filteredTodos = todos.filter((todo) => todo.isDone === isDone);
  return (
    <div>
      <h2>{isDone ? "Done" : "Working..."}</h2>
      {filteredTodos.map((todo) => (
        <TodoItem key={todo.id} todo={todo} setTodos={setTodos} />
      ))}
    </div>
  );
}
export default function TodoListWrapper() {
    return (
        <>
            <TodoList isDone={false} />
            <TodoList isDone={true} />
        </>
    );
}

 

 

Context API

아래 그림처럼 useContext hook을 통해 우리는 쉽게 전역 데이터를 관리할 수 있다. Context Provider 의 모든 children 컴포넌트들에게 context value를 공유할 수 있고, 전역상태관리를 하게 되면 props 를 이용한 상태 공유를 하지 않아도 된다.

 

  • createContext : context를 생성
  • useContext : context를 구독하고 해당 context의 현재 값을 읽기
  • Provider : context를 하위 컴포넌트에게 전달

 

 

 

 

createContext

Context Provider 의 모든 children 컴포넌트들에게 context value를 공유할 수 있다. 

const TodoContext = createContext(null);

 

 

Provider

 context를 하위 컴포넌트에게 전달한다.

function App () {
	const [todos, setTodos] = useState([]);
	
	return (
		<TodoContext.Provider value={{todos, setTodos}}>
			<TodoForm />
			<TodoListWrapper />
		</TodoContext.Provider>	
	);
}

 

예시

// src/context/TodoContextProvider.jsx
import React, { createContext, useState } from 'react';

// 함수 컴포넌트 외부에서 Context 생성
export const TodoContext = createContext(null);

const TodoContextProvider = ({ children }) => {
  const [todos, setTodos] = useState([]);

  const addTodo = (text) => {
    setTodos([...todos, { id: Date.now(), text, isDone: false }]);
  };

  return (
    <TodoContext.Provider value={{ todos, addTodo }}>
      {children}
    </TodoContext.Provider>
  );
};

export default TodosContextProvider;

 

useContext

context를 구독하고 해당 context의 현재 값을 읽어올 수 있다. useContext() 괄호 안은 리턴 + 구독을 뜻한다. 아래 코드는 todoContext를 구독하겠다는 말과 같다.

function TodoForm() {
	const { setTodos } = useContext(TodoContext);
	return (
		...
	)
}

 

 

 

실습을 하던 중에 디테일 페이지에 데이터를 받아야하는데 부모 컴포넌트와 종속성이 없어 데이터를 받아오지 못하는 상황이 있었다. 라우터 간 데이터 전달은 context api로 할 수 있는데 props-drilling으로 작업하고 있어서 어찌해야할 방법을 몰랐다. 그런데 설마 했던 방법이 맞았다..! 라우터 컴포넌트에서 상태를 전달하는 것..! 너무나도 생소해서 여긴 아니겠지 하는 생각으로 갈피를 잡지 못했었다. 

// 여러 페이지 컴포넌트에 동일한 상태 공유가 필요할 때
function Router() {
	return (
	<BrowserRouter>
	    <Routes>
        	<Route path="/" element={<Home />} />
        	<Route path="/detail/id" element={<Detail />} />
	    </Routes>
	</BrowserRouter>		
	)
}

 

Route.jsx에다 상태를 만들어주고 props로 내려주면 되는것.. 방법을 못찾아서 useLocation으로 데이터를 받아왔었는데 props-drilling의 불편함을 어느정도 알 수 있게해준 오류였다.

function Router() {
const [todos, setTodos] = useState([]);
	return (
	<BrowserRouter>
	    <Routes>
        	<Route path="/" element={<Home />} />
        	<Route path="/detail/id" element={<Detail todos={todos}/>} />
	    </Routes>
	</BrowserRouter>		
	)
}

 

+ Recent posts