k-sang-soo 님의 상세페이지[8팀 김상수] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍 🦍

과제 링크

https://k-sang-soo.github.io/front_6th_chapter2-2/

과제의 핵심취지

  • 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과 라이브러리 훅과의 계층의 성격이 다르다는 것을 이해하고 적용했는가?

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

과제 셀프회고

지난 시간 동안 너무 향해에 몰두하고 있어 다른 쪽에도 시간을 분해해야 될 것 같아서 이력서에 많은 시간을 투자하게 됐다. 이번에는 또 이력서에 너무 몰두하느냐고 시간 분배를 잘못했다. 이번 과제가 하다보니 실무에서 내가 고민했던 부분들을 해소 할 수 있는 기회가 많은 과제였던 것 같은데 많은 시간을 쓰지 못해 진짜 너무 아쉽다... 왜 하필 또 향해에 몰두하는 시간을 줄이니까 이런 꿀 같은 과제가!!!!!😡

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

각 계층이 자신의 역할에 집중할 수 있도록 했습니다. 장바구니에 상품을 담는 로직을 예시로 들면

비즈니스 로직 계층

// ✅ models/cart.ts - 순수한 도메인 로직만 담당
export const addItemToCart = (cart: CartItem[], product: Product): CartItem[] => {
  const existingItem = cart.find(item => item.product.id === product.id);
  
  if (existingItem) {
    return cart.map(item =>
      item.product.id === product.id
        ? { ...item, quantity: item.quantity + 1 }
        : item
    );
  }
  
  return [...cart, { product, quantity: 1 }];
};

애플리케이션 계층

// ✅ hooks/useCart.ts - 상태 관리만 담당
export const useCart = () => {
  const [cart, setCart] = useState<CartItem[]>([]);
  
  const addToCart = useCallback((product: Product): OperationResult => {
    try {
      const newCart = addItemToCart(cart, product); // 📌 비즈니스 로직 위임
      setCart(newCart);
      return { success: { message: '상품이 장바구니에 추가되었습니다' } };
    } catch (error) {
      return { error: { message: '장바구니 추가 실패' } };
    }
  }, [cart]);
  
  return { cart, addToCart };
};
// ✅ components/CartPage.tsx - UI 표현과 사용자 상호작용 담당
const CartPage = () => {
  const { cart, addToCart } = useCart();
  const { addNotification, showNotifyFromResult } = useNotifications();
  
  const handleAddToCart = useCallback((product: Product) => {
    const result = addToCart(product); // 📌 상태 관리 계층 호출
    showNotifyFromResult(result);      // 📌 UI 표현 로직 실행
  }, [addToCart, showNotifyFromResult]);
  
  return (
    <div>
         <button
          onClick={() => handleAddToCart(product)}
          disabled={remainingStock <= 0}
          className={`w-full py-2 px-4 rounded-md font-medium transition-colors ${
            remainingStock <= 0
              ? 'bg-gray-100 text-gray-400 cursor-not-allowed'
              : 'bg-gray-900 text-white hover:bg-gray-800'
          }`}
        >
          {remainingStock <= 0 ? '품절' : '장바구니 담기'}
        </button>
    </div>
  );
};

useCart는 상태를 관리하는 hook 이라고 생각했고, onShowNotification 같은 UI 로직을 그 안에서 다루고 싶지 않았습니다. 그래서 책임을 분리하기 위해, 사용처인 CartPage 에서 실행될 때만 onShowNotification으로 UI 로직을 처리하도록 했습니다. 지금은 필요 없지만, 나중에 실패/성공 여부나 타입에 따라 DOM을 조작하거나 외부 의존이 필요한 상황이 생기면, useCart 내부에서 관리하기가 더 어려워질 수 있기 때문에 이렇게 분리했습니다.

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

엔티티 컴포넌트, UI 컴포넌트 분리하는거 너무 재미있을 것 같았고, 하다 보면 궁금한 점도 많이 생겼을 텐데 시도를 못한게 아쉽습니다. 관라자 페이지 쪽도 거의 진행하지 못한 것도 아쉽습니다. 앞으로는 시간 분배를 더 잘해야될 것 같습니다.

함수를 어떻게 분리할지, 이 로직이 정말 이 책임 안에 있어야 하는지 등을 고민하는데 시간이 너무 오래 걸리는데, 이런 저런 고민들을 과제할 때 마다 너무 오랜 시간을 사용하다보니 정작 구현에 쓸 시간을 많이 놓치는 것 같아 아쉽네요

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

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

  1. 표현식, 선언식 함수

Hook, 컴포넌트, 페이지, 유틸 함수를 만들 때 함수 표현식(const test = () =>)과 함수 선언식(function test() {})을 어떤 기준으로 나눠서 사용하시나요?


  1. 함수 네이밍 (handleAddCart vs addCart)

이벤트 핸들러 실행 함수는 handleAddCart 처럼 handle 을 붙이는 편인데, 과제 코드에서는 addCart 처럼 동사형으로 사용하더라구요 handle을 붙이는 습관이 안 좋은 걸까요?


  1. 목록 컴포넌트 설계 방식

아래처럼 ProductList 컴포넌트를 설계하는 여러 방식이 있을 때, 각각의 장단점과 선택 기준이 궁금합니다. 아직까지도 어떨 때 써야될지 기준이 헷갈리네요😢

<ProductList data={products} />

// Function as Children 패턴
<ProductList>
  {products.map(product => <ProductItem>{product.name}</ProductItem>)}
</ProductList>

// Render Props 패턴
<ProductList render={(products) => products.map(product => <ProductItem>{product.name}</ProductItem>} />


  1. ViewModel을 class로 관리하는 방식

상품 UI 렌더링 시 분기문이나 변환 로직이 많을 경우, 아래처럼 ProductViewModel 클래스로 캡슐화해서 사용하는 건 어떻게 생각하시나요?

export class ProductViewModel {
  id!: string;
  name!: string;
  description?: string;
  price!: number;
  stock!: number;
  isRecommended?: boolean;
  discounts: Discount[] = [];

  constructor(init?: Partial<ProductViewModel>) {
    Object.assign(this, init);
  }

  /** 추천 상품 여부 (BEST 뱃지) */
  get isBest(): boolean {
    return this.isRecommended === true;
  }

  /** 할인 존재 여부 */
  get hasDiscount(): boolean {
    return this.discounts.length > 0;
  }

  /** 가장 높은 할인률 (0.2 → 20) */
  get maxDiscountRate(): number {
    return this.hasDiscount
      ? Math.max(...this.discounts.map((d) => d.rate)) * 100
      : 0;
  }

  /** 첫 번째 할인 규칙 */
  get primaryDiscount(): Discount | null {
    return this.discounts[0] ?? null;
  }

  /** 첫 번째 할인 수량 기준 (10개 이상 시) */
  get primaryDiscountQuantity(): number | null {
    return this.primaryDiscount?.quantity ?? null;
  }

  /** 첫 번째 할인률 (0.1 → 10) */
  get primaryDiscountRate(): number | null {
    return this.primaryDiscount ? this.primaryDiscount.rate * 100 : null;
  }
}

jsx 에서 사용할 때

기존

{product.isRecommended && (
  <span>BEST</span>
)}
{product.discounts.length > 0 && (
  <span>~{Math.max(...product.discounts.map((d) => d.rate)) * 100}%</span>
)}

변경 후

{product.isBest && (
  <span>BEST</span>
)}
{product.hasDiscount && (
  <span>~{product.maxDiscountRate}%</span>
)}

네이밍만 봐도 의미가 명확해지고, 변환 로직도 한 곳에서 관리 가능하다는 장점이 있어보이는데 실무에서는 사용해본 적이 없어서 좋은 코드인지 잘 모르겠습니다!


  1. Header 분기 처리

페이지마다 Header에서의 분기 조건이 많아질 경우 어떻게 구조를 짜야될까요?

  • 하나의 Header 컴포넌트를 세세하게 분리해서 재사용하는 것이 좋을지,
  • 약간의 중복된 코드가 있더라도 페이지별로 Header를 별도 컴포넌트로 만들어 일부만 다르게 관리하는 것이 좋을지

  1. useCart에서 Toast 로직 처리 위치

useCart 내부에서 Toast 훅을 사용하는 대신, 호출하는 쪽에서 Toast를 실행하게 했습니다. 이러한 부분이 책임 분리라는 측면에서는 좋아 보이지만, 오히려 쓸데없이 분리해서 복잡도가 늘린 건 아닌지 고민됩니다


  1. 베럴 파일(Barrel File)에 대한 의견

베럴 파일 사용에 대해 긍정적인 의견과 부정적인 의견이 갈리는데, 주니어 입장에서 어떤 기준으로 판단하면 좋을까요? 코치님께서는 어떻게 사용하고 계신가요? 사용하고 계신다면 어떤 필요성에 의해서 사용하게 됐는지 궁금합니다!


  1. 과제와는 직접적인 관련은 없지만 궁금한 점이 있습니다! 예를 들어 아래 블로그처럼 라이브러리, 프레임워크, 언어 등등 성능 테스트 결과를 보면, “6배 차이” 이런 단어 들은 수치상으로는 꽤 커 보이는 결과로 보이긴 하는데, 실제로는 그게 얼마나 느린 건지, 체감상 어느 정도 영향을 주는 건지 잘 와닿지가 않습니다. 이런 걸 구분하기 위해서는 어떤 감각이나 지식 같은게 필요한걸까요?

https://pawmi.tistory.com/15

과제 피드백

ㅎㅎㅎ 아쉬움이 남는 주차셨군요 도현님 지금 모두가 함께 과제를 하는 시간에 함께 몰두하는 경험이 가장 이상적이지만, 시간이 없다면 혼자 다시 되짚어보면서 수강생들과 이야기를 나눠보면 되니 꼭 다시 해보는걸 목표로 하시죠. 아직 전반적으로 개선이 추가로 필요한 부분은 남아있는 것 같아요! 꼭 더 집중해서 해보는걸로 하죠~

질문 주신게 많아서 바로 답변을 해보겠습니다 ㅎㅎ

표현식, 선언식 함수

표현식과 선언식, 그리고 화살표 함수와 일반함수 표기법의 차이가 함께 있는데요. 관련해서 설명을 드리기보다는 https://codingeverybody.kr/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%ED%99%94%EC%82%B4%ED%91%9C-%ED%95%A8%EC%88%98%EC%9D%98-%EC%9D%B4%ED%95%B4%EC%99%80-%EC%82%AC%EC%9A%A9%EB%B2%95/ 이 글을 보시면 좋을것 같구요.

리액트 내부에서는 사실 크게 차이가 없을 수 있는데요. 크게 차이가 없는 경우에는 통일된 규칙만 갖고 작성하면 될 것 같아요.

함수 네이밍 (handleAddCart vs addCart)

개인적으로 네이밍을 할 때는 역할을 잘 드러내도록 이름을 짓는게 중요하다고 생각하는데, 핸들러 즉 특정 이벤트나 상황이 발생했을 때 그것을 처리하는 함수라고 볼 수 있다면 handle을 붙이는게 나쁜건 아닌것 같아요. 다만, 영어로 읽었을때나 문법상으로 아름다운 명칭은 아니기때문에 handleClickAddCartButton 같은 형태로 명확하게 어떤 핸들러인지 드러낸다면 괜춘지 않을까 아니면 addCart를 바로 쓰는 형태로 하는것도 나쁘지 않을것 같구요!

목록 컴포넌트 설계 방식

여기서 기준을 한번 제안 드려볼테니 나눠보시는 게 좋을것 같아요.

  1. 가독성이 어느게 좋은지? 재사용성이 좋은 컴포넌트 작성 방식은?
  2. 명시적으로 제어할 수 있는 패턴은 어떤건지? (만약 외부 컴포넌트였다면?)
  3. 확장성은 어떤 컴포넌트가 좋은지?
  4. 성능에 있어서 이슈가 발생할 수 있는 컴포넌트는?
  5. 제어의 역전(IoC)이 가능한지?
  6. 테스트가 용이한지?

일단은 요정도로 표를 만들어서 한번 비교해보시면 좋을것 같아요!

ViewModel을 class로 관리하는 방식

굉장히 많이 사용하는 방식이죠 ㅎㅎ 특히 BE측에서 많이 사용하는 방식인데 좋은 방식이라고 생각해요.

Header 분기 처리

합성 컴포넌트 패턴같이 디자인 패턴을 활용해보면 좋을것 같아요! 조건을 모두 프로퍼티로 전달받아 제어를 하다보면 가독성이 안좋아지고 조건이 너무 복잡해지는 문제가 발생하니까요! 공통적인 규칙을 따르지 않는다면 별도 컴포넌트로 만들어버려도 괜찮구요

useCart에서 Toast 로직 처리 위치

책임 분리 관점에서는 잘 하셨습니다! 다만, "누가 알아야 하는 정보?" 관점에서 고민을 하는게 좋을 것 같은데요. toast 표시 여부나 메세지가 상황별로 달라져야 한다면 호출을 하는 방향으로도 한번 고민해볼수는 있을것 같아요.

베럴 파일(Barrel File)에 대한 의견

음 저는 선호하지 않는 편인데요. 하지만, 종종 필요한 경우에는 아래 문서에 나온 기준처럼 문제가 발생할 여지는 없는지 얻을수 있는 이점은 뭔지 명확하게 하고 사용하는 편인것 같아요. 아마 다음주쯤에 FSD를 하면서 배럴 파일 같은것들을 사용하게 될텐데 이 때 와일드 카드 형태로 export를 하는게 아니라 지정을 하게 되면 인터페이스 관점에서 공개하는 정보를 명확하게 할 수 있다는 장점이 생기거든요.

https://github.com/yeonjuan/dev-blog/blob/master/JavaScript/speeding-up-the-javascript-ecosystem-the-barrel-file-debacle.md 요 글도 함께 읽어보면 좋겠네요!

과제와는 직접적인 관련은 없지만 궁금한 점이 있습니다!

~배 %보다는 실질적으로 시간이 얼마나 줄었는지 살펴보는건 필요한것 같아요. 사실 ms가 6ms -> 1ms되었으면 매우 빠르겠지만, 매우 적은 변화라 인간이 인지하기 힘들고 사실 제일 중요한건 라이브러리 광고하는 당사자들이 벤치마킹하는 방식이 자기들 유리한식으로 되어있어서 문제가 된적이 많았었거든요.

그럼에도 우리는 매우 작은 그 ms, 런타임에서 발생할 수 있는 일련의 작은 로딩들을 줄이는 그 기술들을 선택하기는 해야할거에요. 따로 감각이나 지식을 추구하기보다는 경쟁 라이브러리들, 그리고 내가 지금 사용하고 있는게 어느정도였는데 얼마나 빨라졌구나 어떻게 빨라졌고 어떻게 측정이 되었구나만 보면 괜찮지 않을까 싶어요!

고생하셨고 다음주도 화이팅입니다~