Skip to content

[6팀 장희진] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍#27

Open
JHeeJinDev wants to merge 66 commits into
hanghae-plus:mainfrom
JHeeJinDev:main
Open

[6팀 장희진] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍#27
JHeeJinDev wants to merge 66 commits into
hanghae-plus:mainfrom
JHeeJinDev:main

Conversation

@JHeeJinDev
Copy link
Copy Markdown

@JHeeJinDev JHeeJinDev commented Aug 4, 2025

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

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

- 상품, 장바구니, 쿠폰 등 도메인별 atom/action/selector 파일 분리
Copy link
Copy Markdown

@JunilHwang JunilHwang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요! 과제 잘 진행하셨고 설계/리팩토링에 많은 신경을 쓴 점이 인상적입니다. 아래 피드백은 PullRequestFiles(변경된 파일들)와 PullRequestBody(셀프 회고 등)를 바탕으로 작성한 PR 리뷰 형식의 종합/상세 피드백입니다. 장황한 설명보다는 실무에서 바로 적용 가능한 개선안과 코드 예시(AS-IS / TO-BE)를 중심으로 정리했습니다.

현재 코드는 GPT-4 계열로 분석해 작성한 자동화 리뷰입니다. 참고만 해주세요 🤖

요약(한눈에)

  • 장점: 컨테이너-프레젠터 패턴 적용, Jotai 도입으로 옵저버/파사드 개념 적용, 훅/컴포넌트 분리·모듈화 진행, 테스트 독립성(HydrateAtoms) 처리
  • 주의/개선 포인트: 상태 라이브러리(Jotai)에 대한 결합(직접 useAtom 호출)이 남아 있음 → 라이브러리 교체 시 수정범위가 넓어질 수 있음, .eslintrc/ESLint 설정 파일 형식 불일치, 일부 훅/액션이 store-구현 세부사항에 강하게 의존

목차

  1. 종합 피드백 (키워드, 셀프회고 피드백, PR 질문 답변)
  2. 상세 피드백 (개념 정의: 응집도/결합도, 문제 정의, AS-IS / TO-BE + 코드 예시)
  3. 권장 작업(우선순위) 및 추가 질문

  1. 종합 피드백

A. 전체 문제에 대한 키워드

  • 상태 라이브러리 결합도 (Jotai 직접 사용)
  • 훅/컴포넌트 분리(응집도는 좋아짐)
  • 서비스/액션 아톰(파사드) — 좋은 시도
  • 순수 유틸/엔티티 계산 함수 분리(우수)
  • 테스트 isolation (HydrateAtoms + createStore) — 모범사례
  • 도메인별 패키징 준비 필요성 (엔티티 vs UI vs lib)
  • eslint/prettier 설정 파일 포맷/구성 문제

B. PullRequestBody의 셀프회고 / “제가 제일 신경 쓴 부분”에 대한 피드백 (인사이트 중심)

  • 좋은 관찰: Props drilling의 경험 → 컨테이너-프레젠터 분리로 해결 시도. 이 과정에서 Hooks에 책임을 이동한 점은 유지보수 관점에서 매우 바람직합니다.
  • 좋은 인사이트: Jotai를 도입하며 옵저버/파사드 패턴을 자연스럽게 적용한 경험을 잘 정리하셨습니다.
  • 추가로 생각해볼 인사이트 질문들:
    1. 현재 훅들이 Jotai API를 직접 호출하고 있습니다. "훅을 통해 전역 상태를 숨긴다"는 목표와, "훅이 특정 상태 라이브러리에 의존한다"는 현실 사이의 trade-off를 어떻게 평가하나요?
    2. 파사드로서 action atoms를 만들 때, 액션의 '부수효과' (예: 알림/로깅/analytics) 제어는 어떻게 하시겠습니까? (onSuccess/onError 콜백 패턴 또는 이벤트 버스 사용 고려)
    3. 엔티티 레이어(순수함수, 타입 등)와 플랫폼 레이어(store/atom/impl)를 분리할 때 테스트와 문서화는 어떻게 관리할 계획인가요?

C. PR에서 요청하신 질문들에 대한 답변

  1. 패턴 적용(컨테이너-프레젠터, 파사드, 옵저버) — 잘 적용하셨습니다.

    • 컨테이너 역할을 커스텀 훅(useAdminProducts 등)으로 이동: 적절함. 단, 훅의 내부가 특정 store 라이브러리(Jotai)에 강결합되어 있는 점은 개선 포인트입니다(아래에 개선안 제시).
    • 파사드: store/actions 폴더의 action atoms는 복잡한 로직을 숨기고 있어 파사드 역할을 잘 수행합니다. 다만, "atom"이라는 구체적 구현명이 외부로 드러나지 않도록 훅에서 캡슐화하십시오.
    • 옵저버: Jotai가 제공하는 옵저버 성격을 잘 이용하셨습니다.
  2. 의존 함수를 인자로 주입(inject)하는 방식이 복잡도를 높이는가?

    • 결론: "의도적 순수성" 확보에는 매우 효과적입니다. 함수의 예측가능성·테스트 용이성이 크게 좋아집니다.
    • 단점: 호출부가 복잡해질 수 있으므로 실무에서는 다음 절충안을 권합니다:
      • 기본 구현을 내부에서 제공하되 optional 인자로 외부 주입 허용 (default param).
      • 또는, 상위 레벨에서 "플러그형"으로 서비스(또는 store adapter)를 주입하는 방식 사용(예: useCart({ discountCalculator })).

예시:

  • 완전 주입 방식(현재 방식): calculateItemTotal(item, cart, getMaxDiscount)
  • 절충 TO-BE: calculateItemTotal(item, cart, getMaxDiscount = defaultGetMaxDiscount)

  1. 상세 피드백 (정의 → 문제 → AS-IS → TO-BE)

먼저 개념 정의 (요청하신 대로)

  • 응집도(cohesion)
    • 정의(정의된 규칙에 맞춰): 한 모듈(파일/훅/컴포넌트)이 얼마나 하나의 책임(또는 목적)에 집중되어 있는가. 더 구체적으로는:
      1. 변경에 대한 동선(수정/추가/삭제)이 얼마나 짧은가 — 한 변경사항을 해결하기 위해 터치해야 하는 파일과 라인이 적을수록 응집도가 높음.
      2. 라이브러리로 추출할 때 ‘매끄럽게 떼어낼 수 있는가’ — 외부 의존성이 적고 내부 API로 캡슐화되어 있으면 응집도가 높음.
  • 결합도(coupling)
    • 정의: 모듈 간 의존성 강도. 낮을수록 좋음. 인터페이스(함수 시그니처, 훅 반환값)로 결합도를 낮출 수 있음.
    • 좋은 예: useAddProduct({ onSuccess, onError })
    • 나쁜 예: useAddProduct(addNotification) — 구체적 이름에 의존

이제 파일 수준/기능별로 문제 정의와 개선안(AS-IS / TO-BE 코드)을 제시합니다.

피드백 항목 1 — 상태 라이브러리 결합도 (핵심)

  • 문제

    • 다수 훅(useCart, useAdminProducts, useAdminCoupons 등)과 컴포넌트가 직접 Jotai API(useAtom / useAtomValue / useSetAtom / atoms 파일)를 사용 중입니다.
    • 결과: Jotai를 다른 라이브러리로 바꿔야 할 때(예: Zustand, Redux, TanStack Query) 훅 내부와 action atoms를 모두 수정해야 함 → 변경 범위가 넓음.
  • AS-IS (예: useCart)

    // AS-IS: useCart 내부(요약)
    import { useAtomValue, useSetAtom } from "jotai";
    import { cartAtom } from "../../../store/atoms/cartAtoms";
    import { cartTotalAtom } from "../../../store/selectors/cartTotalSelector";
    import { handleAddToCartItem, handleRemoveCartItem /*...*/ } from "../../../store/actions/cartActions";
    
    export const useCart = () => {
      const cart = useAtomValue(cartAtom);
      const totals = useAtomValue(cartTotalAtom);
      const addCartItem = useSetAtom(handleAddToCartItem);
      // ...
      return { cart, totals, addCartItem, /*...*/ }
    }
  • 문제 발생 시나리오: "Jotai → Zustand" 전환

    • 모든 useAtomValue / useSetAtom 호출을 Zustand selector/hooks로 바꿔야 하며, store/actions 파일(현재는 action atoms)도 전부 재작성 필요.
  • TO-BE (권장) — store adapter + DI(의존성 주입) 패턴

    1. 먼저 CartService 인터페이스를 정의 (public API)
    // src/advanced/services/cartService.ts (인터페이스)
    export type CartService = {
      getCart(): CartItem[];
      getTotals(): { totalBeforeDiscount: number; totalAfterDiscount: number; };
      addItem(product: ProductWithUI): void;
      removeItem(productId: string): void;
      updateQuantity(productId: string, qty: number): void;
      applyCoupon(code: string): void;
      completeOrder(): void;
    };
    1. useCart는 이 인터페이스에만 의존 (구현 세부사항 숨기기)
    // useCart.ts (TO-BE)
    import { useMemo } from "react";
    import { useService } from "../../hooks/useService"; // context-based factory, 아래 설명
    
    export const useCart = () => {
      const cartService = useService<CartService>("cart"); // 서비스 레지스트리에서 가져옴
    
      // 구현에 따라 internal 상태를 subscription 형식으로 바인딩하거나,
      // cartService가 리액티브 API를 제공하면 그것을 사용
      // 예: cartService.subscribe(...) 또는 getXxx 훅 제공
      return {
        cart: cartService.getCart(),
        totals: cartService.getTotals(),
        addCartItem: cartService.addItem,
        removeCartItem: cartService.removeItem,
        updateCartQuantity: cartService.updateQuantity,
        applyCoupon: cartService.applyCoupon,
        completeOrder: cartService.completeOrder,
      };
    };
    1. Jotai 구현체 구현 (adapter)
    // services/jotaiCartAdapter.ts
    import { createStore, useAtomValue, useSetAtom } from "jotai";
    import { cartAtom, cartTotalAtom } from "../store/atoms/...";
    import { handleAddToCartItem, ... } from "../store/actions/cartActions";
    
    export const createJotaiCartService = () : CartService => {
      return {
        getCart: () => {/* read from atom or return snapshot */},
        getTotals: () => {/* read from selector atom */},
        addItem: (p) => {/* useSetAtom(handleAddToCartItem)(p) */},
        // ...
      }
    }
    1. Zustand 구현체도 동일한 CartService를 구현하면 훅(useCart)은 변경 불필요.
  • 장점

    • 라이브러리 전환 시 adapter만 교체하면 됨(변경 경로 짧음 → 응집도 향상).
    • 테스트 입장에서도 mock CartService 주입으로 단위 테스트가 쉬워짐.

피드백 항목 2 — 응집도 / 모듈화 (패키지화 준비)

  • 문제

    • 현재 구조는 domain 분리(components/* hooks/* store/*)로 잘 되어 있지만, 몇몇 훅/컴포넌트가 store 내부 구조(아톰 네이밍)를 직접 참조하고 있어 외부로 떼어내기 어렵습니다.
    • 예: components와 tests에서 직접 atom 경로(import ".../store/atoms/...")를 사용.
  • AS-IS 예시 (calculateItemTotal / cart service)

    // AS-IS: cart 계산함수가 getMaxApplicableDiscount에 의존(외부 참조)
    const calculateItemTotal = (item: CartItem, cart: CartItem[]) => {
      const discount = getMaxApplicableDiscount(item); // 내부 전역 참조
      return Math.round(item.product.price * item.quantity * (1 - discount));
    };
  • TO-BE (권장)

    • 모든 "엔티티 순수 함수"는 별도 패키지(src/lib/entities/cart/*)로 추출하고, 외부 의존성을 인자로 주입(또는 순수 함수만 유지).
    • 예:
      // src/lib/entities/cart/calc.ts
      export const calculateItemTotal = (item: CartItem, cart: CartItem[], getMaxDiscount: DiscountCalculator) =>
        Math.round(item.product.price * item.quantity * (1 - getMaxDiscount(item, cart)));
    • 패키지화 시: lib만 export하고 플랫폼(store) 의존 코드는 wrapper에서 처리. 이렇게 하면 lib 패키지는 애플리케이션 독립적으로 재사용 가능.

피드백 항목 3 — 결합도: 콜백 인터페이스 패턴

  • 문제
    • 코드 베이스에 'addNotification' 같은 구체적 명칭을 훅/함수 인자로 넘기는 경우가 일부 보입니다(예: 초기 예시). 실제로 PR 코드에서는 notificationsAtom으로 통합했지만, 전반적으로 패턴을 점검할 필요가 있습니다.
  • 권장(인터페이스 기반)
    • 훅/함수는 onSuccess/onError 같은 일반적인 콜백을 받거나, 이벤트 버스/로그 레이어를 사용해 구체 구현을 분리합니다.
  • AS-IS / TO-BE 예시
    • AS-IS:
      const useAddProduct = (addNotification) => { /* addNotification('...') */ }
    • TO-BE:
      const useAddProduct = ({onSuccess, onError}) => {
        // 내부에서 onSuccess?.({id}) 등을 호출
      }
      // 또는 CartService 내부에서 notification 이벤트를 publish

피드백 항목 4 — ESLint / 설정 파일 문제 (실행 시 에러 위험)

  • 문제
    • .eslintrc.cjs 파일이 ESM 스타일(import ...; export default ...)로 바뀌어 있으며(.cjs 이름과 ESM 내용 불일치). CI/로컬에서 ESLint 실행 시 구문 오류로 실패할 가능성 높음.
  • 권장
    • .eslintrc.cjs이면 module.exports = {...} 형식을 사용하거나, .eslintrc.mjs로 변경하여 ESM 형식을 사용하십시오.
    • Prettier / ESLint 플러그인 의존성 버전과 peerDependencies 충돌 여부 확인.

피드백 항목 5 — 테스트와 Jotai store isolation (잘한 점)

  • 좋은 점: tests에서 createStore + Provider + HydrateAtoms 조합으로 테스트 간 상태 오염을 방지한 점은 매우 바람직합니다. 이 방식은 Jotai 테스트 권장 패턴입니다.
  • 개선 제안: HydrateAtoms 구현을 테스트 util로 분리(src/test-utils/renderApp)하면 반복 코드 제거.

  1. 구체적 TO-BE 코드 샘플 (핵심 전환 시나리오)

A. useCart: AS-IS → TO-BE(서비스 어댑터 사용)

  • AS-IS (요약):
    useCart 직접 useAtomValue/useSetAtom 호출(현재 코드)
  • TO-BE: CartService 인터페이스 + useService 훅을 통해 DI
    • service interface (위에서 제시)
    • useService 및 Provider 패턴(간단 예):
      // src/advanced/services/ServiceRegistry.tsx
      import React, { createContext, useContext } from "react";
      
      type Registry = { [key: string]: any };
      const ServiceContext = createContext<Registry | null>(null);
      
      export const ServiceProvider = ({ services, children }) => (
        <ServiceContext.Provider value={services}>{children}</ServiceContext.Provider>
      );
      
      export const useService = <T,>(key: string): T => {
        const ctx = useContext(ServiceContext);
        if (!ctx) throw new Error("ServiceProvider 미설정");
        return ctx[key] as T;
      };
    • Jotai adapter를 서비스로 등록:
      const jotaiCartService = createJotaiCartService(); // adapter 구현
      <ServiceProvider services={{ cart: jotaiCartService, /*...*/ }}>
         <App />
      </ServiceProvider>

B. calculateItemTotal 리팩토링

  • AS-IS (초기)
    export const calculateItemTotal = (item, cart) => {
      const discount = getMaxApplicableDiscount(item);
      return Math.round(item.product.price * item.quantity * (1 - discount));
    };
  • 이미 개선한 형태(주입) — 매우 좋음:
    export const calculateItemTotal = (item, cart, getMaxDiscount) => {
      const discount = getMaxDiscount(item, cart);
      return Math.round(item.product.price * item.quantity * (1 - discount));
    };
  • TO-BE (패키지화)
    • 파일: src/lib/entities/cart/calc.ts (순수함수만, 의존 주입)
    • 테스팅/문서화가 쉬워짐

C. 함수 의존성 주입: 기본값 제공 예시

  • TO-BE로 복잡도 낮추기:
    import { defaultGetMaxDiscount } from "./defaultDiscount";
    
    export const calculateItemTotal = (
      item,
      cart,
      getMaxDiscount = defaultGetMaxDiscount
    ) => {
      return Math.round(item.product.price * item.quantity * (1 - getMaxDiscount(item, cart)));
    };
    • 호출부는 기존처럼 calculateItemTotal(item, cart)로 호출 가능. 테스트나 특수 케이스에서만 getMaxDiscount를 주입.

  1. 패키지화(모듈화) 시 고려사항 및 권장 분리 단위

권장 패키(모듈) 경계:

  • entities (도메인 순수유틸/타입) — 완전 독립
    • src/lib/entities/cart/*
    • calculate*, discount logic, 타입
  • core/hooks/adapter-layer — store-구현에 의존
    • src/services/jotaiCartAdapter.ts
    • src/services/zustandCartAdapter.ts
  • features (도메인별 훅/컴포넌트)
    • components/, hooks/ (presentation에는 adapter만 사용)
  • ui (재사용 UI 컴포넌트)
    • components/ui/*
  • test-utils
    • renderApp, HydrateAtoms, createStore helper

패키지화 팁:

  • entities는 외부에 의존하지 않게 만들 것(react, jotai 등 미사용).
  • 훅/서비스 어댑터는 인터페이스를 구현하고 Provider로 주입.
  • 컴포넌트는 가능하면 훅만 사용(앱 내부 구현 숨김) — "컴포넌트에 store 임포트 금지".

응집도/결합도 측면:

  • 응집도: entities, ui, features 각자 책임을 가지므로 응집도는 높아졌음(좋음).
  • 결합도: 현재 결합도는 '중간' 수준 — components는 hooks에만 의존해야 하는데 일부 컴포넌트/테스트가 atoms를 직접 import 하고 있으므로 결합도를 더 낮출 필요 있음.

우선순위 권장 액션 (빠르게 개선 가능한 것부터)

  1. .eslintrc.cjs 파일 포맷 정리 (.cjs이면 module.exports 사용 OR .mjs로 변경). (높음)
  2. useCart/useAdminProducts/other 훅을 service-adapter 패턴으로 감싸기 — 핵심 인터페이스 정의. (중~높)
  3. entities(계산 함수) → src/lib로 이동(순수함수 모음). (중)
  4. 테스트 유틸(renderApp, HydrateAtoms)을 공통 util로 분리. (낮)
  5. README / 개발문서에 서비스 어댑터 교체 가이드(예: Jotai→Zustand)를 추가. (중)

마지막으로 몇 가지 체크리스트(간단)

  • .eslintrc.cjs 파일 형태 확인 및 CI 통과 여부 확인
  • store 디펜던시가 외부로 누출되는 import가 있는지(components/tests) 검색 — 모두 hooks/service로 교체
  • calculateItemTotal 등 엔티티 함수는 라이브러리로 떼어낼 때 외부 의존성 없는지 확인
  • ServiceProvider 패턴 도입 시 기존 useAtom 기반 훅과 병행하여 마이그레이션 플랜 수립 (점진적 전환 가능)

추가 질문(저녁/다음 PR에 같이 고민해볼 거리)

  1. 서비스 어댑터(예: CartService)와 현재 action atom 방식 중 어떤 것을 먼저 적용해보고 싶으신가요? (단계화해서 도와드릴게요)
  2. 라이브러리 전환을 가정할 때, “hooks 내부에서 바로 useAtom을 쓰는 방식”과 “ServiceProvider로 캡슐화하는 방식” 중 어느 쪽으로 마이그레이션을 선호하시나요?
  3. 패키지로 떼어내려는 유닛(entities/훅/UI 컴포넌트) 중에서 우선순위를 정하신 게 있다면 알려주세요 — 그에 맞춰 분리 예시 코드와 배포 스크립트(rollup/vite 패키지)를 제안드리겠습니다.

종합 평가(한 줄)

  • 아키텍처 관점에서 훌륭한 분리와 의도(컨테이너/프레젠터, 파사드, 옵저버, 테스트 독립성)를 가지고 잘 구현하셨습니다. 다음 단계는 Jotai에 묶여있는 구현 디테일을 서비스 인터페이스로 캡슐화하여 라이브러리 전환과 패키지화에 대비하는 것입니다.

원하시면:

    1. useCart → CartService 어댑터로 리팩터링한 PR 예시 코드(실제 파일 변경)를 만들어 드리겠습니다.
    1. 또는 .eslintrc.cjs / package.json 변경 관련 CI 문제를 빠르게 해결하는 PR도 도와드릴게요.

원하시는 다음 작업을 말씀해 주세요.

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