JHeeJinDev 님의 상세페이지[6팀 장희진] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍

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

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

과제 셀프회고

과제를 시작하기 전에는 디자인 패턴에 대해 대략적으로만 알고 있었고, 그 중요성이나 구체적인 적용 방법에 대해서는 깊이 있게 이해하지 못했습니다. 특히, 내 코드가 어떤 패턴을 따르고 있는지, 왜 그런 설계를 해야 하는지에 대한 기준이 없었습니다. (사실 실무에서 배운 방식을 그냥 따라 했던 것 같습니다...)

하지만 이번에는 디자인 패턴에 대해 직접 찾아보고, 최대한 그 원칙에 맞춰 구현해보자는 마음가짐으로 시작했습니다.

1. basic 과제 이번 과제에서는 Jotai 같은 전역 상태 관리 도구 없이, 상위 컴포넌트에서 상태를 모두 관리하고 하위 컴포넌트로 전달하는 구조로 구현했습니다. 이 과정에서 Props Drilling을 자연스럽게 경험할 수 있었고, 상태 관리의 어려움도 체감할 수 있었습니다.

App 컴포넌트는 전체 상태와 로직을 책임지는 Container 역할을 했고, 하위 컴포넌트들은 주어진 props만으로 동작하는 Presenter처럼 구성했습니다.

특히 여러 핸들러 함수들을 productHandlers, cartHandlers, couponHandlers 등으로 묶어서 넘기는 방식은 Facade Pattern 느껴졌고, 실제로 컴포넌트 간의 인터페이스가 훨씬 단순해지는 장점이 있었습니다.

또한, useCart, useProducts, useCoupons 같은 커스텀 훅을 활용해 상태 관리와 비즈니스 로직을 분리했습니다. 이 구조 덕분에 App 컴포넌트는 훨씬 읽기 쉬워졌고, 로직의 재사용성도 높일 수 있었습니다.

전체적으로는 관심사 분리와 상태 흐름에 대한 감각을 익히기에 좋은 구조였고, 동시에 전역 상태가 없을 때의 한계도 자연스럽게 체감할 수 있었습니다.


2. advanced 과제 (Jotai 활용) 이번 과제에서는 첫 번째 과제에서 경험한 Props Drilling의 한계와 복잡한 상태 관리 문제를 해결하고 한번도 사용해보지 못한 Jotai도 공부해 볼겸 jotai를 도입하였습니다

디자인 패턴 적용 후 달라진 점

컨테이너-프레젠터 패턴: 이전에는 App 컴포넌트가 컨테이너 역할을 했지만, 이제 그 역할이 useAdminCoupons나 useProducts와 같은 커스텀 훅으로 옮겨갔습니다. 이 훅들이 Jotai의 상태와 로직을 모두 책임지게 되면서, AdminPage나 CartSection 같은 컴포넌트들은 그저 훅이 주는 데이터와 함수를 받아서 화면을 보여주는 프레젠터가 되었습니다.

옵저버 패턴: Jotai의 atoms를 활용하면서 옵저버 패턴의 개념을 자연스럽게 적용할 수 있었습니다. atoms는 **상태 변화를 알리는 주제(Subject)**가 되고, useAtom을 사용하는 모든 컴포넌트나 훅은 그 변화를 지켜보는 **관찰자(Observer)**가 되었습니다.

파사드 패턴: 첫 과제에서 productHandlers처럼 함수들을 묶어 넘겼던 역할은, 이제 store/actions 폴더의 액션 아톰들이 대신하게 되었습니다. addProductAtom, removeCouponAtom 같은 액션들이 복잡한 로직을 캡슐화하고, 커스텀 훅을 통해 파사드가 되었습니다.

전체적인 소감 Jotai를 사용하면서 상태 관리와 컴포넌트 간의 책임 분리가 훨씬 명확해지는 것을 느꼈습니다. 첫 과제에서는 힘들었던 Props Drilling이 사라졌고, 코드도 훨씬 읽기 쉬워졌습니다.


트러블 슈팅 기록

Jotai 테스트 환경 구축 및 상태 오염 문제 해결 문제 인식: 전역 상태 오염과 불안정한 DOM 선택자 심화 과제를 진행하며 Jotai를 사용한 컴포넌트의 테스트 코드를 작성할 때, 제공된 테스트 코드가 예상치 못한 에러와 함께 실패하는 문제가 발생했습니다. 이러한 문제의 원인을 파악하는 데 어려움을 겪었고, 6팀 팀원들과 함께 문제를 찾아보며 많은 도움을 받을 수 있었습니다. 동료와 논의하는 과정에서 Jotai의 전역 스토어 생명주기에 대한 이해를 깊게 할 수 있었고, 해결책을 찾는 데 큰 실마리를 얻었습니다.

원인은 React Testing Library는 각 테스트마다 컴포넌트를 다시 렌더링하지만, Jotai의 전역 스토어는 자동으로 초기화되지 않습니다. 이 때문에 이전 테스트에서 변경된 상태가 다음 테스트에 영향을 주어 테스트 간의 독립성이 깨지는 문제가 있었습니다.

해결 과정: Jotai 테스트 환경 최적화 이 문제를 해결하기 위해 가장 먼저 테스트 간의 상태 독립성을 확보하는 데 집중했습니다. 기존에는 Jotai의 를 한 번만 렌더링하는 방식이었지만, 각 테스트마다 새로운 와 스토어를 생성하는 전략을 채택했습니다.

이를 위해 Jotai의 useHydrateAtoms 훅을 활용하여 테스트 시작 전에 Atom의 초기 상태를 원하는 값으로 미리 설정하는 헬퍼 함수를 구현했습니다.

  1. 초기값 주입을 위한 헬퍼 컴포넌트 구현: initialValues라는 prop을 받아 useHydrateAtoms를 호출하는 컴포넌트를 만들었습니다. 이 컴포넌트는 모든 자식 컴포넌트가 렌더링되기 전에 Atom에 초기 상태를 주입하는 역할을 합니다.
const HydrateAtoms = ({ initialValues, children }) => {
  useHydrateAtoms(initialValues);
  return children;
};
  1. 테스트 렌더링 헬퍼 함수 생성: render 함수를 래핑하는 renderApp 함수를 만들었습니다. 이 함수는 매 테스트마다 새로운 스토어를 createStore()로 생성하고, 로 App 컴포넌트를 감싸도록 했습니다.
const renderApp = (initialValues = []) => {
  const store = createStore();
  return render(
    <Provider store={store}>
      <HydrateAtoms initialValues={initialValues}>
        <App />
      </HydrateAtoms>
    </Provider>
  );
};

이제 각 테스트는 renderApp 헬퍼 함수를 통해 완전히 독립된 상태에서 시작할 수 있게 되었고, initialValues를 전달하여 테스트 시나리오에 맞는 초기 상태를 유연하게 설정할 수 있게 되었습니다.

배운 점 이 과정을 통해 Jotai와 같은 전역 상태 관리 라이브러리를 사용할 때 테스트 환경을 어떻게 구성해야 하는지 깊이 있게 이해하게 되었습니다. 단순한 컴포넌트 렌더링을 넘어, 전역 상태의 생명주기를 테스트 환경에 맞게 관리하는 것도 중요하다고 느꼇습니다.(사실 1주차때도 겪었던 문제..)

또한, 페어 코딩과 팀의 중요성도 함께 느꼈습니다. 혼자서는 해결하기 어려웠던 문제를 동료와 함께 논의하고 해결책을 찾아가는 과정 자체가 매우 의미 있는 학습 경험이었습니다.

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

제가 가장 신경 쓴 부분은 디자인 패턴의 의도적인 적용함수의 순수성 확보, 그리고 유연한 설계였습니다.

과제를 시작하기 전, 저는 디자인 패턴에 대해 막연하게 알고 있었고, 그 중요성이나 구체적인 적용 방법에는 익숙하지 않았습니다. 하지만 이번 기회를 통해 **'기능에 따른 단순한 파일 정리'**를 넘어, 의도적으로 디자인 패턴을 적용하고 구현하는 것에 집중했습니다.

  1. 디자인 패턴의 의도적인 적용
  • Props Drilling 경험: 첫 번째 과제에서 Jotai 없이 상태를 관리하며 Props Drilling의 비효율성을 경험했습니다. 이를 통해 상위 컴포넌트는 컨테이너 역할을, 하위 컴포넌트는 프레젠터 역할을 충실히 하도록 설계하는 것의 중요성을 깨달았습니다.

  • Jotai를 활용한 패턴 구체화: Jotai를 도입한 심화 과제에서는 이전 과제의 한계를 해결하며 디자인 패턴을 더욱 명확하게 적용했습니다. App 컴포넌트의 컨테이너 역할을 커스텀 훅으로 분리하고, Jotai의 atoms를 활용하여 옵저버 패턴을 적용했습니다. 또한, 여러 핸들러 함수를 하나로 묶어 전달하는 방식을 통해 파사드 패턴을 구현하여 컴포넌트의 인터페이스를 단순화했습니다.

  1. 함수의 순수성 확보 디자인 패턴과 함께 함수의 순수성에 대해서도 깊이 고민했습니다. 순수 함수는 같은 입력에 대해 항상 같은 출력을 반환하고, 외부 상태를 변경하지 않는 부수 효과(side effect)가 없어야 합니다.
  • 초기 코드의 문제점: 초기에 작성한 calculateItemTotal 같은 함수는 외부의 전역 상태를 직접 변경하지 않았지만, getMaxApplicableDiscount와 같은 다른 함수에 암묵적으로 의존하고 있었습니다. 이는 완벽한 순수 함수라고 볼 수 없었고, 예측 가능성을 떨어뜨리는 원인이었습니다.
export const calculateItemTotal = (
  item: CartItem,
  cart: CartItem[]
): number => {
  const { price } = item.product;
  const { quantity } = item;
  const discount = getMaxApplicableDiscount(item, cart);
  const finalPrice = price * quantity * (1 - discount);

  return Math.round(finalPrice);
};
  • 리팩토링을 통한 개선: 이 문제를 해결하기 위해 함수형 프로그래밍 원칙을 적용하여 함수를 리팩토링했습니다. 의존하는 함수들을 직접 호출하는 대신, 인자로 명시적으로 받도록 코드를 수정했습니다. 이를 통해 함수는 외부의 어떤 것도 참조하지 않는 '완전한 순수 함수'가 되었습니다.
// getMaxApplicableDiscount 함수를 외부에서 주입받기 위한 타입 정의
type DiscountCalculator = (item: CartItem, cart: CartItem[]) => number;

export const calculateItemTotal = (
  item: CartItem,
  cart: CartItem[],
  getMaxDiscount: DiscountCalculator // 계산 함수를 인자로 받음
): number => {
  const { price } = item.product;
  const { quantity } = item;
  const discount = getMaxDiscount(item, cart);
  const finalPrice = price * quantity * (1 - discount);

  return Math.round(finalPrice);
};
  1. 라이브러리 전환에 대비한 설계 준일코치님 멘토링을 청강하던 중, "Jotai로 상태 관리 라이브러리를 구성했는데, 갑자기 Zustand로 바꿔야 하는 상황"과 같은 요구사항을 기반으로 생각해보라는 조언을 들었습니다. 이 조언은 단순히 기능을 구현하는 것을 넘어, 미래의 변화에 대비하는 유연한 코드 설계의 중요성을 깨닫게 해주었습니다.
  • 기존 코드
// useAdminProducts.ts (훅 내부에서 Jotai와 로컬 상태를 모두 관리)
import { useState } from "react";
import { useAtom } from "jotai";
import { productsAtom, ... } from "store/atoms";
import { addProductAtom, ... } from "store/actions";

export const useAdminProducts = () => {
  const [products] = useAtom(productsAtom);
  const [, addProduct] = useAtom(addProductAtom);
  const [showProductForm, setShowProductForm] = useState(false);
  // ... 기타 로직과 핸들러 함수들
  
  return {
    products,
    showProductForm,
    // ... 모든 상태와 핸들러 함수 반환
  };
};
  • 리팩토링 코드
// AdminProductSection.tsx (훅만 사용)
import { useAdminProducts } from "./hooks/useAdminProducts";

export const AdminProductSection = () => {
  const {
    products,
    showProductForm,
    editingProduct,
    // ... 모든 상태와 핸들러 함수를 훅에서 받아옴
  } = useAdminProducts();
  
  return (
    // ... 훅에서 받은 값들로 UI만 구성
  );
};

이 경험을 통해 유지보수와 확장성에 강한 라이브러리 독립적인 설계의 중요성을 체감했습니다.

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

과제를 다시 해본다면 더 잘할 수 있었겠다고 느낀 점은, 프로젝트 구조와 코드 설계 부분입니다. 다른 분들과 코드도 보고 이야기를 나누면서 느낀 점은, 기능을 분류하고 구조화하는 방법이 생각보다 다양하다는 것이었습니다.

예를 들어, 저는 기존에 단순히 디렉터리만 나눴지만, 다른 분들은 features, shared, entities, pages 등으로 세분화해 도메인 중심, 재사용 중심으로 잘 나누고 있었습니다. 이런 구조를 미리 알았다면 좀 더 깔끔하고 확장성 있는 설계를 할 수 있었을 텐데 하는 아쉬움이 남았습니다.

또, 상태 관리도 처음에는 단순하게 관리했지만, Jotai에서 Zustand로 바꾸면서 느꼈던 것처럼, 상태의 위치나 역할에 따라 어디에 위치시킬지 고민하는 것도 중요하다는 걸 느꼈습니다.

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

  1. 이번 과제에서 컨테이너-프레젠터, 파사드, 옵저버 패턴을 의도적으로 적용해보았습니다. 제가 이해하고 적용한 방식이 실제 의도와 잘 맞는지, 개선할 점은 없는지 궁금합니다!

  2. 함수에서 의존 함수를 인자로 주입하는 방식이 한편으로는 오히려 복잡도를 높이는 건 아닌지 우려되기도 하는데요. 순수성을 확보하는 데 효과적인 패턴인지 궁금합니다!

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

과제 피드백

안녕하세요 희진! 수고했습니다. 이번 과제는 React 코드의 구조적 분리와 함수형 프로그래밍 원칙을 이해하고 적용해보는 것이 핵심이었습니다. 특히 엔티티를 다루는 코드와 UI를 다루는 코드의 분리, 순수함수의 활용 등 현대적인 프론트엔드 개발의 중요한 패턴들을 경험했기를 바래요 :)

회고를 보니 디자인 패턴을 의도적으로 적용하려는 노력이 인상적입니다. 특히 Props Drilling을 경험하고 이를 개선하기 위해 컨테이너-프레젠터 패턴과 Jotai를 활용한 상태 관리 구조를 구현하신 과정이 정말 좋은 학습 경험이었을 것 같아요. 옵저버 패턴과 파사드 패턴을 자연스럽게 적용하신 부분도 훌륭합니다.

코드를 살펴보니 상태와 로직을 커스텀 훅으로 캡슐화하여 컴포넌트를 깔끔하게 유지하신 부분이 돋보입니다. 특히 useAdminProducts, useCart 같은 훅을 통해 비즈니스 로직을 분리하고, 컴포넌트는 UI 렌더링에만 집중하도록 한 점은 매우 좋은 접근법입니다.

순수함수의 의존성 주입에 대한 고민도 흥미롭네요. calculateItemTotal 함수에 getMaxDiscount를 인자로 주입하여 순수성을 확보하려고 했던 시도가 인상적이었습니다. 사실 순수함수 + 순수함수 = 순수함수이기에 내부적으로 순수함수를 사용한다고 해도 의존성에 대한 문제는 발생하지 않아요. 다만 지금처럼 외부의존을 밖으로 빼내기도 용이하고 실제로 다른 전략을 써야한다면 OCP방식으로 필요한 부분만 빼내기도 용이한 방식이 되겠죠. 해당 과제의 경우 getMaxApplicableDiscount(item)만 되어 있는 경우에는 문제가 되는 코드였지만 이미 getMaxApplicableDiscount(item, cart) 형태로 순수하게 만들어두게 되면서 암묵적 문제는 해결이 된 상태랍니다. getMaxDiscount의 경우 전략적으로 선택을 해야하는 상황이 아니라면 내부에 두면 되고 필요할때 빼내어도 좋아요! 함수형 프로그래밍의 장점과 확장에 대한 체험을 제대로 했다고 생각이 드네요. 잘했씁니다.

1 이번 과제에서 컨테이너-프레젠터, 파사드, 옵저버 패턴을 의도적으로 적용해보았습니다. 제가 이해하고 적용한 방식이 실제 의도와 잘 맞는지, 개선할 점은 없는지 궁금합니다! => 잘했습니다. 디자인 패턴은 어떻게 하거나 적용하는게 아니라 말 그대로 패턴인거죠. 내가 일반적으로 더 좋은 코드의 형태로 구조를 바꾸게 되는 여러가지 패턴들이 있는게 하고보니 우리가 하고 있던 패턴들이었던거죠. 그런 측면에서 컨테이너-프레젠터, 파사드, 옵저버 패턴을 특정 구현 방법이 아닌 패턴이라는 측면에서 아주 잘 활용했다고 생각합니다.

2 함수에서 의존 함수를 인자로 주입하는 방식이 한편으로는 오히려 복잡도를 높이는 건 아닌지 우려되기도 하는데요. 순수성을 확보하는 데 효과적인 패턴인지 궁금합니다! => 위에서 설명한대로 순수함수 + 순수함수 = 순수함수이기 때문에 인자로 주입하지 않아도 됩니다. 인자로 주입을 한다는건 외부에서 전략을 바꿀 수 있도록 위임하는 것이죠. 마치 Array.map Array.filter와 같은 식으로요.

=> 필요한 경우에는 어렵지 않게 외부의존을 담을 수 있다는게 장점이고 필요없다면 그냥 사용해도 무방합니다. 무조건적인 적용이 아니라 점진적확장의 형태로 처음에는 가장 단순한 형태로 시작해서 필요할때 필요한 만큼 복잡하게 만든다라는 식으로 적용하면 됩니다.

아니면 이렇게도 할수 있습니다. 외부에서도 받을 수 있게 만들면서 기본 값은 적용하는 방식이죠. 이렇게 하면 순수함과 외부의존성 전략패턴과 간결함 모두 얻을 수 있습니다.

export const calculateItemTotal = ( item: CartItem, cart: CartItem[], getMaxDiscount = defaultGetMaxDiscount // 기본 구현 제공 ): number => { const discount = getMaxDiscount(item, cart); return Math.round(item.product.price * item.quantity * (1 - discount)); };

그렇다고 이 방법이 만능이니 다 이렇게 해야되냐? 하면 필요없다면 이 역시도 오버엔지니어링이죠. 필요할때 필요한만큼!

아주 아주 잘했습니다. 6주차 과제에서 지금의 고민들을 많이 적용해보길 바래요!!