디자인 패턴과 함수형 프로그래밍 31일차

anvel·2025년 4월 21일

항해 플러스

목록 보기
17/39

항해 플러스 프론트엔드 - 디자인 패턴과 함수형 프로그래밍

디자인 패턴과 함수형 프로그래밍

이번 주차는 디자인 패턴과 함수형 프로그래밍이라는 주제로, 기능은 모두 구현이 되어있는 React 기반의 프로젝트 내에서, 순수 함수와 액션 함수들을 구분하여 분리를 하는 과정을 학습하는 내용이었습니다.

디자인 패턴이란?

소프트 웨어 개발에서 자주 반복되는 문제를 해결하기 위한 검증된 설계방법을 의미합니다.

핵심 특징

  • 문제 해결 중심: 특정 맥락(Context)에서 발생하는 반복적인 구조적 문제를 해결
  • 재사용 가능: 다른 팀이나 프로젝트에서도 재사용할 수 있는 설계 아이디어
  • 공통 언어 제공: 개발자들 간에 설계에 대한 의사소통을 원활하게 함

함수형 프로그램이이란?

함수형 프로그래밍(FP)이란 순수 함수와 수학적 함수 개념을 기반으로 한 프로그래밍 스타일이며, 데이터와 상태 변경을 최소화하고, 함수의 조합추상화를 통해 프로그램을 구성합니다.

함수형 프로그래밍의 장점

  • 예측 가능한 코드 (동일한 입력 → 동일한 결과)
  • 테스트 용이성 (사이드 이펙트가 없으므로)
  • 재사용과 조합성이 뛰어남 (작은 함수 단위로 설계)
  • 선언적 프로그래밍 → 읽기 쉬운 코드

React에서의 활용

사실 근래의 프레임워크는 이미 디자인 패턴들이 기저에서 동작하고 있고, 개발자는 로직 개발에만 집중할 수 있는 환경이 이미 구성되어있다는 상태입니다.

  • useCart.ts
// useCart.ts
import { useCallback, useState } from "react";
import { CartItem, Coupon, LOCAL_STORAGE_KEY, Product } from "../../types";
import { addToCartCheckStock, calculateCartTotal, updateCartItemQuantity } from "../models/cart";
import { useLocalStorage } from "./useLocalStorage";

export const useCart = () => {
  // const [cart, setCart] = useState<CartItem[]>([]);
  const [cart, setCart] = useLocalStorage<CartItem[]>(LOCAL_STORAGE_KEY.CART, []);
  const [selectedCoupon, setSelectedCoupon] = useState<Coupon | null>(null);

  const addToCart = useCallback((product: Product) => {
    setCart((prev) => addToCartCheckStock(prev, product));
  }, []);

  const removeFromCart = useCallback((productId: string) => {
    setCart((prev) => updateCartItemQuantity(prev, productId, 0));
  }, []);

  const updateQuantity = useCallback((productId: string, newQuantity: number) => {
    setCart((prev) => updateCartItemQuantity(prev, productId, newQuantity));
  }, []);

  const applyCoupon = useCallback((coupon: Coupon) => {
    setSelectedCoupon(coupon);
  }, []);

  const calculateTotal = () => calculateCartTotal(cart, selectedCoupon);

  const getRemainingStock = useCallback(
    (product: Product) => {
      const cartItem = cart.find((item) => item.product.id === product.id);
      return product.stock - (cartItem?.quantity || 0);
    },
    [cart]
  );

  return {
    cart,
    addToCart,
    removeFromCart,
    updateQuantity,
    applyCoupon,
    calculateTotal,
    selectedCoupon,
    getRemainingStock,
  };
};
  • cart.ts
import { CartItem, Coupon, DISCOUNT_TYPE, Product } from "../../types";

export const calculateItemTotal = (item: CartItem) => {
  const { product, quantity } = item;
  const total = product.price * quantity;
  const applicableDiscount = getMaxApplicableDiscount(item);
  if (!applicableDiscount || applicableDiscount >= 1) {
    return total;
  }
  return total * (1 - applicableDiscount);
};

// 컴포넌트에서 최대 할인 금액 표기 UI
export const getMaxDiscount = (discounts: { quantity: number; rate: number }[]) => {
  return discounts.reduce((max, discount) => Math.max(max, discount.rate), 0);
};

export const getMaxApplicableDiscount = ({ quantity, product: { discounts } }: CartItem) => {
  // 상품 수량과 제한 기준에 따른 적용가능한 할인율
  return discounts.reduce((acc, { quantity: limit, rate }) => {
    return quantity >= limit ? Math.max(acc, rate) : acc;
  }, 0);
};

// 수량에 따른 할인금액 계산 내부함수
const _calculateCartTotalWithoutCoupon = (cart: CartItem[]) => {
  return cart.reduce(
    ({ totalBeforeDiscount, quantityDiscount: totalDiscount }, item) => {
      const itemTotal = item.product.price * item.quantity;
      return {
        totalBeforeDiscount: totalBeforeDiscount + itemTotal,
        quantityDiscount: totalDiscount + itemTotal * getMaxApplicableDiscount(item),
      };
    },
    { totalBeforeDiscount: 0, quantityDiscount: 0 }
  );
};

// 쿠폰 유무, 타입에 따른 할인액 계산 내부함수
const _calculateCouponDiscountValue = (selectedCoupon: Coupon | null, totalBeforeDiscount: number, totalDiscount: number) => {
  if (!selectedCoupon) return 0;
  else if (selectedCoupon.discountType === DISCOUNT_TYPE.AMOUNT) return selectedCoupon.discountValue;
  else return (totalBeforeDiscount - totalDiscount) * (selectedCoupon.discountValue / 100); // DISCOUNT_TYPE.PERCENTAGE
};

export const calculateCartTotal = (cart: CartItem[], selectedCoupon: Coupon | null) => {
  // 빈 장바구니 예외처리 -> 할인 쿠폰이 음수값이 적용 가능함
  if (!cart.length) return { totalBeforeDiscount: 0, totalAfterDiscount: 0, totalDiscount: 0 };

  // 쿠폰 적용 전 할인된 총액, 기본 할인 액
  const { totalBeforeDiscount, quantityDiscount } = _calculateCartTotalWithoutCoupon(cart);

  // 쿠폰 적용 후 할인된 총액
  const couponDiscountValue = _calculateCouponDiscountValue(selectedCoupon, totalBeforeDiscount, quantityDiscount);

  // 최종 할인 금액, 쿠폰 적용 할인 금액
  const totalDiscount = quantityDiscount + couponDiscountValue;
  const totalAfterDiscount = totalBeforeDiscount - totalDiscount;
  return {
    totalBeforeDiscount,
    totalAfterDiscount,
    totalDiscount,
  };
};

export const updateCartItemQuantity = (cart: CartItem[], productId: string, newQuantity: number): CartItem[] => {
  // 0이면 제거해야한다.
  if (newQuantity === 0) {
    return cart.filter(({ product }) => product.id !== productId);
  }

  // 재고가 충분히 있다면 새로운 수량으로 변경
  return cart.map((item) => {
    if (item.product.id !== productId) return item;

    const quantity = item.product.stock >= newQuantity ? newQuantity : item.product.stock;
    return { ...item, quantity };
  });
};

// 장바구니에 추가할때 새배열 함수, 재고 확인 로직 포함
export const addToCartCheckStock = (cart: CartItem[], product: Product) => {
  const newCart = [...cart];
  const idx = newCart.findIndex((item) => item.product.id === product.id);

  // 있는 경우 재고 확인 후 추가
  if (idx !== -1) {
    const item = newCart[idx];
    const quantity = Math.min(item.quantity + 1, product.stock);
    newCart[idx] = { ...item, quantity };
    return newCart;
  }

  return [...newCart, { product, quantity: 1 }];
};

이번 과제에서는 이중 함수형 프로그래밍을 위한 순수함수를 구분하는 것을 중점적으로 수행중에 있습니다.
아직 기능 구현과 함수의 분리 정도만 이뤄진 상태인데, 과제를 추가로 진행하면서 유틸함수와 순수함수를 구분하는 능력을 숙달해보도록 하겠습니다.

0개의 댓글