[7팀 정건휘] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍#48
[7팀 정건휘] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍#48geonhwiii wants to merge 34 commits intohanghae-plus:mainfrom
Conversation
JunilHwang
left a comment
There was a problem hiding this comment.
안녕하세요! PR 정리 & 리팩토링 잘 진행하셨습니다. 전반적으로 UI 로직과 도메인(모델) / 상태 관리가 많이 분리되어 있고, Context / Model / Hook 단위로 나눈 점이 인상적입니다. 아래 피드백은 '기술 요구사항 변화 시나리오 → 코드가 그 시나리오에 대해 어떤 영향이 있는가 → 개선 제안' 흐름으로 작성했습니다. PR을 GitHub Review로 바로 남기실 수 있도록 종합/상세 피드백, AS-IS/TO-BE 코드 예시를 포함합니다.
주의: PR에 eslint/prettier 및 lockfile 변경이 매우 큽니다. 리뷰 포인트는 주로 src/advanced 쪽 리팩토링(도메인, 컨텍스트, 훅, 모델)에 집중했습니다.
종합 피드백
- PullRequestFiles에서 보이는 키워드(요약)
- 분리(Separation): entities(models)/contexts/hooks/ui/pages 로 계층 분리
- Domain Model: CartModel, CouponModel, ProductModel(부분적)
- Context 기반 상태관리: NotificationProvider, ProductProvider, CouponProvider, CartProvider, AppProvider
- Pure Hook & Utilities: useLocalStorage, useDebounceValue, useNotifications
- UI 컴포넌트 분리: Header, NotificationItem, AdminDashboard, UserDashboard(참조된 파일)
- 대규모 도입: ESLint/Prettier 설정 및 의존성 패치(많은 패키지 추가)
- 잠재적 문제: import alias(예: '@/basic' vs '@/advanced'), circular dependency 주석, 일부 중복/경계가 모호한 로직
- PullRequestBody("과제 셀프회고" 등)에 대한 피드백
- 현재 PR 본문(제공된 PullRequestBody)은 과제 목적/검증 항목을 잘 정리해두셨습니다. 좋은 구조입니다.
- “과제를 하면서 내가 제일 신경 쓴 부분은 무엇인가요?” 같은 주관적 질문에 대해, 인사이트형 피드백:
- 잘 하신 점: UI/도메인/상태 관리를 분리하려는 시도(entities/models, contexts, hooks 분리). 도메인 로직을 CartModel 같은 클래스로 옮긴 것은 재사용/테스트 관점에서 좋습니다.
- 더 생각해볼 점: Context들이 상태+사이드이펙트(예: localStorage, notifications, validation)를 직접 수행하고 있는데, 이게 장기적으로 결합도를 높일 수 있습니다. "도메인 로직(순수) vs 인프라(저장소, 알림 등)" 경계를 더 명확히 하면 추후 상태관리 라이브러리 교체나 패키지 분리 시 비용이 훨씬 낮아집니다.
- 이어서 생각해볼 질문:
- "Notification은 도메인 로직에서 직접 호출해야 할 책임인가?" (예: addProduct가 성공하면 알림을 호출할 책임은 UI/오케스트레이션 레이어에 두는게 더 깔끔)
- "localStorage는 Provider 내부에 있어야 하는가, 아니면 adapter로 분리되어야 하는가?"
- "현재 contexts 훅들이 직접 localStorage를 사용하도록 되어 있는데, 이걸 추상화하면 어떤 장점이 생기는가?"
- PullRequestBody의 "리뷰 받고 싶은 내용"에 대한 답변(예상)
- 질문이 구체적으로 기재되지 않았지만, 보통 요청하시는 내용은 다음일 거라 가정하고 답변드립니다:
- 상태관리 라이브러리 변경(예: jotai → zustand/tanstack-query/redux) 시 영향: 아래 섹션에 시나리오별로 구체적으로 적었습니다.
- 모듈화/패키지 배포 시 체크포인트: entities(models)는 응집도가 높아 패키지화 적합. contexts/hooks/ui는 응집도가 섞여 있어 분리 작업 필요 — 상세에 기술합니다.
- 코드가 잘 작성되었는가: 전체적으로 개선되었으나, 결합도 일부(알림·저장·검증의 혼재), import 경로 불일치, circular dependency 가능성 등 수정 권장 사항이 있습니다.
상세 피드백 — 정의 · 문제 · AS-IS · TO-BE
먼저 개념 정의 (리뷰에서 사용할 기준)
- 응집도(Cohesion): 한 모듈(파일/패키지/컴포넌트)이 하나의 책임(변경 이유)을 얼마나 잘 갖고 있는가. 높은 응집도는 변경 시 수정해야 할 파일 수가 적고, 패키지로 떼어내기 쉬움.
- 실무적으로 보는 규칙(제안하신 규칙 반영):
- 변경에 대한 동선(파일/코드 수정 경로)이 짧은가? (짧을수록 응집도 높음)
- 라이브러리로 떼어낼 때 매끄럽게 추출 가능한가?
- 실무적으로 보는 규칙(제안하신 규칙 반영):
- 결합도(Coupling): 모듈이 다른 모듈에 얼마나 직접적으로 의존하는가. 낮을수록 좋음. 특히 인터페이스 기반(함수 인자, 콜백, 이벤트, 타입)으로 결합을 낮추는 게 핵심.
문제 요약(큰 항목)
A. contexts/hooks 내부에서 알림/저장 로직이 섞여 있어 변경 시 파급범위가 커짐(응집도/결합도 이슈)
B. 일부 훅/컨텍스트가 서로를 내부적으로 호출하거나 주석으로 "순환 의존성"을 언급 — 가능성 있는 circular dependency
C. addNotification 등 'Notification'을 직접 사용하는 도메인 훅들이 외부 행동(알림)을 직접 수행함 — 호출자 제어 불가(결합도)
D. import alias / 루트 경로 혼재('@/basic' vs '@/advanced') — 빌드/패키징/추출 시 문제 가능
E. ESLint/Lockfile 변화가 너무 크고 리뷰에서 도메인 변경 분석을 방해함(부수 변경 분리 권장)
각 항목별 상세
A. 알림/저장 로직 혼재 — AS-IS (예시)
- 파일: src/advanced/contexts/ProductContext.tsx (또는 CartContext)
- AS-IS (요약): addProduct 내부에서 setProducts(...) 및 addNotification('상품이 추가되었습니다.', 'success') 같이 상태 갱신과 알림을 혼재.
- 문제: addProduct가 내부에서 알림까지 다루기 때문에, 알림 동작을 바꾸려면 ProductContext를 수정해야 함(동선 길어짐). 또한 상태관리 라이브러리 변경 시 알림 호출 코드가 여러 훅/컨텍스트에 흩어질 수 있음.
AS-IS 코드 (간략)
- (현재)
function addProduct(newProduct) {
setProducts(prev => [...prev, { id:p${Date.now()}, ...newProduct }]);
addNotification('상품이 추가되었습니다.', 'success');
}
TO-BE (권장) — 책임 분리: 도메인 함수는 상태만 변경/반환, 알림은 호출자(혹은 오케스트레이터)가 처리
- 옵션 1: addProduct는 성공/실패 결과를 반환하고 caller가 알림 처리
- 장점: 상위 레이어(UI/컴포저)가 알림을 제어 → 재사용성↑, 결합↓
- 옵션 2: addProduct에 선택적 콜백(onSuccess,onError) 인자를 추가
- 장점: 호출자가 원하는 행동을 주입하여 결합 감소
TO-BE 코드 예시 (선택: 반환형)
async function addProduct(newProduct) {
try {
setProducts(prev => [...prev, { id: p${Date.now()}, ...newProduct }]);
return { ok: true, productId: generatedId };
} catch (e) {
return { ok: false, error: e };
}
}
호출자:
const res = addProduct({...});
if (res.ok) notifySuccess('상품 추가됨');
else notifyError(res.error.message);
또는 콜백 버전:
function addProduct(newProduct, { onSuccess, onError }) {
try {
setProducts(...);
onSuccess?.();
} catch (e) {
onError?.(e);
}
}
권장 이유: "알림"은 인프라/UX 관점의 행동이지 도메인 핵심 변경사항이 아니므로 분리해야 추후 알림체계(모달, toast, 서버 로그 등) 변경 시 도메인 코드 변경이 필요없음.
B. 순환 의존성 & import 혼재
- 예: src/advanced/entities/coupons/hooks/useCoupons.ts 에서 '@/basic'을 import 하고, contexts 파일은 '@/advanced' 를 참조하는 등 모듈 경계가 섞여 있습니다. CouponContext에 'Note: This creates a circular dependency issue' 주석도 존재.
- 문제: 빌드 시 alias/tsconfig paths 설정에 따라 순환 또는 잘못된 모듈 해석 가능. 또한 basic/advanced 혼재는 패키징 시 의존성 분리가 어려움.
해결 권장:
- 프로젝트 내 모듈 경계 규칙 정의: entities(models) → domain(상위) → features/contexts → UI 순으로 의존성은 단방향(내부→외부).
- 사용 예시: entities/* 는 절대 불변(순수 로직)으로 두고, contexts/hooks는 entities를 소비만 함. 절대로 entities가 contexts를 import하지 않게 한다.
- 일관된 import alias 사용: 모든 파일에서 '@/...'가 같은 루트(또는 상대경로로 통일). PR에서 'basic'/'advanced' 혼재는 바로 정리 권장.
C. 상태관리 라이브러리 변경 시 영향(시나리오 별)
목표: jotai → (1) tanstack-query, (2) zustand, (3) redux
공통 전제: 지금 코드는 React Context + useLocalStorage(useState) 기반. 도메인 로직은 CartModel/CouponModel 등으로 분리되어 있어 '상태 로직'과 '도메인 계산'의 경계가 어느 정도 존재함. 이점은 변화 적응에 유리.
시나리오 1: zustand 로 전환
-
무엇을 바꿔야 하나?
- ProductProvider, CartProvider, CouponProvider 등 Context + useLocalStorage 부분을 zustand store로 대체.
- 기존 useProducts/useCart 훅은 store를 읽고 쓰도록 바꾸거나(구현체만 교체) 기존 훅 레이어를 유지하면서 내부에서 zustand를 사용해도 됨 (호환 레이어).
-
파급 범위
- Provider 컴포넌트(현재 AppProvider) 제거 가능(혹은 그대로 두고 내부에서 store 초기화).
- 컴포넌트에서 useProducts()나 useCart() 훅 API를 유지하면 수정 최소화 가능(추상화가 잘 되어 있으면).
-
예시 TO-BE (zustand)
// store/productsStore.ts
import create from 'zustand';
export const useProductsStore = create(set => ({
products: initialProducts,
addProduct: (p) => set(state => ({ products: [...state.products, p] })),
updateProduct: (id, updates) => set(state => ({ products: state.products.map(x => x.id === id ? {...x,...updates}:x) }))
}));// 기존 useProducts 훅을 wrapper로 남겨 호출부 변경 최소화
export function useProducts() {
const products = useProductsStore(s => s.products);
const addProduct = useProductsStore(s => s.addProduct);
return { products, addProduct, ... };
} -
난이도: 중(Provider 제거, 일부 훅 재구성). 현재 모델/도메인 로직이 분리되어 있어 상태전환 비용은 낮은 편.
시나리오 2: tanstack-query (React Query)
- tanstack-query는 서버/비동기 캐시용에 강함. 로컬 상태(장바구니, localStorage) 자체를 서버와 동일한 캐시 모델로 관리할 수 있지만, '로컬 동기 상태'를 직접적으로 대체하기엔 부적합.
- 권장 사용처: products/coupons를 서버에서 fetch하는 시나리오로 전환 시 tanstack-query로 전환 → 캐쉬/동기화/refresh 용이.
- 파급 범위
- localStorage 중심의 useLocalStorage는 유지하거나, hydrate/serialize 전략으로 tanstack-query의 초기데이터를 채움.
- 장바구니처럼 로컬 중심 상태는 여전히 zustand/Context가 더 적합.
- 난이도: 보통 — 서버 연동/캐시 추가 시 이득 큼.
시나리오 3: redux
- 무엇을 바꿔야 하나?
- Context Provider들을 Redux store/Reducer로 옮기고, 기존 hooks는 selector/action 호출로 바꿈.
- 파급 범위
- 많은 파일이 바뀔 수 있음(Provider→store, useX → useSelector/useDispatch)
- 권장: 만약 큰 앱이나 미들웨어(analytics, persistence, undo 등)가 필요하면 redux 추천. 지금 코드베이스는 중소규모라면 오히려 복잡도 증가.
결론(상태관리 변경 관점)
- 현재 구조(도메인 로직: 모델 / 상태: 컨텍스트 훅 / UI 소비자)라면
- zustand 도입 시 가장 적은 파급(Provider가 필요 없고 훅 래퍼로 교체 가능)
- tanstack-query는 서버 데이터/캐싱 전환에 적합
- redux는 큰 규모/특정 미들웨어가 필요할 때 고려
- 중요한 조건: contexts/hooks의 public API(예: useProducts().addProduct)가 안정적이면 구현체 교체는 쉬워짐. 지금은 어느 정도 추상화가 되어 있어 이식성이 괜찮습니다. 다만 내부에서 addNotification과 강하게 결합한 부분을 분리하면 더 쉬워집니다.
D. 패키지화(모듈화) 시 고려사항 — 응집도 판단
- "응집도가 높은 것" (즉, 패키지로 떼어내기 쉬운 것)
- src/advanced/entities/* (models: CartModel, CouponModel, 등) — 도메인 순수 로직, 외부 의존 적음 → 높은 응집도 → 좋은 후보
- src/advanced/constants/* 및 utils (pure) — 후보
- "응집도가 낮고 재구성 필요"
- contexts/providers/hooks: 현재는 도메인과 인프라(localStorage, notification)에 걸쳐 있음. 패키지로 떼려면 인프라(저장소, 알림)를 추상화(adapter)를 통해 주입해야 함.
- UI(components/pages) — 앱 종속성(스타일/tailwind 등)이 있어 분리 시 많은 의존성 검토 필요.
- 패키지화 TO-BE 전략(권장)
- entities/models: 독립 패키지로 추출 (npm 패키지로 배포). public API는 순수 함수 또는 클래스(입력: items, product 등 → 출력: 계산결과)로 제한.
- 예: @your-org/domain-cart: export CartModel, calculateItemTotal(cart, item), types
- adapters: localStorage-adapter, notification-adapter(인터페이스화)
- 패키지 내부에서는 저장소/알림 직접 호출하지 않고 adapter 인터페이스로 주입받음.
- 예: createCartService({ storageAdapter, notificationAdapter })
- contexts/providers: thin orchestration layer. 내부적으로 domain 패키지 + adapters를 조합.
- UI: 최상단에서 provider(혹은 store)를 조합하여 렌더링
- entities/models: 독립 패키지로 추출 (npm 패키지로 배포). public API는 순수 함수 또는 클래스(입력: items, product 등 → 출력: 계산결과)로 제한.
- 기대 이득: 도메인 패키지(응집도 높음)는 독립 테스트/릴리스 가능. Context 레이어는 앱별로 가벼운 glue 코드만 유지.
구체적 코드 개선 제안(AS-IS → TO-BE 예시)
- 알림 결합 개선 (addProduct)
-
AS-IS (ProductContext.tsx)
const addProduct = useCallback((newProduct) => {
const product = {..., id:p${Date.now()}};
setProducts(prev => [...prev, product]);
addNotification('상품이 추가되었습니다.', 'success');
}, [addNotification, setProducts]); -
TO-BE 1 (반환형)
const addProduct = useCallback((newProduct) => {
const product = {..., id:p${Date.now()}};
setProducts(prev => [...prev, product]);
return { ok: true, product };
}, [setProducts]);// 호출부
const res = addProduct(payload);
if (res.ok) addNotification('상품이 추가되었습니다.', 'success'); -
TO-BE 2 (콜백 주입)
const addProduct = useCallback((newProduct, { onSuccess, onError } = {}) => {
try {
const product = {..., id:p${Date.now()}};
setProducts(prev => [...prev, product]);
onSuccess?.(product);
} catch (e) {
onError?.(e);
}
}, [setProducts]);// 호출부
addProduct(payload, {
onSuccess: () => addNotification('상품 추가 완료', 'success'),
onError: (e) => addNotification(e.message, 'error')
});
- CartModel을 패키지화 가능한 '순수' API로 정리
- AS-IS: CartModel는 잘 작성되어 있음. 다만 내부에서 direct 로컬 state에 의존하지 않게, 생성 시 items만 주입하는 형태로 유지(현재도 그런 형태). 좋은 상태입니다.
- TO-BE: export 순수 함수도 제공 (테스트/패키지 이용 편의)
export function calculateTotals(items: CartItem[], coupon?: Coupon) { ... }
// 이렇게 하면 Context는 이 함수만 import해서 사용하면 됨.
- 상태관리 교체 예시: ProductProvider → zustand (작은 예)
-
AS-IS: ProductProvider + useLocalStorage
-
TO-BE (zustand, wrapper 유지)
// stores/products.ts
import create from 'zustand';
export const useProductsStore = create((set) => ({
products: initialProducts,
addProduct: (p) => set(state => ({ products: [...state.products, p] })),
updateProduct: (id, updates) => set(state => ({ products: state.products.map(x => x.id === id ? {...x,...updates} : x) }))
}));// 기존 훅을 유지해 호출부 영향 최소화
export function useProducts() {
const products = useProductsStore(s => s.products);
const addProduct = useProductsStore(s => s.addProduct);
// wrap notifications same as before at caller level
return { products, addProduct, ... };
}
그 외 개선 권장 포인트 (우선순위 포함)
- (우선) Notification과 Domain 로직의 결합 완화 — 반환 값 또는 콜백 패턴 도입
- 일관된 import alias/폴더 구조 정리: '@/basic' vs '@/advanced' 섞여있음 → 빌드 깨질 수 있음
- CouponContext에 주석으로 적어둔 순환 이슈 해결: contexts 간 의존 방향 재설계 (예: CouponContext는 CartContext를 참조하지 않게, 필요한 데이터(총액)는 호출자 전달)
- tests: CartModel / CouponModel / 계산 함수(unit tests) 추가 — 모델이 순수함으로 테스트하기 쉬움
- PR 분리 권장: eslint/lockfile 변경과 리팩토링(도메인/컨텍스트) 변경을 분리하면 리뷰 난이도 낮아짐
- Public API 설계: hooks/contexts가 앱 전반에 걸쳐 사용하는 안정된 API를 먼저 문서화(예: useCart()가 제공하는 함수/시그니처)하면 상태관리 교체 시점에 바꾸기 쉬움
마무리(요약 및 행동 항목)
- 잘한 점: 도메인(모델) 분리, Context/Hook 계층 형성, AppProvider로 조합. CartModel/CouponModel 설계는 패키지화가 유리할 만큼 응집도가 높습니다.
- 위험/개선: Notification(알림)·저장(localStorage)·검증 로직이 도메인 훅에 섞여 결합도가 높아짐 — 알림을 호출자에 위임하거나 콜백/반환값 패턴으로 결합도 낮추세요. import alias 일관성도 반드시 정리하세요.
- 상태관리 변경: 현재 구조라면 zustand 도입이 가장 적은 비용(Provider 제거, 훅 래퍼로 교체 가능). tanstack-query는 서버 데이터 용도에 추천. redux는 대규모/미들웨어 필요 시.
- 패키지화: entities/models 먼저 추출(높은 응집도), adapters(localStorage/notification)를 인터페이스로 추상화 후 contexts에 주입하세요.
추가로 원하시면
-
- useProducts/useCart 등 특정 훅을 zustand로 바꾸는 구체적 코드 PR(예시) 작성
-
- entities(models)를 npm 패키지로 떼어낼 때의 package.json / 빌드 스크립트(rollup/tsup) 템플릿 제안
-
- PR을 분리하는 구체적 분기(ESLint 변경 분리, 모델 추가 분리 등) 제안
원하시는 항목을 알려주시면 그에 맞춰 TO-BE 코드(완전한 파일)와 커밋 단위 분리 계획을 만들어드리겠습니다.
과제 링크
https://geonhwiii.github.io/front_6th_chapter2-2/
과제의 핵심취지
과제에서 꼭 알아가길 바라는 점
기본과제
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는 잘 제거했나요?
전체적으로 분리와 재조립이 더 수월해진 결합도가 낮아진 코드가 되었나요?
과제 셀프회고
고민 Point?
주제가 함수형 프로그래밍이었는데, 고민할 시간이 없었습니다...!
AI를 안쓰니까 일단 리팩토링 시간이 상대적으로 오래걸렸습니다.
목표는 FSD를 떠나서, entities와 같은 각 모델들을 잘 구분해서 분리하는 것에 집중하였습니다.
처음에 분리하다보니 무작정 hooks로 분리하는 자신을 발견하게 되었고,
정신차리고 예시를 참고해서 기능별 entities에 집중해서 model 분리에 집중하였습니다
느낀점 및 리뷰 요청
아쉬운 부분은 각 Provider가 상위 Provider를 참조하다보니 좋은 구조라고는 생각은 안됩니다.
zustand,jotai등의 상태 관리 라이브러리를 사용하거나,내부에서 context를 직접 사용하지 않고 의존성을 주입하는 것이 좋을 것이라 생각은 듭니다.
[질문]
Context API를 사용할 때 보통 이런 구조로 Provider를 연계해서 사용하는 경우가 자주 나오는데,
각 Provider가 내부적으로 외부 Context를 덜 참조되게 구성할 수 있을까요?