Skip to content

[6팀 김수현] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍#26

Open
suhyeon57 wants to merge 24 commits intohanghae-plus:mainfrom
suhyeon57:main
Open

[6팀 김수현] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍#26
suhyeon57 wants to merge 24 commits intohanghae-plus:mainfrom
suhyeon57:main

Conversation

@suhyeon57
Copy link

@suhyeon57 suhyeon57 commented Aug 4, 2025

배포링크

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

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

과제 셀프회고

  • 사이드 이펙트란?
    --> 프로그래밍에서 어떤 함수나 연산의 수행 결과로 시스템의 상태가 예상치 못하게 변경되는 현상
    --> 함수 외부의 상태가 변화
    --> 순수 함수는 같은 입력 값에 대해 항상 동일한 결과 반환, 즉 사이드 이펙트를 일으키지 않음

  • 순수 함수란?
    --> 외부 상태 변경이 없고, 입력에만 의존 항상 같은 출력
    ex)

function add(a, b) {
  return a + b; // 외부에 영향을 주지 않음
}

=> 즉, 사이드 이펙트 일으키지 않음

  • 유틸 함수란?
    --> 반복적으로 사용하는 로직을 재사용 가능하게 만든 작은 함수
    --> 일반적으로 계산, 형식 변환, 데이터 가공, 조건 검사 등 작은 단위의 기능을 담당

  • 커스텀 훅이란?
    --> React의 hook을 조합해 만든 함수
    --> 재사용 가능한 상태 관리 로직과 사이드 이펙트 처리를 묶은 것
    --> 이름은 반드시 use로 시작

<GPT에게 물어본 궁금증>
Q.
그 커스텀 훅 안에 있는 함수들이 있잖아, 거기서도 한 번만 쓰이는 함수가 있으면 안돼?
A.
커스텀 훅 안에 있는 함수가 한 번만 사용된다고 해도, 그 로직이 역할이 분명하고 의미가 있다면 괜찮다.
목적은 가독성, 유지보수성, 역할 분리야.
재사용성은 중요한 기준이지만, "무조건 여러 번 쓰여야 의미 있는 건 아니다".

  • 문제 해결 (의존성 배열)
const setValue = useCallback(
  (value: T | ((prev: T) => T)) => {
    const valueToStore = value instanceof Function ? value(storedValue) : value;
    setStoredValue(valueToStore);
    window.localStorage.setItem(key, JSON.stringify(valueToStore));
  },
  [key, storedValue] //
);

useLocalStorage.ts를 구현할 때
setValue 함수가 storedValue를 의존성으로 가져서 무한루프가 발생하는 문제점이 발생하였다.
(처음에 product와 cart를 변경할 때에는 테코가 통과해서 문제점을 알지 못 했다.)

그래서 외부 상태를 참조하지 않고, 함수형 업데이트를 사용하여 의존성 배열에서 storedValue를 제외시켰다.

const setValue = useCallback(
    (value: T | ((prev: T) => T)) => {
      try {
        setStoredValue((prevValue) => {
          const valueToStore =
            value instanceof Function ? value(prevValue) : value;

          if (typeof window !== "undefined" && window.localStorage) {
            try {
              window.localStorage.setItem(key, JSON.stringify(valueToStore));
            } catch (storageError) {
              console.warn(
                `Error setting localStorage key "${key}":`,
                storageError
              );
            }
          }

          return valueToStore;
        });
      } catch (error) {
        console.warn(`Error in setValue for key "${key}":`, error);
      }
    },
    [key]
  );

🔍 분리 기준 명확히!
📋 훅 (Hook) 분리 기준
상태 관리 + 상태 변경 로직 + React 생명주기
📋 비즈니스 로직 분리 기준
순수 계산 + 조건 판단 + 데이터 변환 (React와 무관)

<GPT와의 대화>
Q.

export function addItemToCart(
cart: CartItem[],
product: ProductWithUI
): CartItem[] {
const existingItem = cart.find((item) => item.product.id === product.id);

if (existingItem) {
// 기존 아이템이 있으면 수량 증가
return updateCartItemQuantity(cart, product.id, existingItem.quantity + 1);
}

// 새 아이템 추가
return [...cart, { product, quantity: 1 }];
}

그럼 이건 상태 변경이 아니라 react와 무관하기 때문에 비지니스 로직으로 분리하는거지? 커스텀 훅이 아니라?

A.

// 🏢 비즈니스 로직: "무엇을" "어떻게" 할지
export function addItemToCart(cart: CartItem[], product: ProductWithUI): CartItem[] {
  // 장바구니 추가 규칙만 담당
}

// 🎣 React 로직: "언제" "어디서" 상태를 업데이트할지  
const useCart = () => {
  const [cart, setCart] = useState<CartItem[]>([]);
  
  const addItem = (product: ProductWithUI) => {
    const newCart = addItemToCart(cart, product); // 비즈니스 로직 위임
    setCart(newCart); // React 상태 관리
  };
  
  return { cart, addItem };
};

✅ React와 완전히 독립적 - 순수한 계산 로직
✅ 비즈니스 규칙에만 집중 - "어떻게 추가할지"만 담당
✅ 재사용성 극대화 - 어떤 상태 관리든 사용 가능
✅ 테스트 용이성 - React 환경 없이 단위 테스트 가능
✅ 함수형 프로그래밍 - 순수함수, 불변성 유지
반면 커스텀 훅은 "React 컴포넌트에서 어떻게 이 로직을 사용할지"에 대한 인터페이스를 제공하는 역할입니다! 🎯

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

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

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

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

export function isCartEmpty(cart: CartItem[]): boolean {
  return cart.length === 0;
}

이 함수는 조건을 판단하기 때문에 cart 비지니스 로직으로 구분하였는데 비지니스 로직인지, 유틸 함수로 구분해야할지 궁금합니다.

저는 과제를 진행할 때
ask 모드로 "여기서 ui를 분리하고 싶어, 진행 방법 알려줘" --> Ai가 답변 --> 복사 붙여넣기

비슷하게 ui를 분리 후 "비지니스 로직을 분리하고 싶어 어떻게 해야 돼?" -->
대답을 보고 "이건 비지니스 로직으로 빠져야 하구나" 하고 비지니스 로직으로 복붙을 하며 진행

에러 발생 --> 에러를 복사해서 ai에게 "이건 뭐야?" --> 답변을 통해 어디서 문제가 왜 발생하는지 확인 후 변경
이런 식으로 진행해 왔습니다.

이렇게 진행하다 보면 손코딩은 별로 없고 그냥 "아 ~ 그렇구나" 라고 하며 지나가게 됩니다.
그러다 문득, 베이직 진행 중 이 방향에 의문이 들어 "다시 처음부터 AI에게 묻지 않고 내 손으로 나눠가며 진행을 해볼까?" 라고 생각했지만,
일단 과제의 통과가 목적이기 때문에 어찌됐든 진행이 되면 넘어갔습니다. (회사에서 deadline 맞추듯이 과제도 똑같이 수행)

이런식으로 하는게 맞는지 고민이 되고, 다음 주차 과제에서는 또 똑같이 이렇게 진행을 해야할지 의문이 듭니다.

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.

안녕하세요, 과제 잘 진행하셨습니다. 구조화(엔티티 · 훅 · UI · 순수 로직) 관점에서 의도적으로 분리한 흔적이 잘 보입니다. 아래 피드백은 PR 파일들(src/advanced 이하 변경분)을 중심으로 작성했고, 요청하신 흐름대로

  • 기술요구사항 변화 시나리오 제시 → 각 시나리오에 대한 현재 코드의 영향 및 필요한 변화 → 이를 바탕으로 “잘 작성되었는가”를 판단하는 방식으로 구성했습니다.

요약/목차

  • 핵심 키워드(종합 피드백)
  • PullRequestBody(셀프회고)에 대한 피드백 및 질문 답변
  • 기술 요구사항 변화 시나리오 (2개) + 각 시나리오별 AS-IS → 영향 → TO-BE(권장 변경)
  • 상세 피드백 (결합도 / 응집도 정의 → 파일/영역별 문제·AS-IS·TO-BE)
  • 권장 작업 목록(우선순위)

종합 피드백 - 핵심 키워드

  • 분리(엔티티 vs UI) — 좋음: models, hooks, components로 책임을 분리
  • 상태관리 전환 준비성 — 보완 필요: 현재 Jotai 중심구현(원자적)은 좋지만 추상화 계층(인터페이스)이 부족해 라이브러리 전환 시 수정 범위가 넓음
  • 응집도 — 대체로 높음: models(순수 로직), constants, utils의 역할이 명확. 하지만 atoms/hooks/components 사이 일부 경계가 더 명확해지면 패키지화가 쉬움
  • 결합도 — 개선 여지: 컴포넌트가 직접 useAtom/useAtomValue를 쓰지는 않지만(대신 훅이 제공되면 더 좋음), 일부 훅이 외부 콜백(addNotification)을 인자로 받아 의존성이 유입됨
  • 테스트 전략 — UI 통합 테스트는 잘 구성되어 있으나, 핵심 순수로직(models)은 단위테스트가 더 유리함

PullRequestBody(셀프회고) 관련 인사이트

  • 분리 기준(엔티티/뷰/순수함수)에 대한 이해가 잘 잡혀 있음 — 특히 useLocalStorage의 setValue 문제를 함수형 업데이트로 고친 점은 좋은 학습 포인트(의존성 관리/불변성/부수효과 고려).
  • “커스텀 훅 안에서 한 번만 쓰이는 함수”에 대한 질문에 대한 답변도 아주 합리적입니다 — 재사용성만이 분리 기준이 아니고 책임 명료화, 가독성, 테스트 용이성이 더 중요한 경우가 많습니다.
  • 추가로 생각해볼 질문들:
    • 모델(순수함수) 수준에서 가정한 입력/출력 제약(예: product의 discounts가 항상 존재)들이 충분히 방어적인가?
    • 훅 / atoms / 모델 간 인터페이스(파라미터와 반환값)가 문서화되어 있나? 팀원과 공유하기 쉽게 만들면 더 안전함.
    • 현재 단위 테스트(모델 함수)를 만들면 어떤 가치(버그 회귀 방지, 라이브러리 전환 시 안전성)가 생길까?

PullRequestBody - 질문에 대한 답변
Q2. isCartEmpty 함수는 비즈니스 로직인가, 유틸 함수인가?

  • 권장: 엔티티(비즈니스) 로직으로 분류하세요.
    이유:
    • isCartEmpty(cart) 는 Cart 엔티티의 상태를 판단하는 비즈니스 규칙(도메인 규칙)이다.
    • 단순하더라도 entity 관련 규칙은 models/cart.ts 같은 도메인 레이어에 두는 게 테스트·재사용·이식성 측면에서 유리합니다.
    • 유틸함수로 두면 다른 영역(문자열 유틸 등)과 섞여 가독성이 떨어질 수 있음.
  • 결론: 지금처럼 src/advanced/models/cart.ts에 둔 것은 적절합니다. (AS-IS: models에 포함되어 있음 — OK)

Q3. AI를 보조도구로 쓴 방식에 대한 고민(“복붙/수정” vs “스스로 풀기”)

  • 인사이트:
    • 목표(과제 통과, 데드라인 준수)를 우선시하는 현실적인 접근이며 생산성 측면에서 합리적입니다.
    • 다만 “학습”을 목표로 한다면, AI의 제안을 단순 복붙으로 끝내지 말고 (1) 작은 단위로 손으로 다시 작성해보기, (2) 제안된 코드를 리팩토링하여 테스트 추가해보기 를 권합니다.
  • 권장 실습 루틴:
    • ‘모델 함수 1개’를 AI에게 설계하게 한 뒤, 직접 손으로 다시 구현하고 테스트해보기.
    • ‘훅 하나’를 AI가 제안한 인터페이스대로 작성한 뒤, 다른 상태 라이브러리(간단: local useState)로 포팅해보기(책임·인터페이스의 의미가 명확해짐).

시나리오 1 — 상태관리 라이브러리 변경 (jotai → react-query / zustand / redux 등)
요구사항: 향후 Jotai 대신 다른 상태관리 라이브러리를 도입할 수도 있음.

현재 코드(AS-IS) 영향 요약

  • src/advanced/atoms.ts 에 atom/atomWithStorage가 중심. components/hooks가 useAtom/useAtomValue를 직접 사용하거나 useXxx 훅에서 useAtom을 사용함.
  • useCartJotai, useProducts, useCoupons 등 훅이 내부 구현에서 Jotai API를 직접 호출(또는 의존). 예: useCartJotai에서 useAtom(cartItemsAtom).
  • 결과: 상태관리 라이브러리 변경 시 atoms.* + 훅 내부 구현 대부분을 변경해야 함.

변화가 발생할 수 있는 부분

  • atoms 파일 전체를 새 store로 옮기거나 재작성해야 함.
  • hooks는 대부분 store read/write 부분만 바꾸면 되지만, 현재 컴포넌트들이 store API를 직접 사용하고 있는 곳(예: Header가 useAtom/useSetAtom 사용)까지 바뀌면 컴포넌트도 수정 필요.

권장 TO-BE (추상화 계층 추가)

  • 목표: 컴포넌트와 비즈니스 로직은 state 라이브러리 종류에 영향을 받지 않도록 추상화 계층(포트/어댑터)을 둠.
  • 방법: public API 훅을 “인터페이스”처럼 만들고, 내부 구현은 jotai/zustand/redux로 바꿀 수 있게 분리.

예시 — AS-IS (간단 재현)
컴포넌트에서 직접 useAtom 사용

// AS-IS: Header.tsx
const [isAdmin, setIsAdmin] = useAtom(isAdminAtom);

예시 — TO-BE: state adapter 사용

  1. define public hook (interface)
// src/state/cart-api.ts (추상 인터페이스)
export type CartAPI = {
  getCart(): CartItem[];
  addToCart(product: ProductWithUI): void;
  updateQuantity(productId: string, quantity: number): void;
  removeFromCart(productId: string): void;
  getTotals(): { totalBeforeDiscount: number; totalAfterDiscount: number; };
  subscribe?: (cb: () => void) => () => void; // optional
};

// Default 구현(현재는 Jotai 기반)
export const createJotaiCartAPI = (): CartAPI => {
  return {
    getCart: () => { /* use atom inside closure? or wrap in hook */ },
    addToCart: (p) => {},
    // ...
  }
};
  1. 컴포넌트/훅은 CartAPI 기반의 훅 사용
// useCartState.ts (public hook)
import { useMemo } from "react";
import { cartApi } from "../state/impl"; // jotai impl을 export 함

export function useCartState() {
  // 내부에서 cartApi를 호출
  // cartApi가 Context에 등록되어 있거나 모듈로부터 가져오게 할수도 있음
  return cartApi;
}

// 사용처
const cart = useCartState();
cart.addToCart(product);
  1. 상태 라이브러리 전환 시
  • cart-api의 구현(createJotaiCartAPI → createZustandCartAPI)만 바꾸면 됨.

간단 예시(구현 대체의 차이):

  • Jotai impl: useAtom 내부 구현을 wrapping한 createJotaiCartAPI
  • Zustand impl: zustand store의 get/set을 wrapping한 createZustandCartAPI

결론(시나리오1)

  • 현재 구조는 "models(도메인) vs state vs UI"로 잘 분리되어 있어 전환이 쉽지 않은 수준은 아님.
  • 다만, components/hooks에 useAtom/atomWithStorage 노출이 일부 있어 "완전 교체" 시 수정범위가 atoms + hooks + 일부 컴포넌트에 걸쳐 발생.
  • 권장: state adapter 계층(useCartState, useProductsState 등)을 만들고 내부를 Jotai 기반으로 구현 → 추후 교체 시 adapter만 바꿔라.

시나리오 2 — 모듈화(패키지로 배포) — 분리/응집도/결합도 관점
요구사항: 일부 코드를 패키지화(예: entities/models 라이브러리, UI 컴포넌트 라이브러리)하여 다른 프로젝트에서 재사용.

응집도 정의(요청하신 정의 반영)

  • 응집도: 변경에 대한 파일 및 코드의 추가/수정/삭제 동선이 얼마나 짧은가 (짧을수록 응집도 높음)
  • 모듈로 떼어낼 때 매끄럽게 떼어낼 수 있는가 (의존성 범위가 작을수록 좋음)

결합도도 함께 (인터페이스를 통한 결합 완화)

  • 낮은 결합도 예: 인터페이스(옵션 콜백, onSuccess/onError)를 통한 의존성 주입
  • 높은 결합도 예: 특정 용어(addNotification)를 직접 파라미터로 요구

현재 코드 구조(파일군 / 응집성 관점)

  • 잘 응집된 부분
    • src/advanced/models/* : Cart / Product / Coupon 의 순수 비즈니스 로직이 정리되어 있음 → 높은 응집도
    • src/advanced/constants/* : 도메인 상수는 잘 모였음
    • src/advanced/hooks/* : 훅들이 각 도메인별로 모여 있음 (useProducts, useCoupons, useCartJotai, useNotificationJotai)
    • src/advanced/components/* : UI 컴포넌트들이 기능별로 모여 있음 (AdminPage, ProductPage, CartPage, Header, ToastList 등)
  • 낮은 응집도 / 결합 문제 지점
    • atoms.ts: 많은 atom(제품, 쿠폰, notifications 등)을 하나 파일에 몰아둠 → 파일이 커지고 Jotai 내부구현에 의존
    • hooks와 components의 경계: components import some models (e.g., AdminPage imports models/product) — 괜찮지만 models는 순수 로직으로 유지해야 함.
    • useCartJotai API 설계: 외부 의존(addNotification)을 파라미터로 받고 있음 → 결합을 유발

모듈화(패키지화) 제안 - 권장 분리

  • 패키지 A (entities / domain)
    • types.ts, models/, constants/, utils/* (순수 함수)
    • 응집도: 매우 높음 (domain만 포함), 외부 상태관리/DOM 의존 없음 → 바로 패키지화 가능
  • 패키지 B (state adapters)
    • atoms.ts (if using jotai), 또는 zustand/store.ts, 그리고 adapter-level hooks (useCartState, useProductsState interface)
    • 의존: 패키지A만 참고
  • 패키지 C (ui components)
    • UI만 담당 (Button, ToastList, ProductCard, Header). Props 기반으로 동작. 내부에서 상태를 직접 읽지 않음(혹은 최소한으로).
    • 의존: 패키지A (타입) + 패키지B(선택) via injected props/hooks
  • 앱 레이어
    • 패키지B의 구현을 사용해 훅(createContext 또는 provider)로 묶고, UI 컴포넌트에 props 주입

AS-IS → TO-BE 코드 예시 (모듈 분리)
AS-IS: components 직접 useCartJotai 사용 (tight coupling)

// AdminPage.tsx (AS-IS)
import { useProducts } from "../hooks/useProducts";
const productsHook = useProducts({ addNotification: notifications.addNotification });

TO-BE: UI 컴포넌트는 인터페이스 훅(useProductsState)을 사용

// state/public-api.ts (패키지B, 추상화만)
export type ProductsState = {
  products: ProductWithUI[];
  showProductForm: boolean;
  onShowProductForm: () => void;
  onProductSubmit: (e: FormEvent) => void;
  // ...
};

export function useProductsState(): ProductsState {
  // internally use jotai or zustand (impl file)
}
// AdminPage.tsx (TO-BE)
import { useProductsState } from "state/public-api"; // 패키지B
const { products, onShowProductForm } = useProductsState();

결론(시나리오2)

  • models/constants/utils는 패키지화 대상으로 매우 적합(응집도 높음, 결합도 낮음)
  • atoms/hooks/components는 조금 더 다듬어 adapter/props 기반으로 바꾸면 UI 패키지 vs state 패키지로 깔끔히 분리 가능

상세 피드백 — 파일 / 영역별 (정의 → 문제 → AS-IS → TO-BE)

  1. 개념 정의(요청하신 것)
  • 응집도(cohesion)
    • 같은 모듈(파일·패키지) 안의 구성요소들이 얼마나 같은 목적(책임)을 갖고 있는가
    • 높은 응집도면 모듈 변경 시 수정 범위(동선)가 짧음 → 패키지화나 재사용이 쉬움
  • 결합도(coupling)
    • 모듈들 간 의존성의 강도. 낮을수록 모듈을 독립적으로 교체하기 쉬움
    • 좋은 사례: 인터페이스/콜백(onSuccess/onError)으로 의존을 주입
    • 나쁜 사례: 특정 네이밍/행위를 직접 요구(예: addNotification으로 고정)
  1. 문제 정의 및 파일별 피드백

A. atoms.ts

  • 문제(AS-IS)
    • atoms.ts에 많은 atom/atomWithStorage(제품, 쿠폰, notifications, search 등)와 비즈니스 알림 로직(addNotificationAtom)이 몰려 있음.
    • 파일 크기가 크고 Jotai에 강결합되어 있음.
  • 왜 문제인가?
    • 상태 라이브러리 변경 시 대규모 교체 대상이 됨(응집도는 높지만, “Jotai 전용”임).
    • atoms 내부에서 setTimeout(알림 자동 제거)을 직접 사용 → side effect가 섞여 있음(원칙적으로는 괜찮지만, 테스트하기 어려움).
  • TO-BE (권장)
    • atoms 파일을 작은 모듈로 분리: productsAtoms, cartAtoms, couponAtoms, uiAtoms(notifications).
    • notifications는 "도메인 이벤트"를 발행하는 식으로 바꿈: addNotificationAction을 통해 알림을 발생시키고, 그 구현부만 atoms 패키지에 둠.
    • 또는 notifications는 domain 패키지(또는 state adapter)로 옮겨서, UI는 subscribe 형태로 알림을 사용.

예시 (TO-BE snippet)

// notifications.api.ts (public)
export type NotificationService = {
  show: (message: string, type?: 'success'|'error'|'warning') => void;
  dismiss: (id: string) => void;
};

// jotai impl
export const createJotaiNotificationService = (): NotificationService => {
  return {
    show: ({ message, type = 'success'}) => {
      // use addNotificationAtom or set notificationsAtom
    },
    dismiss: (id) => { /* ... */ }
  }
}

B. hooks/useCart.ts (useCartJotai)

  • 문제(AS-IS)
    • 훅이 addNotification을 파라미터로 받음: useCartJotai({ addNotification }) — 외부에서 알림 로직을 넘겨야 함.
    • 일부 함수(예: calculateItemTotal) 내부에서 전체 cartItems를 인자로 요구하여 재계산을 수행함 (이는 괜찮음).
  • 왜 문제인가?
    • addNotification을 인자로 받으면 훅을 사용하는 쪽이 알림 구현을 알고 있어야 함 → 결합도가 증가.
  • TO-BE
    • 덜 결합되게 바꾸기: useCartJotai는 내부에서 useNotificationJotai()를 호출하여 알림을 직접 트리거하도록 하거나, 빈 옵션으로 두어 외부 주입을 선택적(optional)으로 만들 것.
    • 또는 notifications는 전역 서비스(또는 atoms)로 노출하여 훅 내부에서 접근하게 함.

AS-IS → TO-BE 예시
AS-IS:

export function useCartJotai({ addNotification }) {
  // uses addNotification(...)
}

TO-BE:

export function useCartJotai() {
  const notifications = useNotificationJotai();
  // inside: notifications.addNotification(...)
}

C. models/* (cart.ts, product.ts, coupon.ts)

  • 장점(AS-IS)
    • models에 순수 함수가 잘 모여 있음 — 이건 단위테스트/패키지화에 아주 좋음.
    • calculateCartTotal, addItemToCart, updateCartItemQuantity 같은 함수가 순수함수로 구현되었음(대체로).
  • 개선 포인트
    • 타입 안정성: safeParseNumber 같은 유틸을 잘 쓰셨지만, models 내부에서 null/undefined 방어가 더 필요할 수 있음.
    • 더 작은 함수로 분해 (예: getMaxApplicableDiscount는 좋음). 테스트 추가 권장.
  • TO-BE
    • models는 그대로 entities 패키지로 떼어내기 가장 적합.
    • 각 모델에 대해 중심 contract(입력 타입, 출력 타입)를 README/주석으로 명시.

D. components/*

  • 장점
    • UI 컴포넌트들이 props 중심보다는 훅 API를 사용해 상태를 읽고 동작을 수행하도록 바뀌고 있음(제품 페이지, 카트 페이지 등).
    • ToastList/ToastItem처럼 UI만 담당하는 컴포넌트가 분리되어 있음.
  • 개선 포인트
    • 일부 컴포넌트(AdminPage 등)가 훅으로부터 많은 함수들을 받는데, 이들은 명확한 인터페이스로 묶어 public API 훅(useProductsState 등)으로 노출되면 재사용성이 증가함.
  • TO-BE
    • UI 컴포넌트는 가능한 “props + callback” 기반으로 만들고, 상위 레이어에서 해당 callbacks를 주입하도록. (이 방식은 컴포넌트 패키지화를 쉽게 함)

E. tests (origin.test.tsx)

  • 장점: 통합 시나리오를 광범위하게 커버(고객 플로우, 관리자 플로우, 로컬스토리지 등).
  • 문제/취약점:
    • DOM 텍스트 기반 검사(내용 문자열)로 brittle(텍스트 조금만 바뀌어도 깨짐).
    • 통합 테스트만 많고, models의 단위 테스트가 거의 없음. 모델은 빠르게 테스트 가능하므로 단위 테스트 추가 권장.
  • 권장
    • models/cart, models/product 등 핵심 순수함수에 대한 vitest 단위 테스트 작성(로직이 복잡하고 버그 유발 빈도가 높음).
    • 통합 테스트는 “주요 플로우”만 남기고, 덜 중요한 문자열 매칭은 제거해 안정성 확보.

구체적 코드 AS-IS / TO-BE 예시 (요청하신 방식으로 몇 가지 핵심 사례)

  1. 알림(Notifications) 의존성 분리
    AS-IS (부분 발췌)
// useCartJotai signature
export function useCartJotai({ addNotification }: UseCartJotaiProps) {
  // use addNotification inside
}

TO-BE (권장) — 내부에서 notification atom/service 사용

// useNotificationJotai 반환물을 내부에서 사용
export function useCartJotai() {
  const notifications = useNotificationJotai();

  const addToCart = useCallback((product) => {
    const remaining = getRemainingStock(product, cartItems);
    if (remaining <= 0) {
      notifications.addNotification("재고가 부족합니다!", "error");
      return;
    }
    // ...
  }, [cartItems, notifications]);
}

이렇게 하면 훅 사용자가 알림 구현을 알 필요가 없고, 훅 자체가 책임을 가짐. 만약 외부 주입이 필요하면 optional parameter로 허용(externalNotifier?: NotificationService).

  1. 상태 라이브러리 추상화 예시
    AS-IS
// components/Header.tsx
const [isAdmin, setIsAdmin] = useAtom(isAdminAtom);

TO-BE (추상화 계층)

// state/public-api.ts
export function useUIState() {
  return {
    isAdmin: /* get from underlying store */,
    toggleAdmin: /* ... */,
    searchTerm: /* ... */,
    setSearchTerm: /* ... */
  };
}

// Header.tsx
const { isAdmin, toggleAdmin, searchTerm, setSearchTerm } = useUIState();
  • 장점: 내부 구현(jotai/zustand/redux)을 바꿔도 Header 코드는 변경 불필요.
  1. module boundaries (패키지화 용) — entities 분리 예시
    AS-IS
  • src/advanced/models/* + components + hooks 모두 같은 레포에 있음.

TO-BE

  • packages/entities (독립): models/, types.ts, constants/, utils/*
  • packages/state-adapter-jotai: atoms, hooks 구현(useXxxState)
  • packages/ui-components: Toast, ProductCard, Header (props 기반)
  • app: wiring(provider), pages, tests

응집도·결합도 점검표 (현재 코드에 대해)

  • models (순수 함수): 응집도 높음, 결합도 낮음 — 매우 좋음(패키지화 1순위)
  • constants/utils: 응집도 높음 — 좋음
  • hooks: 응집도 보통 — state 라이브러리에 강결합되어 있음 → adapter화 필요
  • components: 응집도 보통 → props 기반으로 더 깔끔하게 만들면 응집도/재사용성 향상
  • atoms.ts: 응집도 약간 낮아질 수 있음 (하나의 파일에 많은 atom) — 분리 권장

권장 우선 작업(우선순위 포함)

  1. models 단위 테스트 추가 (우선순위: 높음)

    • calculateItemTotal, getMaxApplicableDiscount, calculateCartTotal 등
    • 이유: 비즈니스 로직을 안전하게 유지하면서 리팩토링/라이브러리 전환 가능
  2. 상태 추상화 계층 추가 (useCartState, useProductsState, useUIState) (우선순위: 높음)

    • Jotai 구현은 내부에 두되 public hook은 이 추상 API를 사용
    • 라이브러리 전환 준비, 컴포넌트 의존성 축소
  3. atoms 파일 분리(제품·쿠폰·cart·notifications 각각) (우선순위: 중간)

    • 파일이 커서 유지보수 비용 ↑
  4. useCartJotai의 addNotification 의존 제거 또는 optional화 (우선순위: 중간)

    • 내부에서 useNotificationJotai 호출하거나 NotificationService를 주입받는 방식으로 개선
  5. UI 컴포넌트 패키지화 준비

    • 컴포넌트를 props 기반으로 정리 (현재는 잘 되어 있음 — 좀더 엄격 적용)
  6. 테스트 개선: 통합 테스트는 주요 흐름만 유지, 세부 텍스트 매칭 제거 (우선순위: 낮음)


마무리 총평 — “잘 작성되었는가?”

  • 전반적으로 매우 잘 구성되어 있습니다. 특히 models(도메인)와 UI 컴포넌트의 분리는 훌륭합니다.
  • 개선할 가장 중요한 부분은 “상태추상화(어댑터)”와 “알림 의존성의 제거(또는 중앙화)”입니다. 이 두 가지만 해결해도 라이브러리 교체와 패키지화가 훨씬 쉬워집니다.
  • 또한 models에 대한 단위 테스트를 추가하면 리팩토링 시 안전도를 크게 올릴 수 있습니다.

추가 질문 / 도와드릴 수 있는 것

  • 원하시면 useCartState 같은 adapter 패턴의 구체적인 파일/코드 템플릿을 만들어 드릴게요 (Jotai → Zustand 전환 예시 포함).
  • 또는 models의 핵심 함수 3개를 골라서 unit test 케이스를 같이 만들어 드릴 수 있습니다.
  • 패키지 구조(모노레포/패키지 경계) 설계도 도와드릴 수 있습니다.

필요하시면 특정 TO-BE 코드(예: useCartState 인터페이스와 Jotai 구현, 또는 Zustand 구현 예시)를 바로 제공하겠습니다. 어느 쪽을 먼저 도와드릴까요?

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