[9팀 임규원] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍#37
Conversation
- eslint.config.js 로 변경, 일부 rule 추가
- CouponTab, ProductsTab 생성
- Cart, ProductList 생성
- App.tsx에서 useCart 관련 부분 제거
JunilHwang
left a comment
There was a problem hiding this comment.
안녕하세요 — PR 정리/리팩토링 작업 잘 진행하셨습니다. 전체적으로 View ↔ Entity(모델) 분리를 향해 구조를 나눈 점(components / hooks / models / constants)은 좋은 방향입니다. 아래는 PR 파일들을 기준으로 한 종합 피드백과 상세 피드백(개념 정의 → 문제 진단 → AS‑IS / TO‑BE 코드 예시 포함)입니다. 이 코멘트는 GitHub PR 리뷰 형태(마크다운)로 바로 붙여넣어 사용하셔도 됩니다.
주의: 일부 Hook/유틸은 TODO 상태로 남아 있고, 몇몇 Hook/컴포넌트는 여전히 UI 의존도가 있거나 호출 인터페이스가 단단히 결합되어 있어 개선 여지가 큽니다.
질문에 대한 답변 (PullRequestBody 기반)
- 과제의 핵심취지 & 꼭 알아가길 바라는 점(엔티티 분리, 순수함수 등)을 잘 반영하려는 시도가 보입니다. 특히 models/cart.ts 에서 비즈니스 로직(총액 계산, 할인 규칙 등)을 순수 함수로 옮긴 점은 의도에 부합합니다.
- 다만 PR에 남긴 "과제 셀프회고" / "내가 제일 신경 쓴 부분" / "리뷰 받고 싶은 내용"은 비어있습니다. 회고에는 (1) 구조 설계에서 고민했던 점, (2) 선택한 경계(어떤 기능을 모델로 옮기고 어떤 기능을 훅/컴포넌트에 남길지)와 그 이유, (3) 남은 기술적 부채(예: TODO된 hooks) 등을 적어주시면 리뷰어가 더 구체적으로 도와드릴 수 있습니다.
- 즉, 현재 상태에서 제가 드릴 수 있는 인사이트:
- models/*: 엔티티 비즈니스 로직(순수함수)으로 옮긴 것은 패키지화에 가장 적합합니다. 유닛 테스트 작성 대상 1순위.
- hooks/useCart: 좋은 시작점이나 현재 addNotification(부작용) 의존으로 재사용성이 떨어집니다. 콜백 인터페이스(onError/onSuccess)로 변경하거나, 에러/알림 처리를 컨텍스트로 위임하세요.
- components/*: UI로 잘 분리되어 있고 props로 필요한 기능을 주입하는 방식은 괜찮지만, props가 많아지면 사용성이 떨어집니다. Context/Store로 필요한 공통 상태를 제공하거나 컴포넌트 API를 단순화하세요.
- 추가 질문(추천):
- models와 hooks의 책임 경계는 어디까지로 생각하셨나요? (예: 쿠폰 검증은 모델? 훅?)
- 알림(Notifications) 시스템을 전역으로 제공할 생각이 있으신가요?
- 즉, 현재 상태에서 제가 드릴 수 있는 인사이트:
종합 피드백 (키워드, 요약)
-
주요 피드백 키워드
- 응집도: models/cart.ts의 높은 응집도(비즈니스 로직 집중) — 긍정적
- 결합도: useCart ↔ addNotification, 컴포넌트들이 많은 prop(특히 callback)을 직접 주입 — 개선 필요
- 추상화(영속성): localStorage 사용이 하드코딩되어 있음 — 추상화 필요 (useLocalStorage TODO 존재)
- 모듈화 가능성: models는 패키지화 용이. hooks와 UI는 분리 및 경계 정리가 필요
- 테스트 용이성: models는 유닛 테스트 대상, hooks는 통합/유닛 테스트 필요
- 미완성: 여러 TODO Hook/유틸 (useCoupons, useProducts, useDebounce, useLocalStorage 등)
-
PullRequestBody의 회고·중점 항목(비어있음)에 대한 피드백(인사이트)
- 인사이트: "엔티티(데이터) 중심 로직을 models로 옮기는 것"은 향후 state manager 전환이나 패키지화 시 가장 값어치가 있습니다. 왜냐하면 순수 함수로 분리돼 있으면 상태 저장 방식(로컬 state / redux / zustand / 서버 cache)을 바꿔도 모델 함수는 그대로 재사용 가능하기 때문입니다.
- 더 생각해볼 질문들:
- models의 public API(함수 시그니처)를 안정화할 수 있는가? (패키지화 관점에서 breaking change를 줄이기 위해)
- 알림/영속성/비동기 호출(예: 서버 동기화)을 어디까지 훅에 두고 어디까지 상위에서 호출할지 명확히 구분했는가?
- 현재 TODO 훅들을 먼저 구현할 경우 어떤 순서로 할 것인가? (우선순위: useLocalStorage / useDebounce / useCart)
-
PullRequestBody의 "리뷰 받고 싶은 내용"에 대한 답변(예상 질문들에 대한 답변)
- "구조가 괜찮은가?" → 전반적으로 괜찮습니다. models(순수 비즈니스 로직) / hooks(상태/부작용) / components(UI)로 분리한 것은 좋은 방향. 단, hooks와 UI가 결합되는 부분(특히 알림/로컬스토리지 직접 의존)은 추상화해야 합니다.
- "패키지로 만들 수 있겠나?" → models는 바로 패키지화하기 좋습니다. hooks는 UI 환경(React)에 강하게 묶여 있어 별도 패키지화 시 API 안정화/문서화가 필요합니다. UI 컴포넌트는 디자인 시스템 관점으로 패키지화 가능하나 props 정리가 필요합니다.
상세 피드백
먼저 개념 정의와 평가 기준을 정리합니다.
- 개념 정의
- 응집도(cohesion): 한 모듈(파일/패키지/함수)이 동일한 목적/책임을 얼마나 잘 모아두었는가. 높은 응집도는 변경 시 수정해야 할 파일이 적어지는 것을 의미합니다.
- 당신이 제안한 평가 규칙:
- 변경 경로(파일 수정/추가/삭제 동선)가 얼마나 짧은가
- 라이브러리로 잘 떼어낼 수 있는가
- 당신이 제안한 평가 규칙:
- 결합도(coupling): 모듈(혹은 함수)이 다른 모듈에 얼마나 강하게 의존하는가. 낮을수록 재사용성과 테스트 용이성이 높음.
- 좋은 방법: 인터페이스(옵션 객체, onSuccess/onError 콜백, 훅 반환값)로 결합을 낮춤
- 안 좋은 예시: addNotification 이라는 구체적인 함수명을 훅에 하드코딩으로 전달
- 문제 정의(파일 단위 주요 사항)
- models/cart.ts: 매우 좋음 — 순수 함수로 분리되어 있어 응집도 높고 패키지화에 적합.
- useCart.ts: 현재 로직(영속성, 알림, 제품 참조 등)을 담당. 모델을 잘 사용하고 있으나 addNotification을 직접 받음으로써 UI(토스트 로직)에 결합되고 있음.
- App.tsx: 과거의 큰 파일에서 여러 기능을 컴포넌트로 잘 분리했음. 다만 App가 여전히 많은 상태(setProducts, setCoupons 등)를 보유하고 있고, 그 중 일부는 useProducts/useCoupons 훅으로 추상화되어야 함.
- components/*: AdminPage, CartPage, Header, Notifications 등으로 분리하여 좋은 방향. 그러나 props의 수가 많고, 일부 컴포넌트가 상태 변경을 직접 다루는 부분(예: setShowProductForm 등)으로 결합도가 남아 있음.
- TODO 훅/유틸: useLocalStorage, useDebounce 등 구현되어야 함. 이들을 먼저 구현하면 useCart나 useProducts의 의존성을 제거/단순화할 수 있음.
- 문제 상황: AS‑IS (주요 코드 스니펫)
- AS‑IS: useCart가 addNotification을 직접 인자로 받음 (결합이 강함)
// src/basic/hooks/useCart.ts (현재)
export function useCart(products, addNotification) {
// ...
const addItemToCart = useCallback((product) => {
// ...
if (remainingStock <= 0) {
addNotification('재고가 부족합니다!', 'error');
return prevCart;
}
// ...
}, [addNotification]);
// ...
}- AS‑IS: App가 알림 상태(setNotifications)를 관리하고, 이 함수를 여러 훅/컴포넌트에 직접 주입
// src/basic/App.tsx
const addNotification = useCallback((message, type='success') => {
const id = Date.now().toString();
setNotifications(prev => [...prev, { id, message, type }]);
setTimeout(() => setNotifications(prev => prev.filter(n => n.id !== id)), 3000);
}, []);
// useCart에 전달
const { addItemToCart } = useCart(products, addNotification);- AS‑IS: localStorage 직접 사용(중복), useLocalStorage가 아직 구현되지 않음
// src/basic/App.tsx
const [products, setProducts] = useState(() => {
const saved = localStorage.getItem('products');
if (saved) return JSON.parse(saved);
return initialProducts;
});- 문제가 해소된 상황: TO‑BE (구체적 코드 제안)
- 목표: 결합도 낮추기(알림은 콜백 인터페이스로), 응집도 유지(모델은 순수), 저장 추상화(useLocalStorage), state manager 교체 쉬운 구조
A. useCart 인터페이스 개선 (AS‑IS → TO‑BE)
AS‑IS: addNotification 직접 주입 (구체적)
// 기존
export function useCart(products, addNotification) { ... }TO‑BE: options 객체로 콜백 분리(onSuccess/onError) & persistence 전략 주입
// 제안: useCart.ts
type UseCartOptions = {
onSuccess?: (message: string) => void;
onError?: (message: string) => void;
storage?: {
getItem: (key: string) => string | null;
setItem: (key: string, value: string) => void;
removeItem: (key: string) => void;
};
};
export function useCart(products: Product[], opts?: UseCartOptions) {
const { onSuccess, onError, storage = localStorage } = opts || {};
const notifySuccess = (m: string) => onSuccess?.(m);
const notifyError = (m: string) => onError?.(m);
// 내부에서 사용:
if (remainingStock <= 0) {
notifyError('재고가 부족합니다!');
return;
}
notifySuccess('장바구니에 담았습니다');
}- 장점: addNotification이라는 UI 토스트 구현에 종속되지 않고, 테스트 시 가짜 콜백 주입 가능. storage 인터페이스로 localStorage 대체도 쉬움(예: 서버 sync, memory storage).
B. Notifications를 Context로 추출 (App에서 콜백 주입의 대체)
- AS‑IS: addNotification를 여러 훅에 직접 주입
- TO‑BE: NotificationsProvider 를 만들고 useNotifications 훅 사용
// NotificationsContext.tsx
const NotificationsContext = createContext<{ notify: (msg, type) => void } | null>(null);
export const NotificationsProvider = ({children}) => {
// 내부 setNotifications 관리
const notify = (msg, type='success') => { ... } // 기존 로직
return <NotificationsContext.Provider value={{ notify }}>{children}</...>;
}
export const useNotifications = () => {
const ctx = useContext(NotificationsContext);
if (!ctx) throw new Error('useNotifications requires provider');
return ctx;
}
// useCart 사용 예
const { notify } = useNotifications();
useCart(products, { onError: (m) => notify(m, 'error'), onSuccess: (m) => notify(m,'success') });- 장점: UI 로직이 Provider에 모여 응집도가 올라감. 훅은 토스트 구체 구현에 무관하게 알림을 호출할 수 있음.
C. persistence(로컬 저장) 추상화: useLocalStorage 구현 후 주입
- AS‑IS: 여러 곳에서 localStorage 직접 사용
- TO‑BE: useLocalStorage 훅을 만들어 사용
// useLocalStorage.ts
export function useLocalStorage<T>(key: string, initial: T) {
const [state, setState] = useState<T>(() => {
const raw = localStorage.getItem(key);
if (!raw) return initial;
try { return JSON.parse(raw); } catch { return initial; }
});
useEffect(() => {
if (state === undefined || (Array.isArray(state) && state.length === 0)) {
localStorage.removeItem(key); return;
}
localStorage.setItem(key, JSON.stringify(state));
}, [key, state]);
return [state, setState] as const;
}- 후속: useCart 내부에서 storage를 선택적으로 주입하거나 useLocalStorage를 내부에서 사용할 수 있음.
D. 상태관리 라이브러리 교체 시 영향과 권장 분리
- 현재 좋은 점: models는 순수 함수로 모여 있으므로 상태 저장(React state, Zustand, Redux 등)만 바꾸면 대부분의 비즈니스 로직 재사용 가능.
- 바꿀 때 예상되는 변경 영역:
- products, cart, coupons 상태를 어디에 두느냐(로컬 React state → 전역 store)
- useCart 훅은 store 기반으로 rewrite 필요(예: dispatch 사용)
- components는 store에서 상태를 읽도록 바꾸거나 기존 props API를 유지하도록 Adapter를 작성
아래는 각 라이브러리별 간략 마이그레이션 예시(핵심만):
- Zustand 예시 (간단, 로컬 상태 대체 용이)
// store/cartStore.ts
import create from 'zustand';
import * as cartModel from '../models/cart';
type CartState = {
cart: CartItem[];
selectedCoupon: Coupon | null;
addItem: (product: Product) => void;
// ...
};
export const useCartStore = create<CartState>((set, get) => ({
cart: [],
selectedCoupon: null,
addItem: (product) => {
const cart = get().cart;
const next = cartModel.addItemToCart(product, cart);
set({ cart: next });
},
// ...
}));-
사용: 컴포넌트는 useCartStore(state => state.cart)로 읽음. useCart 훅은 thin adapter가 됨.
-
Redux (예: @reduxjs/toolkit) 예시 (더 구조화, 장점: devtools, 중앙 집중)
// features/cartSlice.ts (RTK)
const cartSlice = createSlice({
name: 'cart',
initialState: { cart: [], selectedCoupon: null },
reducers: {
addItem(state, action) { state.cart = cartModel.addItemToCart(action.payload, state.cart); },
// ...
}
});-
사용: dispatch(cartSlice.actions.addItem(product)).
-
TanStack Query (react-query) 사용 상황
- 주로 서버 데이터를 캐시/동기화할 때 사용.
- products/coupons가 서버에서 온다면 useQuery로 가져오고, add/update는 useMutation으로 처리. 로컬 계산(장바구니 총액)은 models에 남겨두고, query cache / optimistic updates를 고려.
E. 패키지화(모듈화) 제안
- 추천 모듈 구조 (monorepo 또는 단일 repo 아래 packages)
- packages/core-models (src/models/*) — 순수 함수 + 타입 + 유닛 테스트
- packages/hooks (src/hooks/*) — useCart, useProducts (React에 의존)
- packages/ui (src/components/*) — 재사용 UI 컴포넌트
- 모델을 패키지로 떼어낼 때 체크리스트:
- models는 React에 의존하지 않아야 함(현재 models/cart.ts 준수)
- 모델의 API(함수 시그니처)를 문서화하고 안정화(패치/메이저 고려)
- 번들러/빌드: tsup/rollup 설정 + types export + tree-shaking 고려
- 예시: packages/core-models/index.ts
export * from './cart';
export * from './product';
export * from './coupon';- 파일/코드별 상세 문제 & 권장 개선(우선순위)
- models/cart.ts — 상태: 매우 좋음. 권장: unit tests 추가 (경계 케이스: percentage coupon + threshold, bulk discount cap 등).
- useCart.ts — 상태: 개선 필요.
- 문제: addNotification 주입, storage 직접 사용, 일부 로직 의존성(제품 배열을 파라미터로 받음)으로 테스트 어려움.
- 권장: (1) 옵션 객체(onSuccess/onError, storage 인터페이스)로 재설계, (2) 내부에서 cartModel을 활용하되 setCart는 외부 저장 전략에 맞춰 추상화.
- App.tsx — 상태: 역할이 많이 축적됨.
- 문제: App가 여전히 state(제품/쿠폰) 유지 + 컴포넌트 라우팅 + 일부 로직 유지.
- 권장: useProducts/useCoupons 훅을 구현하여 App은 조립자 역할만 하도록. NotificationsProvider로 알림 관리.
- components/* — 상태: 전반적으로 분리 잘 되었음.
- 문제: props가 많아지면 API가 복잡해짐(특히 AdminPage Props).
- 권장: AdminPage 내부에서 useProducts/useCoupons hook을 호출하게 하거나, 상위에서 context/store로 공급하여 props 폭발을 막기.
- TODO 훅/유틸:
- 우선 구현 권장 순서: useLocalStorage, useDebounce, useProducts/useCoupons, useValidate.
- 이유: persistence & debounce는 거의 모든 훅/컴포넌트가 필요로 하므로 먼저 구현하면 다른 훅 단순화.
요약 / 권장 작업 흐름(작업 우선순위)
- models/* 에 대한 유닛 테스트 작성 (calculateItemTotal, getMaxApplicableDiscount, calculateCartTotal)
- useLocalStorage 구현(또는 storage interface 정의) → useCart/useProducts에서 사용
- NotificationsProvider + useNotifications 구현 → 기존 addNotification 전달을 Provider 기반으로 이전
- useCart API 정리(onSuccess/onError 옵션) 및 내부 리팩토링
- useProducts, useCoupons 훅 구현 및 App에서 기존 로컬 state 제거
- props 폭발하는 컴포넌트(특히 AdminPage) 리팩토링: 훅 소비 또는 context로 전환
- State manager 전환이 필요하면 models는 건드리지 않고 store adapter를 구현(Zustand/RTK 등)
추가 제안(코드 스니펫/실전 팁)
- useCart의 테스트 가능한 형태 (토이 예시)
// useCart.ts (테스트용 분리)
export function createCartManager({ storage, notify }) {
let cart = storage.getItem('cart') ? JSON.parse(storage.getItem('cart')) : [];
return {
getCart: () => cart,
addItem: (product) => {
const next = cartModel.addItemToCart(product, cart);
cart = next;
storage.setItem('cart', JSON.stringify(cart));
notify?.success('장바구니에 담겼습니다');
},
// ...
}
}- 모델 패키지에 README 및 API 예제(설치/사용)를 추가하세요. 모델 패키지가 작게 설계돼 있으면 다른 프로젝트에도 붙이기 쉽습니다.
필요하시면:
- useLocalStorage / useDebounce / NotificationsProvider의 완전한 구현 코드를 제공해 드리겠습니다.
- 또는 useCart 리팩터(옵션 객체 기반)로 실제 PR 코드를 만들어 드릴 수 있습니다.
원하시는 작업(예: 먼저 NotificationsProvider 구현 → 제가 코드로 제시)을 알려주세요.
과제의 핵심취지
과제에서 꼭 알아가길 바라는 점
기본과제
Component에서 비즈니스 로직을 분리하기
비즈니스 로직에서 특정 엔티티만 다루는 계산을 분리하기
뷰데이터와 엔티티데이터의 분리에 대한 이해
entities -> features -> UI 계층에 대한 이해
Component에서 사용되는 Data가 아닌 로직들은 hook으로 옮겨졌나요?
주어진 hook의 책임에 맞도록 코드가 분리가 되었나요?
계산함수는 순수함수로 작성이 되었나요?
Component에서 사용되는 Data가 아닌 로직들은 hook으로 옮겨졌나요?
주어진 hook의 책임에 맞도록 코드가 분리가 되었나요?
계산함수는 순수함수로 작성이 되었나요?
특정 Entitiy만 다루는 함수는 분리되어 있나요?
특정 Entitiy만 다루는 Component와 UI를 다루는 Component는 분리되어 있나요?
데이터 흐름에 맞는 계층구조를 이루고 의존성이 맞게 작성이 되었나요?
심화과제
재사용 가능한 Custom UI 컴포넌트를 만들어 보기
재사용 가능한 Custom 라이브러리 Hook을 만들어 보기
재사용 가능한 Custom 유틸 함수를 만들어 보기
그래서 엔티티와는 어떤 다른 계층적 특징을 가지는지 이해하기
UI 컴포넌트 계층과 엔티티 컴포넌트의 계층의 성격이 다르다는 것을 이해하고 적용했는가?
엔티티 Hook과 라이브러리 훅과의 계층의 성격이 다르다는 것을 이해하고 적용했는가?
엔티티 순수함수와 유틸리티 함수의 계층의 성격이 다르다는 것을 이해하고 적용했는가?
과제 셀프회고
(안녕하세요! 제시간에 다 끝내지 못해서 계속 로컬에서 계속 진행중입니다. ㅠㅠ 다 끝내지 못했지만 현재까지 한 내용들 위주로 회고 작성합니다.)
과제를 하면서 내가 제일 신경 쓴 부분은 무엇인가요?
이번엔 AI를 사용하지 않고 최대한 코드를 파악하고, ReadMe, 학습 자료 위주로 공부한 후에 최대한 적용하고자 했습니다. 또한 기존에 만들어져있던 hook을 활용만 했지, 실제로 만들어본 적은 없었어서 구조를 파악하는데 시간을 많이 투자한 것 같습니다.
이전에 isValid나 isSoldOut을 분리해서 함수의 역할을 최대한 단일화하고자 했었는데, 순수함수에 대한 자각은 없었습니다. 그런데 비즈니스 로직 ts 화 -> hook으로 분리해서 확장해가는 부분을 학습한 것 같아서 좋았습니다.
과제를 다시 해보면 더 잘 할 수 있었겠다 아쉬운 점이 있다면 무엇인가요?
useCart.ts
아직 진행중이지만, addNotification을 Props로 받아서 메시지를 뿌려주고 있는데, 이 부분에 대해 분리하고 싶습니다.
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문 편하게 남겨주세요 :)
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문