Skip to content

[4팀 김소희] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍#52

Open
esoby wants to merge 44 commits intohanghae-plus:mainfrom
esoby:main
Open

[4팀 김소희] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍#52
esoby wants to merge 44 commits intohanghae-plus:mainfrom
esoby:main

Conversation

@esoby
Copy link

@esoby esoby commented Aug 7, 2025

과제 배포 링크

advanced - esoby.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는 분리되어 있나요?

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

심화과제

  • 이번 심화과제는 Context나 Jotai를 사용해서 Props drilling을 없애는 것입니다.

  • 어떤 props는 남겨야 하는지, 어떤 props는 제거해야 하는지에 대한 기준을 세워보세요.

  • Context나 Jotai를 사용하여 상태를 관리하는 방법을 익히고, 이를 통해 컴포넌트 간의 데이터 전달을 효율적으로 처리할 수 있습니다.

  • Context나 Jotai를 사용해서 전역상태관리를 구축했나요?

  • 전역상태관리를 통해 domain custom hook을 적절하게 리팩토링 했나요?

  • 도메인 컴포넌트에 도메인 props는 남기고 props drilling을 유발하는 불필요한 props는 잘 제거했나요?

  • 전체적으로 분리와 재조립이 더 수월해진 결합도가 낮아진 코드가 되었나요?

과제 셀프회고

과제를 하면서 내가 알게된 점, 좋았던 점은 무엇인가요?

  • 팀원들과 엔티티와 ui에 대해 이해해본 걸 공유하는 시간을 가지면서 과제를 시작했는데요. 덕분에 프로그래밍에서 엔티티에 대한 개념이 명확해졌고, 나름 덜 헤매면서 과제를 할 수 있었습니다! 좋은 토론 시간이었어요

  • 뚜렷한 기준 안에서 로직을 분리해보는 경험은 처음이라 너무 좋았습니다. 이번 과제는 아쉽게 마무리 했지만, 나중이라도 다시 들여다보고 더 배워가고 싶은 과제로 느껴졌습니다.

  • 전역 상태 관리로 jotai 도입을 도전하게 되었는데요. 리코일스러움을 느꼈어요. 컨텍스트보다 직관적이고 사용 방식이 간단한 것 같아서 좋았습니다.

  • 프로바이더를 잊지 말자 .. jotai는 프로바이더가 필요없는 것 같아서 따로 세팅을 안 해두었는데 테스트 환경에서는 필요했나 봅니다. 셀프 e2e 테스트 해보면 잘 돌아가는데, 단위 테스트는 자꾸 실패해서 거의 포기하고 있었던 차에 ,, 따수운 항해 분들의 도움 덕분에 해결했습니다 헤헤헤헤헤 라이브러리 사용하면서 막연하게 사용법만 익히기 보단 전역 상태가 어떻게 관리되는지, 컴포넌트들이 어떻게 접근할 수 있는 건지 디테일하게 생각해보아야 할 것 같습니다.

  • 밤새지 말자 .................. 비효율적이다.

이번 과제에서 내가 제일 신경 쓴 부분은 무엇인가요?

  • 테스트 코드가 깨지지 않는 선에서 한 단계씩 점진적으로 개선해보자! 는 마인드로 임했습니다. 뚜렷한 상태 관련 로직을 도메인별로 커스텀 훅으로 분리하고 -> 순수 비즈니스 로직을 모델로 덜어내고 -> 반복 되는 계산 함수를 빼는 식으로 진행했습니다.

이번 과제를 통해 앞으로 해보고 싶은게 있다면 알려주세요!

  • 로직에 신경 쓰느라 컴포넌트 분리를 촘촘하게 못 한 점과 재사용되는 코드 활용을 많이 못 한 점이 아쉬워서 이 구조 그대로 좀 더 고민해보는 시간을 꼬옥 가져보려고 합니다!

  • 또 이번 과제로 리팩토링과 코드 분리의 기준에 대한 방향성이 많이 잡힌 것 같아 실무에서도 그걸 잘 활용해보고 싶어요!

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

  • useProducts 훅이 addNotification 함수를 props처럼 주입 받는 방식과 훅 내부에서 직접 useSetAtom(addNotificationAtom)을 호출하는 방식과 고민하다가 매번 의존성을 주입해주는 게 번거로워서 후자로 선택했는데, 어떤 방식이 더 좋은 설계라고 생각하실까요? 혹은 아예 독립적으로 분리가 되어야 하는 부분이었는지 궁금합니다.

  • Atom을 products, cart, ui 등 도메인별로 분리했는데, 어느 정도까지 분리하는 것이 적절한지에 대한 기준이 궁금합니다. 예를 들어, 관리자 페이지에서만 사용하는 폼 관련 상태(productForm, showProductForm 등)도 모두 Atom으로 만드는 것이 좋을까요, 아니면 지금처럼 해당 로직을 가진 훅 안에서 useState로 관리하는 것이 더 나은 선택일까요? 전역 상태와 지역 상태를 구분하는 명확한 기준에 대한 조언을 구하고 싶습니다.

esoby and others added 30 commits August 4, 2025 22:19
@heojungseok
Copy link

저도 똑같은 에러가 났었는데 전 formatter 호출할 때 파라미터 순서와 formatter 자체 로직 변경했어요.

// formatters.ts
export const formatPrice = (
  price: number,
 // 변경점 1
  isSoldOut: boolean = false,
  isAdmin: boolean = false
): string => {
  if (isSoldOut) {
    return 'SOLD OUT';
@@ -11,5 +11,6 @@ export const formatPrice = (
    return `${price.toLocaleString()}원`;
  }

   // 변경점 2
    return `${price.toLocaleString()}원`;
};


// ProductTable.tsx
{formatPrice(
   product.price,
   getRemainingStock(product, cart) <= 0,
   isAdmin
  )}

같은 상황인지는 모르겠으나, 한 번 시도를 해보심이!
단위를 지우긴 했습니다.

@heojungseok
Copy link

image

이것도 한 번 봐보세요

Copy link

@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.

안녕하세요, 과제 잘 진행하셨습니다. 전반적으로 단일 파일(App.tsx)에 몰려 있던 책임을 atoms / hooks / models / components 계층으로 분리한 점이 인상적입니다. 아래 피드백은 제공하신 PullRequestFiles를 기반으로 기술 요구사항 변화(특히 상태관리 라이브러리 교체 및 모듈화) 관점에서 예측되는 영향과 개선 제안(응집도/결합도 중심)을 정리한 것입니다. 이 코멘트를 그대로 GitHub PR Review로 남길 수 있도록 마크다운 형식으로 작성했습니다.

(참고) 이 피드백은 자동 생성된 것이며 의사결정 보조용입니다. 구현/설계 의도에 따라 일부 권장사항은 선택적으로 적용하세요.


안녕하세요, 과제 잘 진행해주셨네요. 고생하셨습니다 👍🏻
현재 남기는 코멘트는 AI가 남기는 피드백입니다. 참고해주세요!

질문에 대한 답변 (PullRequestBody 기반)

  • 전역 상태를 jotai로 옮겨서 Props drilling을 없앤 구조는 과제 핵심 취지에 적절히 부합합니다. atoms / hooks / models / UI 계층으로 분리하여 "엔티티 중심 로직"과 "UI"를 구분한 점이 좋습니다.
  • 테스트 관점: 모델(model) 파일들(cart, coupon, product, discount 등)이 순수 함수 위주로 잘 정리되어 있어 단위 테스트 작성이 쉽습니다.
  • "전역상태관리를 통해 domain custom hook을 적절히 리팩토링" 항목은 대체로 달성되었습니다. 다만 몇 군데에서 상태(알림 등)를 직접 주입/사용하는 부분이 남아 있어 완전한 분리가 필요한 곳이 있습니다(아래 상세).

종합 피드백

  1. 전체 피드백 키워드
  • 계층 분리: entities(models) / features(hooks, atoms) / ui(components) — 잘 분리되어 있음
  • 상태관리 결속도: jotai에 의존적인 구현(원천적 의존)
  • 알림 결합(Notification coupling): 여러 훅(useCart, useProducts, useCoupons)에서 addNotificationAtom을 직접 사용
  • 추상화/어댑터 필요: 상태관리 라이브러리 변경 시 교체 비용을 줄이기 위한 어댑터 계층
  • 패키징 가능성: models와 utils는 높은 응집도(패키지화 대상). atoms/hooks/components는 낮은 응집도(환경에 따라 다름)
  • 테스트 용이성: 모델은 테스트 친화적, hooks는 통합/단위 테스트 설계 필요
  1. PullRequestBody의 "과제 셀프회고"와 "내가 제일 신경 쓴 부분"에 대한 인사이트
  • 좋았던 점(인사이트):
    • 팀과의 개념 공유로 엔티티와 UI가 명확해졌다는 점: 협업에서 설계 언어(도메인 경계)를 맞춘 것은 장기적으로 코드 품질과 유지보수성에 큰 도움이 됩니다.
    • 점진적(작은 단계) 리팩토링 방식: 테스트가 깨지지 않는 선에서 단계적으로 리팩토링한 점은 실무에서도 안전한 전략입니다.
    • jotai 도입 경험과 Provider 관련 학습: 상태관리 도구의 동작 범위(전역/테스트 환경)를 직접 겪어본 것은 훌륭한 실습 경험입니다.
  • 더 생각해볼 질문들:
    • 알림/로깅처럼 cross-cutting concern은 어떻게 추상화하면 더 유연해질까? (예: Notifier 인터페이스/DI)
    • 상태관리 라이브러리(jotai)를 바꿔야 할 때 바뀌는 영향 범위(파일 리스트)는 어느 정도일까? 교체를 쉽게 하려면 어떤 추가 추상화를 도입하는 게 좋을까?
    • 훅의 부작용(알림, 로컬스토리지 접근 등)을 테스트 가능한 방식으로 어떻게 분리할 수 있을까?
  1. PullRequestBody의 "리뷰 받고 싶은 내용이나 궁금한 것"에 대한 답변(예상되는 질문에 대한 답변)
  • "jotai로 잘 리팩토링했나요?" — 네, atoms와 hook으로 역할이 분리되어 있고 models는 순수함수로 잘 분리되어 있습니다. 다만 UI 관련 알림(Notifications)이나 포맷터 등 cross-cutting concern은 여전히 훅들에 직접 주입되어 있어, 이 부분을 인터페이스/어댑터로 분리하면 더 좋습니다.
  • "어떤 props는 남겨야 할까?" — 도메인 컴포넌트에 꼭 필요한 entity(예: Product, CartItem)는 props로 남겨두고, 전역/공통인 상태(검색어, 로그인 여부, 알림)는 atoms 혹은 컨텍스트로 제공하는 것이 적절합니다.
  • "Context vs jotai 선택 기준?" — 단순한 전역 boolean(예: isAdmin)이나 트리 구조에서만 사용된다면 React Context로 충분하고, 상태의 부분적 구독/성능/비동기 연관성이 중요하면 jotai 같은 상태 라이브러리가 유리합니다.

상세 피드백 (항목별: 개념 정의 → 문제 정의 → AS-IS → TO-BE)

먼저 개념 정의(요약)

  • 응집도(cohesion): 모듈(또는 파일, 패키지)이 한 가지 책임/목적만을 수행하는 정도.
    • 규칙: 변경(추가/수정/삭제)을 위해 이동해야 하는 파일/코드의 수가 적을수록 응집도가 높다.
    • 패키지화 관점: 떼어낼 때(라이브러리) 내부 의존성이 적고 외부 노출 인터페이스가 명확하면 응집도가 높음.
  • 결합도(coupling): 모듈 간 의존성의 강도. 낮을수록 변경 영향 범위 감소.
    • 규칙: 모듈은 명확한 "인터페이스"(함수 인자, 이벤트 콜백, 서비스 인터페이스)를 통해 소통해야 함. 구체 구현(특정 상태관리 라이브러리 함수 등)에 직접 의존하면 결합도가 높아짐.

문제 1 — 상태관리 라이브러리 변경 시 영향 (jotai -> zustand / tanstack-query / redux 등)

  • 문제 정의

    • 현재 atoms 폴더(src/advanced/atoms/*)에 jotai 의존 코드( atomWithStorage, atom, useAtom 등 )가 분산되어 있음. 훅(useCart, useProducts, useCoupons)들이 atoms를 직접 사용하여 상태에 접근/수정함.
    • 즉, 상태관리 라이브러리를 바꾸면 atoms 파일 + 훅 전반, 그리고 App의 Provider 사용 부분이 변경되어야 함.
  • AS-IS (예: cartAtoms.ts)

    // src/advanced/atoms/cartAtoms.ts (AS-IS)
    import { atom } from 'jotai';
    import { atomWithStorage } from 'jotai/utils';
    import { selectedCouponAtom } from './couponsAtoms';
    import { CartItem } from '../../types';
    import { calculateCartTotal } from '../models/cart';
    
    export const cartAtom = atomWithStorage<CartItem[]>('cart', []);
    export const totalItemCountAtom = atom((get) => {
      const cart = get(cartAtom);
      return cart.reduce((sum, item) => sum + item.quantity, 0);
    });
    export const totalsAtom = atom((get) => {
      const cart = get(cartAtom);
      const selectedCoupon = get(selectedCouponAtom);
      return calculateCartTotal(cart, selectedCoupon);
    });
  • TO-BE: 어댑터/스토어 추상화 추가

    • 목적: jotai를 사용하든 zustand를 쓰든 훅(useCart 등)의 public API는 유지하고 내부 구현만 교체되도록 한다.
    • 방법 1: store-adapter 레이어를 만들기 (stores/cartStore.ts). 훅은 이 어댑터의 함수(예: getCart(), addToCart(), subscribe())만 사용.
    • 방법 2: Provider 기반으로 상태 구현을 주입(injection) — App 레벨에서 store 구현을 주입.

    예시 (zustand로 교체할 때의 TO-BE store-adapter):

    // src/advanced/stores/cartStore.ts (TO-BE, zustand)
    import create from 'zustand';
    import { persist } from 'zustand/middleware';
    import { CartItem } from '../../types';
    import * as cartModel from '../models/cart';
    
    type CartState = {
      cart: CartItem[];
      addItem: (product: Product) => void;
      removeItem: (productId: string) => void;
      updateQuantity: (productId: string, qty: number) => void;
      clear: () => void;
      totalItemCount: () => number;
      totals: (selectedCoupon: Coupon | null) => { totalBeforeDiscount: number; totalAfterDiscount: number };
    };
    
    export const useCartStore = create<CartState>()(
      persist(
        (set, get) => ({
          cart: [],
          addItem: (product) => set((s) => ({ cart: cartModel.addItemToCart(s.cart, product) })),
          removeItem: (productId) => set((s) => ({ cart: cartModel.removeItemFromCart(s.cart, productId) })),
          updateQuantity: (productId, qty) => set((s) => ({ cart: cartModel.updateCartItemQuantity(s.cart, productId, qty) })),
          clear: () => set({ cart: [] }),
          totalItemCount: () => get().cart.reduce((sum, i) => sum + i.quantity, 0),
          totals: (coupon) => cartModel.calculateCartTotal(get().cart, coupon),
        }),
        { name: 'cart-storage' },
      ),
    );
    • 훅(useCart)은 내부적으로 useCartStore를 사용만 하면 되고 나머지는 변화 없음. 이렇게 어댑터를 두면 라이브러리 교체 시 adapter 파일만 바꾸면 됨.

문제 2 — 알림(Notifications) 결합 (cross-cutting concern)

  • 문제 정의
    • useCart, useProducts, useCoupons 등 여러 훅이 addNotificationAtom을 직접 사용(useSetAtom)함 → 알림 구현(현재는 jotai atom) 변경 시 훅들이 직접 수정되어야 함. 알림을 구현체에 종속시키는 안티패턴.
  • AS-IS (useCart의 일부)
    // src/advanced/hooks/useCart.ts (AS-IS 발췌)
    const addNotification = useSetAtom(addNotificationAtom);
    // ...
    addNotification({ message: '장바구니에 담았습니다', type: 'success' });
  • TO-BE: Notifier 인터페이스/DI (의존성 주입)
    • 만들기: Notifier 인터페이스와 default 구현을 하나 둡니다. 훅은 Notifier를 인자로 받거나 컨텍스트로 주입된 Notifier만 사용.
    • 예시 코드:
    // src/advanced/services/notifier.ts
    export type Notifier = { notify: (message: string, type?: 'success'|'error'|'warning') => void };
    
    // default implementation (jotai-backed)
    import { useSetAtom } from 'jotai';
    import { addNotificationAtom } from '../atoms';
    
    export function useJotaiNotifier(): Notifier {
      const setNotif = useSetAtom(addNotificationAtom);
      return {
        notify: (message, type = 'success') => setNotif({ message, type }),
      };
    }
    • useCart는 이제 Notifier를 인자로 받거나 훅 내부에서 useJotaiNotifier()를 통해 감쌀 수 있음. 최종적으로는 테스트 시 모킹 가능한 Notifier를 주입하면 훅 단위 테스트가 쉬워짐.

문제 3 — 패키지화(모듈화) 가능성: 어떤 코드 떼낼 수 있나?

  • 문제 정의

    • 패키지로 만들 때 응집도가 높고 외부 의존이 적어야 편리. 현재 구조에서 models (src/advanced/models)와 utils(포매터, 계산함수)는 거의 독립적(순수함수)이라 패키지화 대상이 적합.
  • AS-IS

    • models/cart.ts, models/product.ts, utils/cart.ts 등은 jotai 의존성이 없음(좋음).
    • 반면 atoms/ hooks / components는 jotai/React에 강하게 결합됨.
  • TO-BE(권장 분리)

    • Extract 1 (domain-models): package @shop-domain/models → export calculateCartTotal, addItemToCart, getRemainingStock, product helpers. 이 패키지는 React/jotai 의존이 없어야 함.
    • Extract 2 (shared-types): package @shop-domain/types → Product, Coupon, CartItem 타입 정의.
    • UI layer remains in app. 훅/atoms는 애플리케이션 레벨에서 domain 패키지를 소비.

    예시: package entry

    // packages/@shop-domain/models/src/index.ts
    export * from './cart';
    export * from './product';
    export * from './coupon';
    

    그리고 앱에서는:

    import { calculateCartTotal } from '@shop-domain/models';
    import { Product } from '@shop-domain/types';

문제 4 — 결합도 낮추기: 함수-함수, 컴포넌트-컴포넌트 인터페이스 개선

  • 문제 정의
    • 나쁜 예: addNotification을 직접 파라미터로 받는 훅(예시 PR의 초반 안티패턴 설명처럼). 현재 구현은 대체로 onSuccess/onError 스타일로 개선되어 있음(좋음) — 다만 아직도 addNotificationAtom에 직접 의존하는 훅들이 있음.
  • AS-IS (예: useProducts의 add/delete)
    const addProduct = useCallback((newProduct) => {
      setProducts(prev => productModel.addProduct(prev, newProduct));
      addNotification({ message: '상품이 추가되었습니다.' });
    }, [setProducts, addNotification]);
  • TO-BE: 이벤트 콜백으로 추상화
    • useProducts는 외부에 onProductAdded 콜백을 받도록 하거나 Notifier 주입을 통해 내부 구현과 알림을 분리.
    // useProducts signature example
    export const useProducts = (notifier?: Notifier) => {
      const notify = notifier ?? useJotaiNotifier();
      // ...
      notify.notify('상품이 추가되었습니다.');
    }
    • 또는 실제 UI에서만 알림을 담당하도록 하고 hooks는 순수 도메인 행위만 수행하도록 분리(권장).

구체적 코드 개선 제안(AS-IS → TO-BE 예시)

  1. AS-IS: useCart 직접 addNotificationAtom 사용
// src/advanced/hooks/useCart.ts (AS-IS)
const addNotification = useSetAtom(addNotificationAtom);
const addToCart = useCallback((product) => {
  const remainingStock = getRemainingStock(product);
  if (remainingStock <= 0) {
    addNotification({ message: '재고가 부족합니다!', type: 'error' });
    return;
  }
  const newCart = cartModel.addItemToCart(cart, product);
  setCart(newCart);
  addNotification({ message: '장바구니에 담았습니다', type: 'success' });
}, [cart, addNotification, getRemainingStock]);

TO-BE: Notifier 주입 (결합도 낮춤, 테스트 쉬움)

// src/advanced/hooks/useCart.ts (TO-BE)
import { Notifier } from '../services/notifier';

export function useCart(notifier?: Notifier) {
  const defaultNotifier = useJotaiNotifier(); // 내부 제공
  const notify = notifier ?? defaultNotifier;

  const addToCart = useCallback((product) => {
    const remainingStock = getRemainingStock(product);
    if (remainingStock <= 0) {
      notify.notify('재고가 부족합니다!', 'error');
      return;
    }
    const newCart = cartModel.addItemToCart(cart, product);
    setCart(newCart);
    notify.notify('장바구니에 담았습니다', 'success');
  }, [cart, notify, getRemainingStock]);
  // ...
}

이렇게 하면 테스트에서 fakeNotifier를 주입하여 알림 호출 검증 가능하고, 알림 구현을 바꿀 때 훅 수정을 최소화할 수 있습니다.

  1. AS-IS: atoms가 직접 calculate 로직 호출
  • totalsAtom에서 calculateCartTotal(cart, selectedCoupon) 직접 호출하고 있음. 이건 장단점이 있는데, 모델의 계산 함수를 사용하고 있어 장점이 큼(도메인 로직 재사용).
  • TO-BE: 그대로 유지하되 "계산 로직은 models에만 있음" 원칙을 지키세요. atoms는 models에 위임만 하고 로직 자체는 변경하지 마세요(이미 지켜지고 있음 — Good).

응집도 평가 (요약)

  • models (src/advanced/models/*) — 응집도 높음: 순수 비즈니스 로직으로 잘 분리되어 있어 패키지화 적합.
  • utils (formatters, cart util) — 응집도 높음: UI/도메인 구분 잘 되어 있음.
  • atoms & hooks (src/advanced/atoms, src/advanced/hooks) — 기능적으로 관련성은 있으나 "jotai에 강하게 결합"되어 있음. 응집도는 'feature' 단위로 보면 괜찮지만, 상태관리 라이브러리가 바뀌면 큰 변경이 필요하므로 "외부 의존을 줄인 응집"으로 개선 가능.
  • components — UI 응집도 괜찮음. 도메인 props(entities)는 전달되어 있고, 전역 상태는 atoms를 통해 소비.

응집도 판단(귀하의 정의 기준에 적용)

  • 변경에 대한 동선(파일/코드 변경 범위)이 짧은가?
    • 엔티티 로직 변경(예: 할인 정책) → models만 수정하면 되므로 동선 짧음(좋음).
    • 상태관리 라이브러리 변경 → atoms/hooks/components가 광범위하게 수정되어야 하므로 동선 김(개선 필요).
  • 라이브러리로 떼어낼 때 매끄럽게 분리 가능한가?
    • models/types/utils는 매끄럽게 떼어낼 수 있음.
    • hooks/atoms는 애플리케이션에 특화되어 있어 떼어내려면 adapter/추상화가 필요함.

권장 개선(우선순위)

  1. Notifier(알림) 추상화: addNotificationAtom 직접 사용을 줄이고 Notifier 인터페이스/팩토리(useJotaiNotifier)로 대체.
  2. Store 어댑터 계층 구축: atoms 대신 stores/adapter(또는 store-facade)를 만들어 상태 라이브러리 교체 시 영향 범위를 줄이기.
  3. Public API 정의: 패키지화할 domain(models, types)은 명확한 index.ts로 노출하고, 내부 구현 의존(React/jotai 등)을 제거.
  4. 훅 테스트 전략: 훅들은 외부 의존(notifier, store)을 인자로 받거나 hook-level provider로 주입하여 단위 테스트 시 모킹 용이하게 만들기.
  5. 문서화: 각 계층(entities/features/ui)의 책임과 public 함수(예: useCart API, useProducts API)를 README로 정리하면 유지보수에 도움이 됩니다.

마무리: 구체적인 코드 스니펫 (요약)

  • 상태관리 교체 시 최소 변경 지점: create store-adapter 파일(예: src/advanced/stores/*)을 만들어 기존 훅(useCart 등)이 adapter를 참조하도록 바꾸면 앱 전체가 안전하게 교체 가능.
  • 알림 추상화 예시(간단):

AS-IS

// hooks/useCart.ts (AS-IS)
const addNotification = useSetAtom(addNotificationAtom);
addNotification({ message: '장바구니에 담았습니다', type: 'success' });

TO-BE

// services/notifier.ts
export type Notifier = { notify: (msg: string, type?: 'success'|'error'|'warning') => void };
export function useJotaiNotifier(): Notifier { ... }

// hooks/useCart.ts
export function useCart(notifier?: Notifier) {
  const defaultNotifier = useJotaiNotifier();
  const notify = notifier ?? defaultNotifier;
  notify.notify('장바구니에 담겼습니다', 'success');
}

마지막으로 — PullRequestBody에 대한 코멘트

  • 회고가 성찰적이고 좋습니다. "테스트가 깨지지 않는 선에서 점진적 개선"이라는 접근은 유지보수성 향상에 아주 효과적입니다.
  • 다음 고민거리로 권유드리는 것:
    • "앱을 라이브러리로 떼어낼 때의 경계"를 실제로 시범 적용해보세요(예: models만 패키지로 빼서 로컬로 설치해보기).
    • 훅 단위 테스트를 한두 개 더 추가해서 Notifier/Store 주입 방식이 테스트 편의성을 실제로 얼마나 향상시키는지 확인해보세요.

원하시면

  • 상태관리(jotai → zustand/Redux/React-Query) 전환 가이드(파일 목록 + 변경 스텝)를 구체적으로 생성해 드리겠습니다.
  • models 패키지화(폴더 구조, package.json, 빌드/exports 설정) 템플릿을 만들어 드릴 수 있습니다.
  • 현재 코드 중 Notifier 추상화 또는 store-adapter 코드를 실제로 리팩토링한 PR 패치를 만들어 드릴 수 있습니다.

원하시는 다음 작업을 알려주세요 — 어떤 시나리오(상태관리 교체 / 패키지화 / Notifier 추상화 등)를 먼저 도와드릴까요?

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.

3 participants