Skip to content

[10팀 홍혜원] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍#60

Open
Wonny-ing wants to merge 20 commits into
hanghae-plus:mainfrom
Wonny-ing:main
Open

[10팀 홍혜원] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍#60
Wonny-ing wants to merge 20 commits into
hanghae-plus:mainfrom
Wonny-ing:main

Conversation

@Wonny-ing
Copy link
Copy Markdown

@Wonny-ing Wonny-ing commented Apr 24, 2025

과제의 핵심취지

https://wonny-ing.github.io/front_5th_chapter2-2/index.refactoring.html

  • React의 hook 이해하기
  • 함수형 프로그래밍에 대한 이해
  • 액션과 순수함수의 분리

과제에서 꼭 알아가길 바라는 점

  • 엔티티를 다루는 상태와 그렇지 않은 상태 - cart, isCartFull vs isShowPopup
  • 엔티티를 다루는 컴포넌트와 훅 - CartItemView, useCart(), useProduct()
  • 엔티티를 다루지 않는 컴포넌트와 훅 - Button, useRoute, useEvent 등
  • 엔티티를 다루는 함수와 그렇지 않은 함수 - calculateCartTotal(cart) vs capaitalize(str)

기본과제

  • Component에서 비즈니스 로직을 분리하기

  • 비즈니스 로직에서 특정 엔티티만 다루는 계산을 분리하기

  • 뷰데이터와 엔티티데이터의 분리에 대한 이해

  • entities -> features -> UI 계층에 대한 이해

  • Component에서 사용되는 Data가 아닌 로직들은 hook으로 옮겨졌나요?

  • 주어진 hook의 책임에 맞도록 코드가 분리가 되었나요?

  • 계산함수는 순수함수로 작성이 되었나요?

  • Component에서 사용되는 Data가 아닌 로직들은 hook으로 옮겨졌나요?

  • 주어진 hook의 책임에 맞도록 코드가 분리가 되었나요?

  • 계산함수는 순수함수로 작성이 되었나요?

  • 특정 Entitiy만 다루는 함수는 분리되어 있나요?

  • 특정 Entitiy만 다루는 Component와 UI를 다루는 Component는 분리되어 있나요?

  • 데이터 흐름에 맞는 계층구조를 이루고 의존성이 맞게 작성이 되었나요?

심화과제

  • 재사용 가능한 Custom UI 컴포넌트를 만들어 보기

  • 재사용 가능한 Custom 라이브러리 Hook을 만들어 보기

  • 재사용 가능한 Custom 유틸 함수를 만들어 보기

  • 그래서 엔티티와는 어떤 다른 계층적 특징을 가지는지 이해하기

  • UI 컴포넌트 계층과 엔티티 컴포넌트의 계층의 성격이 다르다는 것을 이해하고 적용했는가?

  • 엔티티 Hook과 라이브러리 훅과의 계층의 성격이 다르다는 것을 이해하고 적용했는가?

  • 엔티티 순수함수와 유틸리티 함수의 계층의 성격이 다르다는 것을 이해하고 적용했는가?

과제 셀프회고

과제를 하면서 내가 제일 신경 쓴 부분은 무엇인가요?

과제를 다시 해보면 더 잘 할 수 있었겠다 아쉬운 점이 있다면 무엇인가요?

리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문 편하게 남겨주세요 :)

리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문

🎯 함수형 프로그래밍을 적용한 리팩토링 여정

프론트엔드 개발을 하다 보면 점점 커져가는 코드베이스 속에서 “어떻게 하면 코드 품질을 유지하면서 더 쉽게 유지보수할 수 있을까?”라는 고민이 자연스럽게 생기게 됩니다.

특히 컴포넌트를 설계할 때 가장 큰 고민 중 하나는 “어디까지를 컴포넌트에 맡겨야 하는가?”입니다. UI와 상태, 그리고 그 상태를 만드는 비즈니스 로직이 한데 섞여 있을 경우, 코드의 재사용성과 유지보수성이 빠르게 떨어지는 것 같습니다.

그래서 이번 과제에서는 UI와 비즈니스 로직의 관심사를 명확히 분리하는 방향으로 리팩토링을 진행하려고 했습니다!

📦 1. 관심사 분리로 코드 구조 재정비하기

🔄 기존 문제점

리팩토링 이전 코드는 다음과 같은 문제를 안고 있었습니다.

  • 컴포넌트 내부에 비즈니스 로직과 UI 로직이 혼재되어 있어 가독성이 낮았고
  • 상태 관리가 일관되지 않고 흩어져 있었으며
  • 코드 재사용성이 떨어졌습니다

이 문제들을 해결하기 위해 프로젝트의 구조 자체를 손보기로 했습니다.

1.1 폴더 구조 작성

리팩토링의 시작은 폴더 구조를 새로 짠 게 아니라, 기존에 어느 정도 갖춰져 있던 기본 구조 위에서 components 폴더 내부를 정리하는 것부터였습니다.

components 안에 페이지 단위(admin, cart 등) 와 그 안에서 다시 엔티티 단위(coupon, product 등) 로 컴포넌트들을 나눴습니다.

src/
  ├── components/
  │   ├── admin/
  │   │   ├── coupon
  │   │   ├── product
  │   │   └── ...
  │   ├── cart/
  │   │   ├── CartLineItem.tsx
  │   │   ├── OrderSummary.tsx
  │   │   └── ...
  │   └── ui/
  │       ├── Button.tsx
  │       └── ...
  ├── hooks/
  │   ├── useCart.ts
  │   ├── useProductEdit.ts
  │   └── ...
  ├── models/
  │   ├── cart.ts
  │   ├── product.ts
  │   └── ...
  └── contexts/
      ├── CartContext.tsx
      ├── ProductContext.tsx
      └── ...
  • admin/에는 관리자 페이지에서 쓰이는 컴포넌트들이 들어가고, 그 안에서 coupon, product 등으로 더 잘게 나눴습니다.
  • cart/는 장바구니 페이지에서 쓰이는 UI 컴포넌트들이 있습니다.
  • ui/는 버튼이나 인풋처럼 재사용 가능한 공통 컴포넌트들이 있습니다.

어떤 페이지에서 어떤 컴포넌트를 쓰는지 한눈에 파악할 수 있게 됐고, 나중에 새로운 기능을 추가할 때도 어디에 코드를 둬야 할지 명확해져서 훨씬 수월해졌습니다.


1.2 컴포넌트 분해: 하나의 책임만 갖도록!

그 다음은 컴포넌트의 책임을 명확히 분리하는 작업이었습니다.

리팩토링 전에는 모든 로직을 하나의 컴포넌트(AdminPage)에서 처리하고 있었습니다. 상태 관리, 렌더링, 이벤트 처리까지… 모든 게 한 파일 안에 들어 있다 보니 유지보수가 정말 어려웠습니다.

이를 아래처럼 역할에 따라 나눈 특화된 컴포넌트들로 쪼갰습니다.

// 리팩토링 전
export const AdminPage = ({ products, coupons, onProductUpdate, onProductAdd, onCouponAdd }) => {
  // 수백 줄의 코드...
}

// 리팩토링 후
export const AdminPage = ({ initialProducts, initialCoupons }) => (
  <ProductProvider initialProducts={initialProducts}>
    <CouponProvider initialCoupons={initialCoupons}>
      <div className="container mx-auto p-4">
        <h1 className="text-3xl font-bold mb-6">관리자 페이지</h1>
        <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
          <ProductManagement />
          <CouponManagement />
        </div>
      </div>
    </CouponProvider>
  </ProductProvider>
)

이제 ProductManagement는 상품 관련 기능만, CouponManagement는 쿠폰 관련 기능만 담당하게 되었습니다. 컴포넌트마다 단일 책임 원칙을 지키도록 리팩토링을 했습니다.


🧼 2. 순수 함수 중심으로 로직 정리하기

함수형 프로그래밍의 또 하나의 핵심은 순수 함수(Pure Function)입니다.

  • 입력에 따라 항상 동일한 출력을 반환하고
  • 외부 상태에 의존하지 않으며
  • 부작용(side effects)이 없는 함수입니다.

이러한 성질 덕분에 테스트가 쉬워지고, 예측 가능한 코드 흐름을 만들 수 있습니다.

예시: 장바구니 금액 계산

기존 코드에서는 장바구니 금액을 컴포넌트 안에서 계산하고 있었습니다. 이를 별도의 순수 함수로 분리해 더 깔끔하게 정리했습니다.

// before (컴포넌트 내부)
const totalPrice = cartItems.reduce((acc, item) => acc + item.price * item.quantity, 0)

// after (순수 함수로 분리)
export const calculateCartTotal = (items: CartItem[]) =>
  items.reduce((acc, item) => acc + item.price * item.quantity, 0)

이제 이 함수는 어디서든 재사용할 수 있고, 테스트도 간편해졌습니다.


🧩 3. 상태 관리 로직의 추상화

복잡한 컴포넌트 로직을 효과적으로 관리하기 위해 상태 관리 코드를 추상화하는 작업을 진행했습니다.
주된 방법으로는 커스텀 훅을 통한 로직 분리, 그리고 Context API를 활용한 전역 상태 관리가 있습니다. 이를 통해 코드의 응집도는 높이고, UI와 로직을 명확히 분리하여 가독성과 유지보수성을 개선할 수 있었습니다!

3.1 커스텀 훅으로 로직 분리

컴포넌트를 작성하다 보면 비즈니스 로직과 뷰가 한 파일에 뒤섞이는 일이 흔합니다. 처음에는 이 방식이 빠르고 직관적이지만, 기능이 늘어나면서 로직과 UI가 얽혀버려 재사용이 어렵고 테스트하기도 힘든 코드가 된다고 느꼈습니다.

컴포넌트 내부에 비즈니스 로직과 UI 렌더링 코드가 혼재되어 있을 경우, 기능의 확장이나 테스트가 어려워지는 문제가 있습니다. 이를 해결하기 위해, 로직을 useXXX 형태의 커스텀 훅으로 추출하고 컴포넌트는 오직 UI를 표현하는 데 집중하도록 리팩토링했습니다.

변경 전

function ProductEditForm({ product, onProductUpdate, onEditComplete }) {
  const [editedProduct, setEditedProduct] = useState(product);

  const handleNameChange = (e) => {
    setEditedProduct({ ...editedProduct, name: e.target.value });
  };

  return (
    <div>
      <input value={editedProduct.name} onChange={handleNameChange} />
      {/* 기타 UI 요소들 */}
    </div>
  );
}

변경 후

// hooks/useProductEdit.ts
export function useProductEdit(product, updateProduct, onEditComplete) {
  const [editedProduct, setEditedProduct] = useState(product);

  const handleProductNameUpdate = (newName) => {
    setEditedProduct((prev) => ({ ...prev, name: newName }));
  };

  // 기타 핸들러들...

  return {
    editedProduct,
    handleProductNameUpdate,
    // 필요한 값들 반환
  };
}
// components/product/ProductEditForm.tsx
function ProductEditForm({ product, onProductUpdate, onEditComplete }) {
  const {
    editedProduct,
    handleProductNameUpdate,
    // 기타 필요한 값과 함수들...
  } = useProductEdit(product, onProductUpdate, onEditComplete);

  return (
    <div>
      <input
        value={editedProduct.name}
        onChange={(e) => handleProductNameUpdate(e.target.value)}
      />
      {/* 기타 UI 요소들 */}
    </div>
  );
}

이와 같이 로직을 분리하면, 컴포넌트는 오직 UI 렌더링에만 집중할 수 있어 훨씬 더 직관적인 코드가 됩니다.

또한 훅 단위의 테스트도 가능해져, 유지보수가 쉬워지는 장점이 있었습니다.


3.2 Context API를 활용한 전역 상태 관리

컴포넌트 트리의 깊이가 깊어질수록 props를 여러 단계로 전달하는 일명 "props drilling" 문제가 발생할 수 있습니다. 이를 해결하기 위해 Context API를 활용해 공통 상태를 전역으로 관리하도록 구성했습니다.

Context 정의

// contexts/ProductContext.tsx
export function ProductProvider({ children, initialProducts }) {
  const { products, updateProduct, addProduct } = useProducts(initialProducts);

  return (
    <ProductContext.Provider value={{ products, updateProduct, addProduct }}>
      {children}
    </ProductContext.Provider>
  );
}

사용 예시

// components/product/ProductList.tsx
function ProductList() {
  const { products } = useProductContext();

  return (
    <div>
      {products.map((product) => (
        <ProductItem key={product.id} product={product} />
      ))}
    </div>
  );
}

Context를 도입함으로써 데이터 흐름이 간결해지고, 각 컴포넌트는 필요한 데이터에만 의존할 수 있게 되어 결합도를 낮출 수 있었습니다.


🖊️ 4. 순수 함수를 통한 비즈니스 로직 분리

실제 서비스를 만들다 보면, 단순한 계산이라도 특정 엔티티의 규칙을 정확히 반영해야 하는 경우가 많습니다.

예를 들어 "장바구니 금액 계산", "쿠폰 할인 적용", "배송비 판단" 등은 모두 개별 엔티티에 대한 명확한 도메인 규칙이 필요한 영역이라 이런 계산들을 일반적인 유틸 함수처럼 뭉뚱그려 처리하면 엔티티의 의미가 흐려지고 이후 규칙이 바뀌었을 때 영향을 파악하기 어려워진다고 생각합니다.

그래서 calculateDiscountedPrice, getEligibleCoupons, isFreeShippingAvailable 등의 유틸을 엔티티별로 명확히 나누고, 해당 로직에 필요한 구조를 중심으로만 작성했습니다.

그리고 복잡한 계산 로직이나 상태 변형 로직은 컴포넌트와 분리하여 순수 함수(pure function)로 작성하도록 했습니다. 왜냐하면 순수 함수는 동일한 입력에 항상 동일한 출력을 보장하고, 외부 상태를 변경하지 않기 때문에 테스트와 디버깅이 훨씬 수월하기 때문입니다.

4.1 계산 로직을 순수 함수로 분리

// models/cart.ts
export const calculateItemTotal = (item: CartItem) => {
  return item.product.price * item.quantity * (1 - getMaxApplicableDiscount(item));
};

export const calculateTotalBeforeDiscount = (cart: CartItem[]) => {
  return cart.reduce((total, item) => {
    return total + item.product.price * item.quantity;
  }, 0);
};

export const calculateCartTotal = (cart: CartItem[], selectedCoupon: Coupon | null) => {
  const totalBeforeDiscount = calculateTotalBeforeDiscount(cart);
  const totalAfterProductDiscount = calculateTotalAfterProductDiscount(cart);
  const totalAfterDiscount = applyCouponDiscount(totalAfterProductDiscount, selectedCoupon);
  const totalDiscount = totalBeforeDiscount - totalAfterDiscount;

  return {
    totalBeforeDiscount: Math.round(totalBeforeDiscount),
    totalAfterDiscount: Math.round(totalAfterDiscount),
    totalDiscount: Math.round(totalDiscount),
  };
};

순수 함수로 분리된 로직은 단위 테스트 작성이 매우 쉬워지고, 다른 모듈에서도 재사용할 수 있어 개발 효율성을 높일 수 있습니다.


4.2 재사용 가능한 유틸리티 함수 추출

복잡한 로직을 직접 컴포넌트나 훅 내부에서 작성하다 보면 테스트가 어렵고 사이드이펙트를 유발하기 쉽습니다. 그래서 자주 사용되는 반복 로직을 순수 유틸 함수로 추출했습니다.

변경 전

const existingItem = cart.find((item) => item.product.id === product.id);

변경 후

// utils/cart.ts
export const findCartItemByProductId = (cart: CartItem[], productId: string): CartItem | undefined => {
  return cart.find((item) => item.product.id === productId);
};

유틸리티 함수로 추출함으로써 동일한 로직을 여러 곳에서 일관되게 사용할 수 있고, 향후 변경이 필요할 경우 한 곳만 수정하면 되므로 유지보수성이 향상될거라 예상됩니다.


5. 상태 지속을 위한 로컬 스토리지훅 활용

사용자 경험을 향상시키기 위해 장바구니 상태와 쿠폰 선택 정보를 로컬 스토리지에 저장하는 기능을 도입했습니다. 이를 위해 재사용 가능한 useLocalStorage 커스텀 훅을 만들고, 상태 관리를 담당하는 훅 내부에서 활용하도록 구성했습니다.

// hooks/useLocalStorage.ts
function useLocalStorage<T>(key: string, initialValue: T) {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      return initialValue;
    }
  });

  useEffect(() => {
    try {
      localStorage.setItem(key, JSON.stringify(storedValue));
    } catch (error) {
      console.log(error);
    }
  }, [key, storedValue]);

  return [storedValue, setStoredValue] as const;
}

이 훅을 통해 로컬 스토리지와 상태 관리를 연결할 수 있습니다.

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

  const addToCart = (item: CartItem) => {
    setCart((prev) => [...prev, item]);
  };

  // ...

  return {
    cart,
    addToCart,
    selectedCoupon,
    setSelectedCoupon,
    // ....
  };
};

이 로직으로 인해 페이지를 새로 고치거나 브라우저를 닫았다가 다시 열어도 장바구니와 쿠폰 정보가 유지되도록 해줍니다!


6. 공통 UI 컴포넌트 추출

재사용성과 유지보수성을 높이기 위해 프로젝트 전반에서 반복적으로 사용되는 UI 요소들을 식별하여 공통 컴포넌트로 분리했습니다.

// components/ui/Select.tsx
export default function Select<T>({
  options,
  value = '',
  onChange,
  className = 'w-full p-2 border rounded',
  placeholder,
  disabled = false,
  required = false,
  name,
  id,
}: SelectProps<T>) {
  const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    const selectedValue = e.target.value;
    const selectedOption = options.find(option => option.value === selectedValue);
    if (onChange) onChange(e, selectedOption?.data);
  };

  return (
    <select
      id={id}
      name={name}
      value={value}
      onChange={handleChange}
      className={className}
      disabled={disabled}
      required={required}
    >
      {placeholder && <option value="">{placeholder}</option>}
      {options.map(option => (
        <option key={option.value} value={option.value}>
          {option.label}
        </option>
      ))}
    </select>
  );
}

이처럼 공통 컴포넌트를 구성해두면 추후 디자인 수정이나 기능 확장 시에도 하나의 컴포넌트만 수정하면 되므로 생산성과 안정성이 모두 높아질 수 있을거라 생각합니다.

7. 테스트코드 작성

엔티티 관련 유틸리티 함수들을 테스트하면서 느낀 점은 처음에 비즈니스 로직을 순수 함수로 잘 분리해두었기 때문에 테스트 코드 작성이 굉장히 수월했다는 것입니다!

예를 들어, getAppliedDiscount, getRemainingStock, findCartItemByProductId와 같은 함수들은 모두 입력값만으로 동작하고, 외부 상태에 의존하지 않습니다. 그래서 테스트 케이스를 작성할 때 별다른 설정 없이 간단히 데이터를 제공하고 예상 결과만 체크하면 되었습니다.

describe("getAppliedDiscount", () => {
  const testProduct: Product = {
    id: "1",
    name: "Test Product",
    price: 100,
    stock: 10,
    discounts: [
      { quantity: 2, rate: 0.1 },
      { quantity: 5, rate: 0.2 },
    ],
  };

  test("할인이 적용되지 않으면 0을 반환해야 합니다", () => {
    const item: CartItem = { product: testProduct, quantity: 1 };
    expect(cartUtils.getAppliedDiscount(item)).toBe(0);
  });

  test("수량에 맞는 할인을 반환해야 합니다", () => {
    const item1: CartItem = { product: testProduct, quantity: 2 };
    expect(cartUtils.getAppliedDiscount(item1)).toBe(0.1);

    const item2: CartItem = { product: testProduct, quantity: 5 };
    expect(cartUtils.getAppliedDiscount(item2)).toBe(0.2);
  });

  test("수량이 모든 할인 기준을 충족하면 가장 높은 할인율을 반환해야 합니다", () => {
    const item: CartItem = { product: testProduct, quantity: 10 };
    expect(cartUtils.getAppliedDiscount(item)).toBe(0.2);
  });
});

위 코드에서처럼, getAppliedDiscount 함수는 CartItem 객체와 Product 객체만을 사용하여 할인 금액을 계산합니다. 이 함수는 외부 환경에 영향을 받지 않고, 제공된 데이터만으로 결과를 도출해서 테스트도 단순히 "이 입력이 들어가면 이렇게 나와야 한다"는 방식으로 작성할 수 있었습니다.

🌊 회고

이번 과제는 엔티티별로 순수 함수와 액션 함수를 분리하는 과정에서 많이 고생했습니다. 처음에는 비즈니스 로직을 어떻게 분리해야 할지 감이 잘 안 오고, 각 엔티티별로 적절히 함수들을 나누는 게 쉽지 않았습니다.

하지만 순수 함수로 작성하다 보니 테스트 코드를 잘 모름에도 시나리오 별로 테스트코드를 작성하는게 훨씬 수월해졌고, 로직 검증도 간단하게 할 수 있었던 것 같아요.

정말 고민하면서 진행하느라 시간이 매우 많이 소모되었는데 현업에서도 과연 이런식으로 구현 및 리팩토링을 할 수 있을지 고민됩니다😱 이건 익숙해지는 방법 밖에는 없겠죠..?ㅎ

👾 리뷰받고 싶은 내용

1️⃣ 폴더 구조에 대한 고민

이번 과제에서 FSD 방식으로 폴더 구조를 적용하려다 시도해 보니 몇 가지 불편한 점이 있었습니다. 예를 들어, 공용 컴포넌트유틸리티 함수들을 관리하는 방식에서 일부 기능이 중복되는 문제나, 공유되는 타입 정의를 어디에 두어야 할지에 대한 고민이 생겼다. 또한, 각 기능을 독립적으로 관리하다 보면, 기능 간 의존성 관리공통 로직 처리에 있어 코드가 분산되면서 오히려 더 복잡해지는 느낌을 받기도 해서 결국 평소 자주 사용하던 폴더 구조에 엔티티별로 폴더링을 더해주는 방식으로 진행을 했습니다.

실제 현업에서 FSD 방식의 폴더 구조를 많이 사용하고 있나요? 그리고 이 구조의 장점과 단점은 무엇인지, 그리고 실제 개발 현장에서 어떤 방식으로 이 구조를 관리하고 활용하는지에 대해 궁금합니다!

2️⃣ useCart에서 쿠폰 관련 로직을 분리하는 것에 대한 고민
처음 useCart 훅에서 쿠폰 관련 로직을 분리하려고 했는데 분리하는 것이 맞나 싶은 고민들이 있었는데 고민을 끝내지 못해 결국 useCart 훅안에 뒀습니다...!

export const useCart = (): CartContextType => {
  const [cart, setCart] = useLocalStorage<CartItem[]>("cart", [])
  const [selectedCoupon, setSelectedCoupon] = useState<Coupon | null>(null)

  // 장바구니 관련 함수들...
  
  // TODO: 쿠폰 관련 로직 분리
  const applyCoupon = (coupon: Coupon) => {
    setSelectedCoupon(coupon)
  }

  const calculateTotal = () => {
    return calculateCartTotal(cart, selectedCoupon)
  }

  return {
    cart,
    // 다른 반환값들...
    applyCoupon,
    calculateTotal,
    selectedCoupon,
  }
}

위 코드를 보면 applyCoupon 함수는 쿠폰 상태를 변경하는 함수이므로 별도의 커스텀 훅으로 분리하는 것이 맞다고 생각합니다. 하지만 문제는 calculateTotal 함수입니다. 이 함수는 장바구니(cart)와 쿠폰(selectedCoupon) 두 가지 상태에 의존하고 있어서 단순 계산 로직이므로 유틸 함수의 성격을 갖는다고 판단할 수 있으나 상태에 의존하고 있어 완전한 유틸 함수로 분리하기 어렵다고 판단했습니다.

제가 생각한 가능한 해결책들:

  1. 쿠폰 로직만 분리하고 calculateTotal은 useCart에 남겨두기
  • 장점: 구현이 간단하고 직관적입니다.
  • 단점: useCart가 여전히 쿠폰 상태를 사용해야 합니다.
  1. 두 상태를 모두 접근할 수 있는 상위 훅 만들기
  • 장점: 관심사 분리가 명확할 것 같아요.
  • 단점: 코드 복잡성이 증가할 것 같아요.
  1. calculateTotal을 컴포넌트 레벨로 올리기..?
  • 장점: Hook들은 순수하게 상태 관리만 담당합니다.
  • 단점: 컴포넌트 로직이 복잡해질 수 있을 것 같습니다.

커스텀 훅은 "도메인 데이터나 상태를 다루는 로직"을 관리하고, 유틸 함수는 "상태나 사이드 이펙트 없이 동작하는 순수 함수"라는 기준을 지키면서도 코드의 유지보수성과 재사용성을 높일 수 있는 최선의 구조가 무엇일까요?

유용태 and others added 20 commits April 19, 2025 15:42
@junman95
Copy link
Copy Markdown

이틀만에 한게 맞나 싶을정도로 잘하시네요,,,
왜 BP를 쓸어담고 계신지 알아버렸습니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants