본문 바로가기

TIL

TIL - 리액트로 가계부 만들기 - 2

오늘은 리액트를 활용하여 가계부 프로젝트를 계속 진행했습니다. CRUD 기능을 완성하고, 상태 관리를 리팩토링하면서 주요 학습 내용과 작업 과정을 정리했습니다.

 

목표

  1. CRUD 기능 완성
  2. 상태 관리 props drilling에서 useContext로 리팩토링
  3. Redux를 활용해서 상태 관리 리팩토링

주요 학습 내용

  1. Update와 Delete 기능 구현
  2. useContext로 상태 관리 리팩토링
  3. Redux를 사용한 상태 관리 리팩토링

1. Update와 Delete 기능 구현

// expense 항목 Update 기능
const updateExpense = (targetExpense, modifiedExpense) => {
  const updatedData = fetchedData.map((expense) =>
    expense.id === targetExpense.id ? modifiedExpense : expense
  );
  localStorage.setItem("dataItem", JSON.stringify(updatedData));
  setFetchedData(updatedData);
};
// expense 항목 Delete 기능
const removeExpense = (targetExpense) => {
  const updatedData = fetchedData.filter(
    (expense) => expense.id !== targetExpense.id
  );
  localStorage.setItem("dataItem", JSON.stringify(updatedData));
  setFetchedData(updatedData);
};

 

 

2. useContext로 상태 관리 리팩토링

// Context 생성 및 Provider 설정: App.jsx
import { createContext, useEffect, useState } from "react";

export const ExpenseContext = createContext();

function App() {
  const [fetchedData, setFetchedData] = useState([]);

  useEffect(() => {
    const loadData = async () => {
      const fetchedData = await fetchData();
      const localData = JSON.parse(localStorage.getItem("dataItem")) || [];
      const combinedData = [
        ...fetchedData.filter(
          (item) => !localData.some((localItem) => localItem.id === item.id)
        ),
        ...localData,
      ];
      setFetchedData(combinedData);
      localStorage.setItem("dataItem", JSON.stringify(combinedData));
    };
    loadData();
  }, []);

  const addExpense = (newExpense) => {
    const updatedData = [...fetchedData, newExpense];
    localStorage.setItem("dataItem", JSON.stringify(updatedData));
    setFetchedData(updatedData);
  };

  return (
    <ExpenseContext.Provider value={{ fetchedData, addExpense, updateExpense, removeExpense }}>
      <RouterProvider router={router} />
    </ExpenseContext.Provider>
  );
}

export default App;

 

 

props drilling의 단점을 해결하기 위해 useContext를 사용하여 상태 관리를 리팩토링했습니다. 이를 통해 컴포넌트 간의 상태 공유가 더 간편해졌습니다.

 

3. Redux로 상태 관리 리팩토링

// configureStore.js
import { configureStore } from "@reduxjs/toolkit";
import fetchedDataReducer from "../fetchedDataSlice";
// Redux 스토어 구성
const store = configureStore({
  reducer: {
    fetchedData: fetchedDataReducer,
  },
});

export default store;

 

 

Redux를 사용하여 중앙 집중식 상태 관리를 구현했습니다.

 

 

// fetchedDataSlice.js
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import fetchData from "../../fetchData";
// 비동기 데이터 로드 Thunk 생성
export const loadFetchedData = createAsyncThunk(
  "fetchedData/loadFetchedData",
  async () => {
    const fetchedData = await fetchData();
    const localData = JSON.parse(localStorage.getItem("dataItem")) || [];
    const combinedData = [
      ...fetchedData.filter(
        (item) => !localData.some((localItem) => localItem.id === item.id)
      ),
      ...localData,
    ];
    localStorage.setItem("dataItem", JSON.stringify(combinedData));
    return combinedData;
  }
);
// Redux 슬라이스 생성
const fetchedDataSlice = createSlice({
  name: "fetchedData",
  initialState: [],
  reducers: {
    addExpense: (state, action) => {
      state.push(action.payload);
      localStorage.setItem("dataItem", JSON.stringify(state));
    },
    updateExpense: (state, action) => {
      const index = state.findIndex(
        (expense) => expense.id === action.payload.id
      );
      if (index !== -1) {
        state[index] = action.payload;
        localStorage.setItem("dataItem", JSON.stringify(state));
      }
    },
    removeExpense: (state, action) => {
      const index = state.findIndex(
        (expense) => expense.id === action.payload.id
      );
      if (index !== -1) {
        state.splice(index, 1);
        localStorage.setItem("dataItem", JSON.stringify(state));
      }
    },
  },
  extraReducers: (builder) => {
    builder.addCase(loadFetchedData.fulfilled, (state, action) => {
      return action.payload;
    });
  },
});

export const { addExpense, updateExpense, removeExpense } =
  fetchedDataSlice.actions;

export default fetchedDataSlice.reducer;

 

비동기 데이터 로드를 위해 createAsyncThunk를 사용하고, fetchedDataSlice에 리듀서와 액션을 설정했습니다.

 

 


리팩토링 하면서 느낀 점

Props Drilling 단점

  • 복잡성 증가: 부모 컴포넌트에서 필요한 props를 여러 레벨(props가 필요하지 않는 컴포넌트)에 걸쳐 전달하면서 코드가 복잡해졌습니다. 
  • 유지보수 어려움: 상태를 변경할 때 중간 컴포넌트가 추가되거나 변경되면 디버깅이 어렵습니다 .

useContext 장단점

장점

  • 간편한 상태 공유: 여러 레벨에 걸쳐 props를 전달할 필요 없이 전역에서 상태를 쉽게 공유할 수 있었습니다. 
  • 간결한 코드: 코드가 간결해지고 가독성이 좋아졌습니다.

단점

  • 성능 문제: Context가 업데이트될 때마다 모든 하위 컴포넌트가 리렌더링 되었습니다..
  • 테스트 어려움: Context를 사용하는 컴포넌트는 테스트 시 Provider를 필요하다고 합니다.

Redux 장단점

장점

  • 중앙 집중식 상태 관리: 애플리케이션의 모든 상태를 Store(저장소)에서 관리할 수 있어 상태의 흐름을 쉽게 파악할 수 있습니다.
  • 예측 가능한 상태 변경:  데이터의 변경이 예측 가능하고 유지 보수성이 증가해서 큰 규모의 애플리케이션에서도 상태 관리가 용이합니다.
  • 확장성: 큰 규모의 애플리케이션에서도 상태 관리가 용이했습니다.

단점

  • 복잡한 설정: 아주 간단한 상태 관리를 위해서도 처음 작성해야 하는 코드량이 많습니다.
  • 러닝 커브: 알아야 하는 개념과 용어가 많아 학습하는 데 시간이 걸렸습니다

결론

오늘은 Update와 Delete 기능을 구현하고, props drilling의 단점을 보완하기 위해 useContext와 Redux를 각각 이용해서 상태 관리를 리팩토링해봤습니다. 이를 통해 코드의 복잡성을 줄이고 가독성과 유지보수성을 높일 수 있었습니다. useContext는 간편하게 전역 상태를 관리할 수 있지만 성능 문제가 있을 수 있고, Redux는 강력한 상태 관리 도구지만 초기 설정이 복잡할 수 있다는 것을 학습 했습니다.