이제 마지막 ! context 에 이어 리덕스 툴킷 라이브러리로 전역상태 관리하기!
📌 Context로 만든 지출 관리 사이트
컴포넌트
컴포넌트 | 설명 |
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);
});
'프로젝트' 카테고리의 다른 글
Rendered more hooks than during the previous render. (0) | 2024.06.17 |
---|---|
[팀프로젝트] 리액트로 뉴스피드 웹사이트 제작하기 - 사전기획 (0) | 2024.06.13 |
[리액트] 개인 지출 관리 사이트 맹들기 - Context🐢 (0) | 2024.05.31 |
[리액트] 개인 지출 관리 사이트 맹들기🐢 (0) | 2024.05.30 |
[리액트] 리액트로 ToDoList 만들기 - 토글 기능 (0) | 2024.05.20 |