본문 바로가기
Library&Framework/React

리액트 리덕스를 활용한 마이페이지 리팩토링: 코드 개선을 통한 유지보수성 및 재사용성 향상

by 우지uz 2024. 6. 30.

목차

  1. 서론
    • 리팩토링의 필요성
    • 기존 마이페이지 구조
  2. 리팩토링 목표
    • 코드 중복 제거
    • 유지보수성 향상
    • 재사용성 증대
  3. 기존 코드 분석
    • 구조 및 문제점
    • 주요 함수와 컴포넌트
  4. 리팩토링 과정
    • API 유틸리티 분리
    • Redux 액션 및 디스패치 최적화
    • 컴포넌트 간 의존성 감소
  5. 최종 코드 구조
    • 리팩토링 후의 코드 구조
    • 주요 변경 사항
  6. 결론
    • 리팩토링의 결과
    • 향후 계획 및 느낀점

내용:

1. 서론

리팩토링의 필요성

리팩토링은 코드의 기능은 유지하면서 내부 구조를 개선하는 작업입니다. 우리 프로젝트의 '마이페이지'는 기능이 확장되면서 코드가 복잡해지고 중복된 부분이 많아졌습니다. 이를 개선하기 위해 리팩토링을 진행하게 되었습니다.

기존 마이페이지 구조

const navigate = useNavigate(); // 페이지 이동을 위한 네비게이트 훅
const dispatch = useDispatch(); // Redux 디스패치 훅

const [orderGroup, setLocalOrderGroup] = useState({});
const [productInventory, setProductInventory] = useState([]);
function MyPage({
  isLoggedin,
  memberId,
  profileData,
  profileImageUrl,
  setProfileImageUrl,
  cartProducts,
  setCartProducts,
}) {
    const fetchWishLists = async () => {
    try {
      // Authorization 헤더를 포함한 axios 요청
      const response = await axios.get(
        `${process.env.REACT_APP_BACKEND_BASE_URL}/wishlist/${memberId}`,
        {
          headers: {
            Authorization: localStorage.getItem("Token_data"),
          },
        }
      );

      dispatch(setWishList(response.data)); 
    } catch (error) {
      console.error(`잘못된 요청입니다:`, error);
    }
    };

    const fetchPaymentHistory = async () => {
    try {
      // 로컬 스토리지에서 Authorization 토큰 가져오기
      const authorization = localStorage.getItem("Token_data");

      // Authorization 헤더를 포함한 axios 요청
      const response = await axios.get(
        `${process.env.REACT_APP_BACKEND_BASE_URL}/paymenthistory/${memberId}`,
        {
          headers: {
            Authorization: authorization,
          },
        }
      );

      // 그룹화된 데이터로 상태 설정
      const groupedData = groupByOrderId(response.data);
      setLocalOrderGroup(groupedData);
      dispatch(setOrderGroup(groupedData)); 

      // 각 상품에 대해 데이터 조회
      const fetchProductData = async () => {
        const inventories = [];
        for (const orderId in groupedData) {
          const products = groupedData[orderId].products;
          for (const product of products) {
            const productResponse = await axios.get(
              `${process.env.REACT_APP_BACKEND_BASE_URL}/products/${product.productId}`
            );

            if (productResponse.status === 200) {
              const invenResponse = await axios.get(
                `${process.env.REACT_APP_BACKEND_BASE_URL}/inventory`,
                {
                  headers: {
                    Authorization: localStorage.getItem("Token_data"),
                  },
                }
              );

              const invenResData = invenResponse.data;
              const filteredInventory = invenResData.filter(
                (inven) =>
                  parseInt(inven.productId) === parseInt(product.productId)
              );

              inventories.push(...filteredInventory);
            }
          }
        }
        setProductInventory(inventories);
      };

      fetchProductData();
    } catch (error) {
      console.error(`잘못된 요청입니다:`, error);
    }
    };

    const fetchProductReviewData = async () => {
    try {
      // 로컬 스토리지에서 Authorization 토큰 가져오기
      const authorization = localStorage.getItem("Token_data");

      // Authorization 헤더를 포함한 axios 요청
      const response = await axios.get(
        `${process.env.REACT_APP_BACKEND_BASE_URL}/review/user/${memberId}`,
        {
          headers: {
            Authorization: authorization,
          },
        }
      );
      dispatch(setMyReviewList(response.data)); 
    } catch (error) {
      console.error(error.response.data);
    }
    };

    useEffect(() => {
    fetchWishLists();
    fetchPaymentHistory();
    fetchProductReviewData();
    fetchAddressLists(dispatch, memberId);
    }, [memberId]);

	return (
        <div className="my-page">
        	<Routes>
        		<Route path="/example1" element={<Navigate to="example1" replace />} />
                <Route path="/example2" element={<Navigate to="example2" replace />} />
                <Route path="/example3" element={<Navigate to="example3" replace />} />
                <Route path="/example4" element={<Navigate to="example4" replace />} />
                <Route path="/example5" element={<Navigate to="example5" replace />} />
                <Route path="/example6" element={<Navigate to="example6" replace />} />
                <Route path="/example7" element={<Navigate to="example7" replace />} />
                <Route path="/example8" element={<Navigate to="example8" replace />} />
                <Route path="/example9" element={<Navigate to="example9" replace />} />
                <Route path="/example10" element={<Navigate to="example10" replace />} />
                <Route path="/example11" element={<Navigate to="example11" replace />} />
            </Routes>
        </div>
    );
}

기존의 마이페이지는 모든 API 호출과 상태 관리를 한 컴포넌트에서 처리하고 있어 코드가 길어지고 복잡해졌습니다. 이는 유지보수와 기능 확장이 어렵게 만드는 주요 요인이었습니다.

마이 페이지에서는 프로필 페이지, 프로필 수정 페이지, 패스워드 체크 페이지, 배송지 페이지, 배송지 등록 페이지, 배송지 수정 페이지 등 
10개 이상의 페이지 Route 경로를 가지고 설정해주고 있으며, 전체 리스트에 대한 GET Api 는 4개 가지고 있었습니다. 나중에 추가될 수도 있겠죠. 저는 여기서 전체 리스트에 대한 GET Api 를 관리하는 파일을 따로 만들었습니다.

2. 리팩토링 목표

코드 중복 제거

여러 곳에서 반복되는 코드를 제거하여 코드의 길이를 줄이고 가독성을 높이는 것이 목표였습니다.

유지보수성 향상

코드를 모듈화하고 구조화하여 유지보수성을 높이는 것이 주요 목표 중 하나였습니다.

재사용성 증대

공통된 기능을 분리하여 다른 컴포넌트에서도 쉽게 재사용할 수 있도록 하는 것이 목적이었습니다. 

3. 기존 코드 분석

구조 및 문제점

기존 코드는 모든 기능을 한 곳에 모아둔 형태로, API 호출과 상태 관리가 분산되어 있었습니다. 이로 인해 코드 중복과 비효율적인 상태 관리가 발생했습니다. 마이 페이지에서는 실제로 서비스가 운영되는 시점에도, 추가 기능들이 구현되거나, 클라이언트 / 서버에서 리펙토링이 꽤 잦은 편이라 생각됩니다. 이유는 특히나 유저들이 UI/UX 적인 측면에서 관심이 많을 법한 페이지들이 존재한다고 생각하기 때문입니다.

주요 함수와 컴포넌트

  • fetchWishLists: 위시리스트 데이터를 가져오는 함수
  • fetchPaymentHistory: 결제 내역을 가져오는 함수
  • fetchProductReviewData: 리뷰 데이터를 가져오는 함수
  • fetchAddressLists: 주소 데이터를 가져오는 함수

Update, delete, Post 등의 api 는 특정 페이지에서만 사용되는 것이기 때문에
따로 관리해야할 필요성을 느끼진 않았습니다. 너무 세세하게 API 호출에 대한 함수들을 관리한다면, 오히려 생산성이 저해될 수 있다고 생각했습니다.

4. 리팩토링 과정

API 유틸리티 분리

모든 API 호출을 MyPageApiUtils.jsx 파일로 분리하여 컴포넌트 간의 의존성을 줄였습니다.
다른 페이지에서도 관련 함수들을 사용하기 때문에, MyPageApiUtils.jsx 파일에서 가져와 사용할 수 있었습니다.

import axios from "axios";
import {
  setAddressList,
  setMyReviewList,
  setWishList,
  setOrderGroup,
} from "../../store";

export const fetchWishLists = async (dispatch, memberId) => {
  try {
    const response = await axios.get(
      `${process.env.REACT_APP_BACKEND_BASE_URL}/wishlist/${memberId}`,
      {
        headers: {
          Authorization: localStorage.getItem("Token_data"),
        },
      }
    );

    dispatch(setWishList(response.data));
  } catch (error) {
    console.error(`Error fetching wishlist:`, error);
  }
};

export const fetchPaymentHistory = async (
  dispatch,
  memberId,
  setLocalOrderGroup,
  setProductInventory
) => {
  try {
    const authorization = localStorage.getItem("Token_data");

    const response = await axios.get(
      `${process.env.REACT_APP_BACKEND_BASE_URL}/paymenthistory/${memberId}`,
      {
        headers: {
          Authorization: authorization,
        },
      }
    );
	// groupByOrderId 는 결제 내역 데이터를, 주문/상품을 기준으로 데이터를 그룹화해주는 함수입니다.
	// 전체 소스코드에서는 존재하지만, 보안상의 이유로 보여드릴 수 없습니다.
    const groupedData = groupByOrderId(response.data);
    setLocalOrderGroup(groupedData);
    dispatch(setOrderGroup(groupedData));

    const fetchProductData = async () => {
      const inventories = [];
      for (const orderId in groupedData) {
        const products = groupedData[orderId].products;
        for (const product of products) {
          const productResponse = await axios.get(
            `${process.env.REACT_APP_BACKEND_BASE_URL}/products/${product.productId}`
          );

          if (productResponse.status === 200) {
            const invenResponse = await axios.get(
              `${process.env.REACT_APP_BACKEND_BASE_URL}/inventory`,
              {
                headers: {
                  Authorization: localStorage.getItem("Token_data"),
                },
              }
            );

            const invenResData = invenResponse.data;
            const filteredInventory = invenResData.filter(
              (inven) =>
                parseInt(inven.productId) === parseInt(product.productId)
            );

            inventories.push(...filteredInventory);
          }
        }
      }
      setProductInventory(inventories);
    };

    fetchProductData();
  } catch (error) {
    console.error(`Error fetching payment history:`, error);
  }
};

export const fetchProductReviewData = async (dispatch, memberId) => {
  try {
    const authorization = localStorage.getItem("Token_data");

    const response = await axios.get(
      `${process.env.REACT_APP_BACKEND_BASE_URL}/review/user/${memberId}`,
      {
        headers: {
          Authorization: authorization,
        },
      }
    );
    dispatch(setMyReviewList(response.data));
  } catch (error) {
    console.error(`Error fetching product reviews:`, error);
  }
};

export const fetchAddressLists = async (dispatch, memberId) => {
  try {
    const response = await axios.get(
      `${process.env.REACT_APP_BACKEND_BASE_URL}/address/${memberId}`,
      {
        headers: {
          Authorization: localStorage.getItem("Token_data"),
        },
      }
    );
    dispatch(setAddressList(response.data));
  } catch (error) {
    console.error("Error fetching addresses:", error);
  }
};

};

관련 데이터 (위시리스트 데이터, 결제 내역 데이터, 리뷰 데이터, 주소 데이터)를 Redux 상태관리를 통해 

// Configure the Redux store
const store = configureStore({
  reducer: {
    profile: profileReducer, // Include the profile slice reducer
    orderGroup: orderGroupSlice.reducer, // Include the orderGroup slice reducer
    wishList: wishListSlice.reducer, // Include the wishList slice reducer
    myReviewList: myReviewListSlice.reducer, // Include the myReviewList slice reducer
    addressList: addressListSlice.reducer, // Include the addressList slice reducer
  },
});

각각의 데이터에 대한 리듀서를 등록하였고, 관련 데이터를 가져오거나 셋팅해주는 기능이 필요한 페이지에서 사용할 수 있었습니다.

Redux 액션 및 디스패치 최적화

각각의 API 호출 함수에서 Redux 액션을 디스패치하도록 하여 코드 중복을 줄이고 상태 관리를 일원화했습니다.

컴포넌트 간 의존성 감소

컴포넌트 간의 데이터 전달 방식을 개선하여 불필요한 의존성을 제거했습니다.

5. 최종 코드 구조

리팩토링 후, 코드 구조는 다음과 같이 변경되었습니다.

import React, { useState, useEffect } from "react";
import { Routes, Route, Navigate, useNavigate } from "react-router-dom";
import { useDispatch } from "react-redux";

// Component Imports
import { MyProfilePage } from "../MyProfilePage/MyProfilePage.jsx";
import { MyPaymentHistoryPage } from "../MyPaymentHistoryPage/MyPaymentHistoryPage.jsx";
import { PaymentDetailPage } from "../PaymentDetail/PaymentDetailPage.jsx";
import { MyWishListPage } from "../MyWishListPage/MyWishListPage.jsx";
import { MyReviewPage } from "../MyReviewPage/MyReviewPage.jsx";
import { UpdateUserInfoPage } from "../UpdateUserInfoPage/UpdateUserInfoPage.jsx";
import PasswordCheckPage from "../PasswordCheckPage/PasswordCheckPage.jsx";
import { AddressListPage } from "../Address/AddressListPage.jsx";
import { AddressRegistrationPage } from "../Address/AddressRegistrationPage.jsx";
import { AddressUpdatePage } from "../Address/AddressUpdatePage.jsx";
import {
  fetchPaymentHistory,
  fetchAddressLists,
  fetchProductReviewData,
  fetchWishLists,
} from "./MyPageApiUtils.jsx";

// CSS Import
import "./MyPage.css";

function MyPage({
  isLoggedin,
  memberId,
  profileData,
  profileImageUrl,
  setProfileImageUrl,
  cartProducts,
  setCartProducts,
}) {
  const navigate = useNavigate(); // 페이지 이동을 위한 네비게이트 훅
  const dispatch = useDispatch(); // Redux 디스패치 훅

  const [orderGroup, setLocalOrderGroup] = useState({});
  const [productInventory, setProductInventory] = useState([]);

  useEffect(() => {
    fetchWishLists(dispatch, memberId);
    fetchPaymentHistory(
      dispatch,
      memberId,
      setLocalOrderGroup,
      setProductInventory
    );
    fetchProductReviewData(dispatch, memberId);
    fetchAddressLists(dispatch, memberId);
  }, [memberId, dispatch]);
	return (
        <div className="my-page">
        	<Routes>
        		<Route path="/example1" element={<Navigate to="example1" replace />} />
                <Route path="/example2" element={<Navigate to="example2" replace />} />
                <Route path="/example3" element={<Navigate to="example3" replace />} />
                <Route path="/example4" element={<Navigate to="example4" replace />} />
                <Route path="/example5" element={<Navigate to="example5" replace />} />
                <Route path="/example6" element={<Navigate to="example6" replace />} />
                <Route path="/example7" element={<Navigate to="example7" replace />} />
                <Route path="/example8" element={<Navigate to="example8" replace />} />
                <Route path="/example9" element={<Navigate to="example9" replace />} />
                <Route path="/example10" element={<Navigate to="example10" replace />} />
                <Route path="/example11" element={<Navigate to="example11" replace />} />
            </Routes>
        </div>
    );
}

6. 결론

리팩토링의 결과

리팩토링을 통해 코드의 가독성과 유지보수성이 크게 향상되었습니다. 각 기능이 분리되면서 코드의 중복이 줄어들었고, 상태 관리가 명확해졌습니다.

향후 계획

앞으로도 지속적으로 코드 품질을 높이기 위해 리팩토링을 진행할 예정입니다. 또한, 새로운 기능 추가 시 현재 구조를 유지하여 유지보수성을 높일 계획입니다. 

이와 같이 리팩토링을 통해 코드의 구조를 개선해보았습니다. 회원 관련 기능들과 페이지들이 늘어나면서,
당연하게도 코드가 길어지고, 중복되거나 자주 사용되는 함수, 변수 들이 많아 지게 되었습니다.

하지만, 더 좋은 서비스를 제공하기 위해서 
1. 기능들이 개선 되어야 하고
2. 새로운 기능들이 추가 되기도 하며
3. 프로젝트의 규모가 커질 수록

전체적인 구조를 개선하는 것은 중요한 작업인 것 같습니다. 

현재 쇼핑몰 프로젝트에서, FrontEnd, BackEnd 를 둘다 진행하고 있습니다. 
처음에는 API 및 ERD 의 규모도 작았고, 기능이 적었으나
기본적인 기능들이 구현되면서, 클라이언트와 서버단에서의 컴포넌트 구조 및 유지보수의 중요성을 깨달아 가는 것 같습니다! 

포스팅이 길지만, 코드의 유지보수 및 클라이언트 단에서 상태관리의 중요성에 대해서 이야기 드렸고 
아직 리액트를 시작한지 4개월정도 밖에 되지 않은 초보개발자이다보니, 이렇게 작은 변화에 대해서도 포스팅을 하도록 노력해보려 합니다.

아쉬운 점이나, 개선했으면 하는 코드들에 대해서는 댓글 달아주시면 감사하겠습니다.