본문 바로가기

지출 내역 APP (개인 프로젝트)

개인 프로젝트(React) : 지출 내역 리스트 -7 Redux

useContext => Redux (Redux ToolKit 사용)

가이드를 보기 전 일단 내 블로그에 정리한 Redux를 보고 해봤다.

1. 리덕스 및 RTK 설치

터미널

yarn add redux react-redux

yarn add @reduxjs/toolkit

 

2. 폴더 구조 생성

src폴더에 redux폴더 생성 후 

redux폴더에 config폴더와 slices폴더 생성

config폴더에 configStore.js 파일 생성

slices폴더에 expense.js 및 listMonth.js 파일 생성

생성한 store를 main.jsx에 주입

 

expense.js

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

const initialState = {
  expenses: [... // 기존 expenses 더미데이터
  ],
};

const expenseSlice = createSlice({
  name: "setExpenses",
  initialState,
  reducers: {
    addExpense: (state, action) => { // 추가
      state.expenses = [...state.expenses, action.payload];
    },
    deleteExpense: (state, action) => { // 삭제
      state.expenses = state.expenses.filter(
        (expense) => expense.id !== action.payload.currentId
      );
    },
    updateExpense: (state, action) => { // 수정
      state.expenses = state.expenses.map((expense) => {
        if (expense.id === action.payload.id) {
          return {
            id: action.payload.id,
            date: action.payload.date,
            item: action.payload.item,
            amount: +action.payload.amount,
            description: action.payload.description,
          };
        } else {
          return expense;
        }
      });
    },
  },
});

export const { addExpense, selectExpenses, deleteExpense, updateExpense } =
  expenseSlice.actions;
export default expenseSlice.reducer;

지출 내역의 초기 더미데이터 및 추가, 삭제, 수정 로직을 모아놓음

 

listMonth.js

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

const initialState = {
  selectedMonth: 1,
};

const listMonthSlice = createSlice({
  name: "setListMonth",
  initialState,
  reducers: {
    changeMonth: (state, action) => {
      state.selectedMonth = action.payload;
    },
  },
});

export const { changeMonth } = listMonthSlice.actions;
export default listMonthSlice.reducer;

월을 선택하는 로직이 있고, 초기 선택 월은 1월로 설정

 

configStore.js

import { configureStore } from "@reduxjs/toolkit";
import expenseSlice from "../slices/expense";
import listMonthSlice from "../slices/listMonth";

const store = configureStore({
  reducer: {
    expenses: expenseSlice,
    listMonth: listMonthSlice,
  },
});

export default store;

store에 모듈들 등록

 

main.jsx

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
import { Provider } from "react-redux";
import store from "./redux/config/configStore.js";

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
)

main.jsx에 store 주입

 

Context에서는 최상위 컴포넌트를 Provider로 감싸야했지만 Redux는 그럴 필요없이 정말 필요한 곳에서만 뽑아쓰면 됨

Context를 사용했던 컴포넌트들에서 Context를 모두 지우고 Redux로 바꿔줌

 

지출 추가 (AddExpense.jsx)

const AddExpense = () => {
    const dispatch = useDispatch();

    const handleSubmitForm = (event) => {
        event.preventDefault();
        const formData = new FormData(event.target);
        const date = formData.get("date");
        const item = formData.get("item");
        const amount = formData.get("amount");
        const description = formData.get("description");

        if (!item.trim() || !amount.trim() || !description.trim()) {
            event.target.reset();
            return alert("제대로 입력하세요!");
        }
        event.target.reset();
        dispatch(addExpense({
            id: uuidv4(),
            date,
            item,
            amount: +amount,
            description,
        }));
    }

addExpense라는 액션 크리에이터에 폼에 입력한 id, date, item, amount, description을 payload로 전달

(amount는 그냥 보내면 문자열로 저장되어서 형변환 해줌)

 

액션 크리에이터 (추가)

addExpense: (state, action) => {
      state.expenses = [...state.expenses, action.payload];
    },

기존의 지출 배열에서 Spread operator를 이용해 payload로 전달받은 객체를 추가 후 저장

 

선택 월 변경 (MonthSelect.jsx)

import React, { useEffect, useState } from 'react'
import { useDispatch } from 'react-redux';
import styled from 'styled-components'
import { changeMonth } from "../redux/slices/listMonth";

const MonthSelect = () => {
    const dispatch = useDispatch();

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

    const loadedLastSelectMonth = +localStorage.getItem("lastSelect");

    useEffect(() => {
        if (loadedLastSelectMonth) {
            dispatch(changeMonth(loadedLastSelectMonth));
        }
    }, []);

    const [activeMonth, setActiveMonth] = useState(loadedLastSelectMonth || 1);

    const handleSelectMonth = (month) => {
        localStorage.setItem("lastSelect", month);
        setActiveMonth(month);
        dispatch(changeMonth(month));
    };

listMonth의 state를 변경 (선택한 월)

처음 렌더링시 listMonth를 로컬스토리지에 저장되어있던 lastSelect의 월(마지막으로 선택한 월)로 변경

lastSelect의 값이 없다면 listMonth의 초기상태인 1로 설정됨

 

액션 크리에이터 (월 변경)

changeMonth: (state, action) => {
      state.selectedMonth = action.payload;
    },

payload로 전달받은 월로 상태 변경

 

변경할 월을 리스트로 보여주기 (ExpenseList.jsx)

import React from 'react'
import styled from 'styled-components';
import ExpenseItem from './ExpenseItem'
import { useSelector } from 'react-redux';

const ExpenseList = () => {
    const { expenses } = useSelector(state => state.expenses);
    const { selectedMonth } = useSelector(state => state.listMonth);
    const selectedList = expenses.filter(expense => +expense.date.slice(5, 7) === selectedMonth);
selectedList.map(expense => {
                    return (
                        <ExpenseItem
                            key={expense.id}
                            expense={expense}
                        />
                    );
                })

지출 내역과 선택한 월을 useSelector를 이용해서 store에서 가져옴

기존 지출내역에서 지출 날짜의 월과 선택 월이 같은 지출 내역들을 필터링해서 selectedList에 할당해주고

selectedList를 map함수와 props를 이용해서 선택한 월의 지출 리스트를 화면에 렌더링 해줌

(이때, props를 안쓰고싶은데 튜터님께 물어봐야할 것 같음)

 

지출 내역의 상세페이지 및 수정, 삭제 (DetailExpense.jsx)

import React, { useRef } from 'react'
import styled from 'styled-components'
import { useLocation, useNavigate } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { deleteExpense, updateExpense } from '../redux/slices/expense';

const DetailExpense = () => {
    const { expenses } = useSelector(state => state.expenses);
    const dispatch = useDispatch();
    const location = useLocation();
    const currentId = useRef(location.pathname.slice(8)).current;
    const [prevExpense] = expenses.filter(expense => expense.id === currentId);

id와 expenses를 이용해서 현재 해당하는 지출의 정보들을 prevExpense에 담음

 

삭제 로직

const handleDelete = (currentId) => {
        const confirmDelete = confirm("정말로 삭제하시겠습니까?");
        if (confirmDelete) {
            dispatch(deleteExpense({ currentId }));
            handleComeBackHome();
        }
    }

확인창이 나오고 확인을 누르면 삭제 로직 수행

deleteExpense에 id를 전달해 삭제 진행 후 메인 페이지로 돌아감

 

액션 크리에이터 (삭제)

deleteExpense: (state, action) => {
      state.expenses = state.expenses.filter(
        (expense) => expense.id !== action.payload.currentId
      );
    },

payload로 전달받은 id에 해당하지 않는 expense들만 배열에 할당

 

수정 로직

const handleUpdate = (event) => {
        event.preventDefault();
        const id = currentId;
        const formData = new FormData(event.target);
        const date = formData.get("date");
        const item = formData.get("item");
        const amount = formData.get("amount");
        const description = formData.get("description");

        dispatch(updateExpense({ id, date, item, amount, description }));
        handleComeBackHome();
    }

id는 현재 페이지의 id, 그 외의 데이터들은 폼에 입력한 데이터를 updateExpense에 전달해서 수정 진행 후

메인 페이지로 돌아감

 

액션 크리에이터 (수정)

updateExpense: (state, action) => {
      state.expenses = state.expenses.map((expense) => {
        if (expense.id === action.payload.id) {
          return {
            id: action.payload.id,
            date: action.payload.date,
            item: action.payload.item,
            amount: +action.payload.amount,
            description: action.payload.description,
          };
        } else {
          return expense;
        }
      });
    },

payload로 전달받은 id와 일치하는 지출은 전달받은 날짜, 항목, 금액, 내용을 기존 객체에 덮어쓰고

(금액은 숫자로 저장하기 위해 형변환)

일치하지 않는 지출들을 변경없이 배열에 할당

 

-----------------------------------------------------------------------------------------------------------------------------------------------------------------

 

Context를 사용했을 때는 상태만 갖다쓰고 상태의 변경은 컴포넌트에서 했었다.

하지만 Redux를 사용하면 상태 뿐만아니라 상태를 변경하는 로직 또한 모듈에서 작성할 수 있기 때문에 컴포넌트에서 더 깔끔하게 보인다.

그리고 만약 여러 컴포넌트에서 동일한 로직을 사용한다면 Redux를 이용해서 모듈에서 작성한 함수를 필요할 때마다 꺼내서 사용하면 Context 보다 더 좋을 것 같다.

 

이번 개인프로젝트를 하면서 제일 힘들었던 것 같다.

어제 다 완성하고 너무 힘들어서 그대로 vscode 껐다.

Redux가 아직 나한테 너무 어렵다.

 

액션 크리에이터에 아무리 데이터를 전달해도 변경이 안되어서 뭐가 문제인지 계속 고민해봤는데

그냥 내가 전달할 때, 이름만 전달할게 아니라 destructuring을 해주고 넣어야했었다.

또한 id로 필터링할 때, 데이터 타입을 신경써줬어야 했는데 그렇지 못 해서 시간이 더 걸렸다.

대부분 이런 문제였어서, 문제점을 발견하고부터는 그래도 잘 풀렸다.

 

다행히 기능은 모두 그대로 잘 되고 콘솔로 찍어봐도 내가 원하는 대로 잘 나왔다.

근데 아직 스타일링이 너무 별로라서 스타일 수정하고 선택 요구 사항까지 마쳐야겠다.