프로젝트

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

ejunyang 2024. 6. 10. 17:05

 

이제 마지막 ! context 에 이어 리덕스 툴킷 라이브러리로 전역상태 관리하기!

 

 

📌 Context로 만든 지출 관리 사이트

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-Context%F0%9F%90%A2

 

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

props-drilling에 이어 context로 props를 전달해서 기존 기능들이 정상적으로 작동되게 만들어보자!!🔥   📌 props-drilling으로 만든 지출 관리 사이트https://ejunyang.tistory.com/entry/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EA

ejunyang.tistory.com

 

 

 

컴포넌트

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

 

 


 

 

리덕스 설치 및 생성

📂 redux >  📄 configStore.js

yarn add @reduxjs/toolkit react-redux

 

우선 만들어야할 리듀서 함수는 지출 내역과 월버튼이다. 매번 월별 지출내역을 초기값으로 가져와야하기때문에 전역에서 상태관리하는게 좋겠다고 생각이 들었다. 

import { configureStore } from "@reduxjs/toolkit";
import expense from "../slices/expense";
import month from "../slices/month";

const store = configureStore({
  reducer: {
    expense: expense,
    month: month,
  },
});

export default store;

 

 

 

📂 redux >  📂 slices > 📄 expense.js

 

지출 내역 초기값을 세팅해주고, 추가, 수정, 삭제 리듀서 함수를 만들어보자

.

inisialState는 초기값을 나타낸다. 로컬스토리지에 expenseList 변수명으로 저장되어있는 데이터를 객체화하여 가져온다. 데이터가 있으면 객체화해서 가지고오고, 없다면 빈 객체로 반환한다. 

import { createSlice } from "@reduxjs/toolkit";

const initialState = {
  expenseList: localStorage.getItem("expenseList")
    ? JSON.parse(localStorage.getItem("expenseList"))
    : {},
};

const expense = createSlice({
  name: "expense",
  initialState: initialState,
  reducers: {},
});

export const { addExpense } = expense.actions;
export default expense.reducer;

 

 

추가하는 리듀서함수를 만들어보자. 리듀서 함수는 두가지 매개변수를 갖는다. state(기존 객체) 와 action(업데이트할 객체) . 이 두가지 매개변수를 가지고 상태를 업데이트하고 최신 상태를 store에 제공해준다.

내 코드는 우선 월별 지출내역이 없다면 빈배열로 반환하도록 되어있고, 새로운 지출내역을 월별 지출내역에 push 해준다. 그리고 로컬스토리지에 저장. 여기서 내가 받아야할 action.apyload는 선택한 월과 새로운 지출내역이다. 프롭스 드릴링이나 context 를 사용했다면 추가 후에 setState를 사용해 상태를 업데이트해줘야하는데 리듀서 함수 자체가 setState 역할을 하기 때문에 할 필요가 없다.

addExpense: (state, action) => {
      const { selectedMonth, newExes } = action.payload;
      if (!state.expenseList[selectedMonth]) {
        state.expenseList[selectedMonth] = [];
      }
      state.expenseList[selectedMonth].push(newExes);
      localStorage.setItem("expenseList", JSON.stringify(state.expenseList));
      return state;
    },

 

 

📂 redux >  📂 slices > 📄 month.js

선택한 월의 지출내역을 볼 수 있도록 month slice도 생성해주었다. 

import { createSlice } from "@reduxjs/toolkit";

let selectedMonth = JSON.parse(localStorage.getItem("selectedMonth"));

if (!selectedMonth) {
  selectedMonth = new Date().getMonth();
}

const initialState = {
  selectedMonth: selectedMonth,
};

const monthSlice = createSlice({
  name: "month",
  initialState: initialState,
  reducers: {
    setSelectedMonth: (state, action) => {
      state.selectedMonth = action.payload;
    },
  },
});

export const { setSelectedMonth } = monthSlice.actions;
export default monthSlice.reducer;

 

리듀서 함수 생성 후 다른 컴포넌트에서 사용할 수 있게 export 해줘야한다는 것을 잊지 말자.

export const { addExpense } = expense.actions;
export default expense.reducer;
export const { setSelectedMonth } = monthSlice.actions;
export default monthSlice.reducer;

 

 

 

 

 

 

Home.jsx

이제 추가하는 함수가 필요한 컴포넌트에 적용해보도록하자!

useSelector 로 구독할 대상을 설정한다. 구독해야할 대상은 지출내역 데이터가 담긴 expense slice와 어떤 월을 선택했는지 확인할 수 있는 month slice 두개이다. 그리고 useDispatch로 리듀서 함수를 호출해준다. 대신 액션객체를 담아서! 액션 타입에 따라 상태를 어떻게 업데이트해주는지 리듀서 함수에서 정의해주기 때문이다.

import { useDispatch, useSelector } from "react-redux";
import { addExpense } from "../redux/slices/expense";

const Home = () => {
  const dispatch = useDispatch();
  const exes = useSelector((state) => state.expense.expenseList);
  const selectedMonth = useSelector((state) => state.month.selectedMonth);

  console.log("selectedMonth1", selectedMonth);
  const handleMonthSelect = (idx) => {
    dispatch(setSelectedMonth(idx));
  };

  const onInsert = useCallback((date, item, amount, desc) => {
    dispatch(
      addExpense({
        selectedMonth: selectedMonth,
        newExes: {
          id: uuidv4(),
          date,
          item,
          amount,
          desc,
          month: selectedMonth,
        },
      })
    );
  });

 

 

새로운 지출내역을 담아주는 onInsert 함수에 dispatch(리듀서함수(액션객체))를 사용해보자.

우리가 받아야할 액션.페이로드는 seletedMonth와 newExes 였다. 선택한 월과 새로운 지출내역 데이터를 담는 객체를 넣어준다. 리듀서 함수 안 액션 객체 속 우리가 받아야하는 값 페이로드가 객체로 들어가는 이유는 리듀서 함수 정의할 때 객체로 받았기 때문!

const { selectedMonth, newExes } = action.payload;
const onInsert = useCallback((date, item, amount, desc) => {
    dispatch(
      addExpense({
        selectedMonth: selectedMonth,
        newExes: {
          id: uuidv4(),
          date,
          item,
          amount,
          desc,
          month: selectedMonth,
        },
      })
    );

 

dispatch로 선택된 월을 변경한다. 

 const handleMonthSelect = (idx) => {
    dispatch(setSelectedMonth(idx));
  };

 

 

 

 

 

Detail.jsx

📂 redux >  📂 slices > 📄 expense.js 

위 파일에서 삭제, 수정 리듀서 함수를 생성해준다. 삭제는 id 를 액션 페이로드로 받고, 수정은 수정된 데이터를 액션 페이로드로 받아줬다. 초기값이 객체형태였기 때문에 filter(),map() 메서드를 사용하지 못해서 for-in 문으로 객체 month를 키값으로 순회해서 배열로 가지고 왔다. 각 월 지출 내역 데이터를 expensesOfMonth 변수에 할당해주고 filter를 사용해 삭제할 데이터를 제외한 나머지 데이터들을 반환하도록 해주고 반환된 데이터를 기존 월별 데이터에 재할당해주었다. 그리고 로컬스토리지에 저장.

removeExpense: (state, action) => {
      const { id } = action.payload;
      for (const month in state.expenseList) {
        const expensesOfMonth = state.expenseList[month];
        const filteredExpenses = expensesOfMonth.filter(
          (expense) => expense.id !== id
        );
        state.expenseList[month] = filteredExpenses;
      }
      localStorage.setItem("expenseList", JSON.stringify(state.expenseList));
    },
  
  export const { addExpense, removeExpense, modifyExpense } = expense.actions;

 

수정도 마찬가지고 for-in문을 사용해주었고, map메서드를 사용해 수정하려는 데이터의 id가 기존에 있던 데이터와 같다면 기존 데이터를 수정된 데이터로 덮어준다.

 modifyExpense: (state, action) => {
      const { modifiedData } = action.payload;
      for (const month in state.expenseList) {
        const expensesOfMonth = state.expenseList[month];
        const modifyExpense = expensesOfMonth.map((ex) =>
          ex.id === modifiedData.id ? { ...ex, ...modifiedData } : ex
        );
        console.log("modifyExpense", modifyExpense);
        state.expenseList[month] = modifyExpense;
      }
      localStorage.setItem("expenseList", JSON.stringify(state.expenseList));
    },
  },

 

 

 

useSelector로 월 지출내역을 가지고와서 상세페이지에 노출시키려고 보니 오류가 발생했다. 확인해보니 내가 가지고온 데이터는 전체 월 지출내역. 상세페이지에서 필요한 데이터는 해당 월의 지출내역. 완전히 다른 데이터를 가지고 온 것이다. 그래서 가지고온 데이터를 내가 선택한 월과 같은 데이터만 가지고 오도록 find 메서드를 사용해주었다.

import { useDispatch, useSelector } from "react-redux";
import { removeExpense, modifyExpense } from "../redux/slices/expense";

const Detail = () => {
  const { id } = useParams();
  const navigate = useNavigate();
  const dispatch = useDispatch();
  const allExpenses = useSelector((state) => state.expense.expenseList);

  let exe = null;
  for (const month in allExpenses) {
    const expensesOfMonth = allExpenses[month];
    exe = expensesOfMonth.find((expense) => expense.id === id);
    if (exe) break;
  }

  const dateRef = useRef(exe.date);
  const itemRef = useRef(exe.item);
  const descRef = useRef(exe.desc);
  const amountRef = useRef(exe.amount);

  const onRemove = useCallback(() => {
    if (confirm("정말 삭제하시겠습니까?") == true) {
      dispatch(removeExpense({ id }));
      navigate("/");
    } else {
      return false;
    }
  });

  const onModify = useCallback(() => {
    dispatch(
      modifyExpense({
        modifiedData: {
          id: exe.id,
          date: dateRef.current.value,
          item: itemRef.current.value,
          amount: +amountRef.current.value,
          desc: descRef.current.value,
        },
      })
    );

    //navigate(-1);
  });

 

 

길고 길었던 코드가 짧아졌다. 프롭스 드릴링 이슈를 해결하는것 뿐만 아니라 코드도 간결하게 보여줄 수 있는 것 같다. 

const onRemove = useCallback(() => {
    if (confirm("정말 삭제하시겠습니까?") == true) {
      dispatch(removeExpense({ id }));
      navigate("/");
    } else {
      return false;
    }
  });

  const onModify = useCallback(() => {
    dispatch(
      modifyExpense({
        modifiedData: {
          id: exe.id,
          date: dateRef.current.value,
          item: itemRef.current.value,
          amount: +amountRef.current.value,
          desc: descRef.current.value,
        },
      })
    );

    //navigate(-1);
  });