[4팀 김수민] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍#47
[4팀 김수민] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍#47nimusmix wants to merge 15 commits intohanghae-plus:mainfrom
Conversation
JunilHwang
left a comment
There was a problem hiding this comment.
안녕하세요, 수고 많으셨습니다. PR 내용(파일 변경 + 설명)을 바탕으로 전체적인 관점(기술 요구사항 변화 시나리오 → 현재 코드가 받는 영향 → 응집도/결합도 관점에서 개선안)으로 리뷰를 남깁니다. 이 리뷰는 GitHub PR 코멘트로 그대로 옮겨 쓰기 편하게 Markdown 형식으로 구성했습니다.
주의: 코드 일부는 이미 잘 분리되어 있고 좋은 구조(entities / features / UI, 순수 계산 함수 분리 등)를 갖추고 있습니다. 그러나 일부 중복/불일치 및 향후 상태관리 라이브러리 변경·패키지화 시 걸림돌이 될 수 있는 부분들도 보입니다. 아래에서 시나리오별 영향과 구체적인 개선(AS-IS / TO-BE) 제안까지 정리했으니 참고해 주세요.
요약(한 문장)
- 장점: entities용 순수 함수(utils/cartCalculations.ts), UI 컴포넌트 분리(components/), Jotai atom으로 전역 상태화 등을 통해 도메인/뷰가 분리되어 있음.
- 개선 포인트: 상태 접근·퍼시스트 책임이 중복되어 있고(hooks vs atoms), 훅 시그니처/의존성 주입이 일관되지 않아 상태관리 라이브러리 전환이나 패키지화 시 비용이 커질 가능성 있음.
목차
-
기술 요구사항 변화 시나리오 (요구사항별 영향)
-
종합 피드백 (키워드 / PR 본문 회고 답변 / 리뷰 받고 싶은 점에 대한 답변)
-
상세 피드백 (개념 정의: 응집도·결합도 → 문제 정의 → AS-IS → TO-BE 코드 예시)
-
마이그레이션 & 패키지화 제안 (단계 + 파일/모듈 분리 제안)
-
기술 요구사항 변화 시나리오 — 어떤 영향이 생기나
-
시나리오 A: 상태관리 라이브러리를 Jotai → Zustand (or Redux / TanStack Query)로 바꿔야 하는 경우
- 영향 요약:
- 현재 상태 접근이 atoms 전역 접근(Provider + useAtom, atomWithStorage)으로 이루어짐.
- 많은 컴포넌트/훅이 useAtom / atomWithStorage에 직접 의존. 즉 전역 API(Jotai) 가 코드 곳곳에 노출되어 있어 대체 시 리팩토링 범위가 넓음.
- 일부 훅(useCart 등)은 내부적으로 localStorage를 직접 다루므로 atomWithStorage와 중복/충돌 가능.
- 난이도: 중간~높음 (특히 컴포넌트가 atoms에 직접 접근하는 경우)
- 영향 요약:
-
시나리오 B: 모듈화하여 패키지로 배포해야 하는 경우
- 영향 요약:
- 잘 분리된 순수 로직(entities: cartCalculations.ts, formatters.ts, types)은 잘 떼어낼 수 있음(높은 응집도).
- UI 컴포넌트(components/*)는 앱에 종속적인 스타일/클래스(예: Tailwind 유틸)과 Jotai atoms를 직접 참조하면 재사용/패키징이 어려움.
- hooks 폴더에 있는 훅들은 일부가 state management 구체 구현(setProducts 같은 React state setter을 인자로 받음)과 결합되어 있어 재배포 가능한 generic 훅으로 만들기 위해 인터페이스 정리가 필요.
- 난이도: 낮음~중간(순수함수 분리만 잘 되어 있어 유리하나, atoms 의존성 제거가 필요)
- 영향 요약:
-
시나리오 C: 서버-사이드 캐시/동기화를 위해 TanStack Query를 도입해야 하는 경우
- 영향 요약:
- TanStack Query는 서버-동기 상태에 강함. 현재 products/coupons/cart는 브라우저 로컬(로컬스토리지) 중심이라, Query와의 역할 분리가 필요(서버 state vs client UI state).
- Query 도입 시 데이터 fetching/remote-sync layer를 추가하고, UI는 query hook (useQuery/useMutation) 또는 전역 스토어와 연계해야 함.
- 난이도: 중간 (특히 서버/로컬 sync 규칙을 정해야 함)
- 영향 요약:
- 종합 피드백
-
PR 파일 변경에서 도출한 피드백 키워드 (요약)
- 상태관리: Jotai 기반 atoms 도입 (good), 그러나 사용 일관성 문제(직접 useAtom 사용 vs hooks에서 로컬 상태)
- 계층 분리: utils(순수함수), components, hooks, atoms 분리 (양호)
- 중복/불일치: localStorage 접근 및 알림 로직 중복, useCart 훅 vs cartAtom 중복 상태 관리
- 결합도: 컴포넌트가 Jotai에 직접 결합되어 있어 스토어 교체 시 리팩토링 범위 큼
- 응집도: utils는 응집도가 높음. 반면 hooks/atoms/컴포넌트 간 책임 경계가 일부 불분명
-
PullRequestBody의 "과제 셀프회고"에 대한 피드백(인사이트 중심)
- 작성자 코멘트: "개인 사정으로 AI와 함께 빠르게 완성했다" — 솔직함은 장점입니다. 다만, AI로 작업한 경우 다음 단계로 '검증'과 '이해'가 중요합니다.
- 인사이트 / 앞으로 생각해볼 질문:
- 이 앱에서 '엔티티'와 '뷰데이터'의 경계는 어디까지로 보는가? (예: product.description, isRecommended 같은 UI속성은 엔티티에 포함해도 될까?)
- localPersistence(로컬스토리지)을 전역 레이어(atomWithStorage)에 두었는데, 훅들이 로컬스토리지에 직접 접근하면 의도한 아키텍처와 충돌하지 않는가?
- AI로 만들었더라도, 타입 안정성/테스트(유닛테스트)로 'AI가 만든 코드의 의도'를 검증하는 워크플로를 추가하면 어떨까?
-
PullRequestBody의 "리뷰 받고 싶은 내용이나 궁금한 것"에 대한 답변(구체적 권고)
- "분리와 재조립이 더 수월해진 결합도가 낮아진 코드가 되었나요?" → 부분적으로 그렇습니다. 순수 함수와 UI 컴포넌트 분리는 잘 되어 있습니다. 다만, 상태 접근 방식(직접 useAtom 사용)이 컴포넌트들에 퍼져 있어 스토어 구현을 교체할 때 리팩토링 비용이 큽니다. 개선책은 다음 섹션에 제시합니다.
- 추가 권고:
- 핵심 도메인 로직(장바구니 계산, 재고 계산 등)은 entities 패키지로 빼서 별도 npm 패키지로 배포 가능.
- UI 컴포넌트는 스토어에 직접 접근하지 않게 하고 props와 콜백(onAdd, onRemove 등)으로 결합도를 낮추면 재사용성이 좋아집니다.
- 상세 피드백 — 개념 정의 → 문제 → AS-IS → TO-BE
A. 개념 정의 (응집도 / 결합도)
- 응집도(cohesion): 한 모듈(파일/패키지/컴포넌트)이 하나의 책임(관심사)을 얼마나 잘 수행하는가. 높은 응집도란 변경 이유가 한 곳으로 모여 있어 수정 지점이 짧고 명확한 상황을 의미.
- 실무 규칙(사용한 정의에 맞게):
- 변경에 대한 파일/코드의 추가·수정·삭제 동선이 짧을수록(작은 범위에서 끝날수록) 응집도가 높다.
- 패키지로 떼어낼 때 관련 코드를 부드럽게 분리할 수 있을 때 응집도가 높다.
- 실무 규칙(사용한 정의에 맞게):
- 결합도(coupling): 모듈 간 의존성의 강도. 낮을수록 좋음. 함수/컴포넌트는 인터페이스(명확한 props, 콜백)을 통해 결합도를 낮춤.
- 좋은 예: useAddProduct({ onSuccess, onError }) — 호출자가 행위를 제어함.
- 나쁜 예: useAddProduct(addNotification) — 호출자가 특정 함수(addNotification)에 의존해야 함.
B. 문제(요약)
- 상태 접근 책임이 분산되어 중복/불일치 발생
- atoms(atomWithStorage) + useLocalStorage/useCart(로컬 상태) + components가 직접 useAtom 사용 — 동일한 state가 여러 경로로 관리될 가능성(중복 저장, 레이스).
- 훅/컴포넌트가 상태 라이브러리에 강하게 결합
- 많은 컴포넌트(useAtom)와 hooks가 Jotai 전용 API(또는 React state setter)를 직접 요구 → 스토어 교체 시 대규모 리팩토링
- 알림(Notifications) 로직 중복
- useNotification hook, notificationsAtom, NotificationList 등 복수의 구현 흐름 존재. 사용 패턴이 혼재.
- 일부 훅/유틸의 의존성 형태가 불안정
- formatPrice(...)이 products, cart, isAdmin을 모두 전달받음(결합도 큼).
- useProduct, useCoupon 훅은 외부 setProducts API를 받는 반면, 컴포넌트는 useAtom을 직접 호출. 일관성이 없음.
C. 문제 상황(AS-IS) + 구체적 코드 예시
-
AS-IS 1: useCart 훅과 cartAtom가 둘 다 존재 (중복 책임)
- useCart (hooks/useCart.ts) : 내부에 localStorage 활용, cart 상태를 useState로 관리, addToCart 등 제공
- atoms (atoms.ts) : cartAtom = atomWithStorage('cart', [])
- 컴포넌트 / 다른 훅들은 직접 cartAtom을 읽음
- 결과: 누가 진짜 싱글 소스 오브 트루스인가 불명확. (중복, 버그 소지)
-
AS-IS 코드 스니펫 (요약):
- hooks/useCart.ts (일부분) — 내부 localStorage state 사용
- (이미 파일에 존재, 중복되는 구조)
- src/advanced/App.tsx 에서는 useAtom(cartAtom)로 cart를 다룸 → 충돌 가능
- hooks/useCart.ts (일부분) — 내부 localStorage state 사용
-
AS-IS 2: 컴포넌트가 직접 useAtom에 의존
- ex) Header.tsx, ProductCard.tsx, Cart.tsx 등에서 useAtom/ useAtomValue 직접 사용
D. 개선(TO-BE) — 제안 및 구체 코드 예시
목표:
- 상태관리 라이브러리 전환 시 리팩토링 범위를 최소화
- 응집도 높이고 결합도 낮추기(인터페이스/어댑터 패턴)
- 순수 비즈니스 로직은 entities 패키지로 분리
- UI 컴포넌트는 props-driven 으로 외부 API(콜백)만 사용
(1) 스토어 추상화(어댑터) — "스토어 인터페이스"를 둔다
설명: 모든 도메인 훅/컴포넌트는 구체적 구현(useAtom, useStore 등) 대신 추상화된 스토어 인터페이스(또는 context)를 통해 상태를 읽고 변경. 스토어 구현을 Jotai, Zustand, Redux로 바꾸는 건 그 구현만 바꾸면 된다.
AS-IS (직접 종속):
- components/ProductCard.tsx
- const cart = useAtomValue(cartAtom);
- const [, setCart] = useAtom(cartAtom);
TO-BE (어댑터 사용 — 예시)
- store/types.ts
export interface CartItemDTO { productId: string; quantity: number; }
export interface Store {
getProducts(): Product[];
getCart(): CartItemDTO[];
addToCart(productId: string): void;
updateCartItem(productId: string, qty: number): void;
clearCart(): void;
subscribe?(...): any; // 필요 시
}- store/jotaiAdapter.ts (구현 단 한 곳에만 Jotai 의존)
import { useAtom } from 'jotai';
import { cartAtom, productsAtom } from '../advanced/atoms';
export const useStore = (): Store => {
const [products, setProducts] = useAtom(productsAtom);
const [cart, setCart] = useAtom(cartAtom);
return {
getProducts: () => products,
getCart: () => cart.map(it => ({ productId: it.product.id, quantity: it.quantity })),
addToCart: (productId) => { /* 구현: 제품 객체 찾아서 setCart */ },
updateCartItem: (productId, qty) => { /* setCart */ },
clearCart: () => setCart([]),
};
};- components는 이제 useStore를 사용:
const ProductCard: React.FC<{ product: Product }> = ({ product }) => {
const store = useStore(); // 추상적 API
const add = () => store.addToCart(product.id);
// UI는 store의 내부 구현(어떤 라이브러리 사용하는지) 알 필요 없음
}이 방식의 장점:
- 전환 포인트가 store adapter 한 파일로 제한되어 Jotai→Zustand 등으로 교체 쉬움.
- 컴포넌트는 store 인터페이스만 의존하므로 테스트/모킹도 쉬움.
(2) 훅 시그니처를 콜백/옵션으로 만들기(결합도 낮추기)
- 안 좋은 예(결합): useAddProduct(addNotification)
- 좋은 예(유연): useAddProduct({ onSuccess, onError })
- 이유: 알림 UI는 사용자가 결정. 훅은 성공/오류 시 이벤트만 방출.
AS-IS:
- useProduct(setProducts, addNotification) — 특정 addNotification 함수에 의존
TO-BE:
// 훅 인터페이스
function useProduct(store: Store) {
const addProduct = async (productData: ProductData, opts?: { onSuccess?: ()=>void; onError?: (err: Error)=>void }) => {
try {
await store.addProduct(productData);
opts?.onSuccess?.();
} catch(e) {
opts?.onError?.(e as Error);
}
};
return { addProduct, updateProduct, deleteProduct };
}- 호출 시:
const { addProduct } = useProduct(store);
addProduct(payload, { onSuccess: ()=> addNotification('성공'), onError: (e)=> addNotification(e.message, 'error') });(3) 순수 로직(entities) 분리 → 패키지화 준비
- 현재: src/advanced/utils/cartCalculations.ts, formatters.ts 로 잘 분리되어 있음 — 이 파일들은 npm 패키지로 떼어내기 좋음.
- 권장: entities 패키지로 추출 (예: @yourorg/shop-entities)
- 포함: types.ts, calculateItemTotal, calculateCartTotal, getRemainingStock, formatters (순수 로직만)
- TO-BE: entities 패키지는 어떠한 상태관리 라이브러리에도 의존하지 않음(입력으로 cart, products 받음).
(4) Notification 일원화 & DI
- AS-IS: notificationsAtom, useNotification hook, 그리고 컴포넌트가 직접 setNotifications 사용하는 혼합
- 권장 TO-BE: 알림은 app-level 한 곳에서 관리되는 서비스로 추상화 (예: NotificationService context), 컴포넌트/훅은 onSuccess/onError 콜백을 통해 사용.
- NotificationService 추상화를 통해 UI(Toast 컴포넌트)와 비즈니스 로직 분리
(5) 로컬 퍼시스트 책임을 한 곳으로 집중
- AS-IS 문제: atomWithStorage + useLocalStorage + useCart가 중복
- TO-BE: persistence는 store adapter나 atoms(하나의 소스)에만 두고 훅/컴포넌트는 그것을 사용하게 함.
D. 구체적인 예시(AS-IS → TO-BE)
- AS-IS formatPrice (current):
export const formatPrice = (price: number, productId?: string, products?: Product[], cart?: CartItem[], isAdmin: boolean = false): string => {
// productId & products & cart 로 품절 판정 -> 결합도 높음
};- TO-BE (분리된 순수 함수 + 의존성 주입)
- entities/formatters.ts (패키지 내부, 순수)
export const formatPrice = (price: number, opts: { isSoldOut?: boolean; isAdmin?: boolean }) => {
if (opts.isSoldOut) return 'SOLD OUT';
if (opts.isAdmin) return `${price.toLocaleString()}원`;
return `₩${price.toLocaleString()}`;
};- 사용자는 getRemainingStock(cart, product) 같은 순수함수 호출 결과를 isSoldOut 으로 전달.
- 장점: formatPrice는 products/cart 구현에 무관하게 테스트 가능.
- 모듈화(패키지화) 제안 — 어떤 코드들이 함께 묶여야 할까
- 후보 패키지 구조(권장)
- @shop/entities (순수 타입 + 계산 함수)
- types.ts, cartCalculations.ts, formatters.ts
- 응집도: 매우 높음 — 변경 시점이 주로 비즈니스 규칙 변경
- 추출 용이성: 높음 (이미 분리되어 있음)
- @shop/state (store adapter / atoms / store implementations)
- jotai adapter, zustand adapter, atom 정의(선택적)
- atomWithStorage는 여기 유지
- 응집도: 상태 관련 책임 집중
- @shop/hooks (도메인 훅 — store 추상화 사용)
- useCart, useProduct, useCoupon 등 — store 인터페이스를 주입받음
- 응집도: 높음(도메인 훅 책임만)
- @shop/ui (재사용 가능한 UI 컴포넌트)
- ProductCard, Cart, Header 등 — props-driven
- 이 레이어는 스타일(Tailwind) 분리와 클래스 토큰 추상화 필요
- @shop/entities (순수 타입 + 계산 함수)
- 어떤 코드들이 서로 엮여있는가
- 현재: components ↔ atoms (직접 의존)
- 이상적: components ↔ hooks ↔ store adapter ↔ entities 순으로 의존(아래로만)
- 구체적으로 체크해야 할 사항(작업 목록)
- atoms와 hooks 중복된 상태 관리 정리
- useCart 훅: atom 기반으로 리팩토링하거나 사용하지 않도록 정리
- Notification 로직 통일 (NotificationService / context)
- 훅 시그니처를 콜백 기반(onSuccess/onError)으로 수정
- store adapter 인터페이스 정의 후 Jotai 구현체 한 곳으로 모으기
- entities(순수 함수)를 별도 패키지로 분리(또는 최소한 src/entities로 이동)
- 컴포넌트가 직접 useAtom 호출하지 않도록 점진적 리팩토링: props/callback으로 옮기기
- 불필요한 로컬Storage 접근 제거(useLocalStorage hook 사용 시 일관성 확보)
- 파일별(주요 변경 파일) 간단 코멘트
- src/advanced/atoms.ts — 장점: atomWithStorage로 persistence 처리(좋음). 개선: 다른 hook들과 중복되지 않게 주석/문서화 필요. 또한 store adapter 관점에서 atoms 구현부를 한 곳으로 묶기.
- src/advanced/utils/*.ts — 아주 잘 분리되어 있음, 응집도 높음. package로 떼기 쉬움.
- src/advanced/hooks/* — 좋은 분리 시도. 하지만 일부 훅(useCart)은 로컬Storage를 관리하여 atoms와 책임 중복. 훅의 인자/반환 형태는 store adapter를 주입받는 형태로 통일 권장.
- src/advanced/components/* — 잘 쪼개져 있음. 다만 props-driven으로 만들고 Jotai 직접 참조를 피하면 재사용성↑.
- src/advanced/App.tsx — 많은 로직이 atoms/컴포넌트/훅을 혼합. AppContent / Provider 분리한 것은 좋음. 다만 AppContent가 너무 많은 useAtom 호출을 직접 하고 있어 adapter 패턴 적용 시점에 리팩토링 필요.
마무리(권장 마이그레이션 절차)
- Entities 분리: src/advanced/utils → @shop/entities로 추출(먼저 로컬에서 분리)
- Store adapter 인터페이스 정의: Store 타입/훅(useStore)을 정의. 현재 atoms 구현을 jotaiAdapter로 만들기.
- Hooks 리팩토링: useCart/useProduct/useCoupon 등은 store를 인자로 받도록(또는 useStore를 내부에서 사용) 변경. 내부에서 localStorage 다루는 부분을 제거(이미 atomWithStorage가 하므로).
- Components 리팩토링: props-driven으로 변경(점진적으로). 우선 AdminPanel/ProductList 등부터.
- Notification 통일: NotificationService 제공. 훅과 컴포넌트는 onSuccess/onError를 통해 알림 트리거.
- 테스트: entities의 순수함수 유닛테스트 추가(장바구니 계산, 할인, 쿠폰 로직).
- 패키지화: @shop/entities부터 npm 내부 registry 또는 mono-repo로 분리.
질문/권유 (추가로 생각해 볼 것)
- 제품/쿠폰 데이터는 실제로 서버와 동기화할 필요가 있나요? 있다면 TanStack Query와의 역할 분리를 어떻게 할지(로컬 + 서버) 설계하면 좋습니다.
- components가 Tailwind 클래스에 강하게 의존합니다. 디자인 시스템을 별도 패키지(토큰/컴포넌트)로 추출할 계획이 있는지요?
- CI/workflow 관련 추가: pnpm 도입 및 GitHub Pages 배포 workflow 추가는 좋아보입니다(파일 변경 내역 확인). 다만 eslint/plugins 추가로 lockfile이 많이 변경되어 보이므로 의존성 관리 주의가 필요합니다.
끝으로, 잘 정리된 부분도 많고(entities 분리, components/ 폴더화, atoms 도입) 향후 확장성·교체 가능성을 높이는 몇 가지 리팩토링만 진행하면 더욱 견고한 코드베이스가 될 것 같습니다. 원하시면 1) store adapter 설계 예제(실제 코드), 2) 단계별 PR 분할(작은 PR로 나눠 리팩토링) 계획, 3) entities를 npm 패키지로 추출하는 구체적 명령(폴더/패키지.json 설정) 등을 추가로 도와드리겠습니다. 어느 부분을 더 깊게 도와드릴까요?
과제 배포 링크
https://nimusmix.github.io/front_6th_chapter2-2/basic.html
https://nimusmix.github.io/front_6th_chapter2-2/advanced.html
과제의 핵심취지
과제에서 꼭 알아가길 바라는 점
기본과제
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는 잘 제거했나요?
전체적으로 분리와 재조립이 더 수월해진 결합도가 낮아진 코드가 되었나요?
과제 셀프회고
코치님 안냐세요 ..
이번 주에 개인적으로 바쁜 일이 있어 과제를 ai랑 함께 뚝딱 해버렸답니다..
통과에 의의를 둔 점 너그럽게 양해 부탁드림니다아 ,,