[7팀 박의근] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍#54
[7팀 박의근] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍#54adds-bug wants to merge 1 commit intohanghae-plus:mainfrom
Conversation
JunilHwang
left a comment
There was a problem hiding this comment.
안녕하세요 — PR과 과제 설명 잘 올려주셨습니다. 전체 파일 변경 내용(PullRequestFiles)은 아주 사소한 주석 변경 뿐이라 코드 변경량은 거의 없습니다만, PR에 첨부된 과제 설명(PullRequestBody)을 기반으로 구조/아키텍처 관점에서 피드백을 드리겠습니다. 이 피드백은 향후 PR 리뷰로 그대로 남기실 수 있게 마크다운 형태로 정리했습니다.
현재 PR의 실제 변경(diff)은 주석 추가(whitespace/comment)뿐입니다. 따라서 실제 코드 예시는 과제 의도(훅 분리, 엔티티/뷰 분리, 전역 상태 사용 등)에 맞춘 일반화된 개선 예시를 중심으로 드립니다.
질문에 대한 간단 답변 (요약)
- PullRequestFiles: 실제 변경은 주석 추가밖에 없습니다. 실질적 구조/기능 변경 없음.
- PullRequestBody의 과제 취지 및 체크리스트는 잘 정리되어 있음. 다만 PR에 포함된 "과제 셀프회고"나 "내가 제일 신경 쓴 부분" 등의 개인 회고 텍스트는 없습니다(추가해 주세요).
- 아래 피드백은 기술 요구사항 변화(상태관리 라이브러리 변경, 모듈화/패키지화) 시나리오와, 응집도·결합도 관점에서의 구체적 AS-IS/TO-BE 예시를 포함합니다.
종합 피드백 키워드
- 분리(Concerns separation): UI vs 도메인 로직, 엔티티 전용 함수 분리
- 순수함수(Pure functions): 계산 로직의 테스트성/재사용성 확보
- 훅 API 설계(Interface): onSuccess/onError 등 콜백 기반으로 외부 의존 주입
- 전역 상태 선택(Global state): Context / Jotai / Zustand / React Query 용도 구분
- 응집도(Cohesion) / 결합도(Coupling): 파일 경계, 변경 동선, 외부 라이브러리 교체 용이성
- 패키징(모듈화): 엔티티/비즈니스 로직의 독립 패키지화 가능성
PullRequestBody(과제 설명)에 대한 인사이트 피드백
- 좋은 점
- 과제 취지(훅/함수형/엔티티 분리)가 명확합니다.
- 체크리스트가 현실적인 항목(엔티티 vs 뷰 데이터 분리, 전역 상태 고려 등)으로 잘 작성되어 있어 구현 방향을 잡기 좋습니다.
- 개선 제안 / 더 생각해볼 점 (인사이트)
- "어떤 props는 남기고 어떤 props를 제거할지" 기준을 더 구체화하세요. 예: 도메인 컴포넌트는 엔티티 id/행동만 받고, UI 상태(열기/닫기)는 상위에서 관리한다 등.
- 전역 상태가 필요한 범위를 정의하세요. 예: '공유 엔티티'(여러 컴포넌트에서 읽기/쓰기)만 전역으로, 랜덤 UI 토글 등은 로컬로.
- 테스트 기준을 추가하세요. 엔티티 계산 함수는 단위 테스트, 훅은 렌더러 기반 테스트. 이 기준이 코드 분해의 방향을 제시합니다.
- 이어서 생각해볼 질문들
- 어떤 상황에서 Context 대신 Jotai를 선택하겠는가(규모, 성능, 중첩 렌더링)?
- 엔티티 로직이 복잡해졌을 때(예: 할인 규칙), 그 로직을 라이브러리화할 기준은 무엇인가?
- 컴포넌트 API(도메인 props) 설계 시 최소한의 인터페이스는 어떻게 정의할 것인가?
"리뷰 받고 싶은 내용이나 궁금한 것"에 대한 답변
(본문에 별도 질문 없음 — 일반적으로 자주 물어보는 항목에 대해 답변드립니다.)
-
Context vs Jotai vs Zustand vs React Query:
- 서버/캐싱 중심 상태: React Query (or TanStack Query)
- 여러 컴포넌트가 공유하는 클라이언트 상태(간단한 CRUD 도메인): Jotai / Zustand (Jotai는 atom으로 부분 구독, Zustand는 단일 store + selector)
- 전형적인 React 트리 전파 문제를 해결하려면 Context + useReducer (소규모) 또는 Jotai/Zustand (중간 규모)
- 권장: 비동기/서버 데이터는 React Query, 클라이언트 로컬 엔티티는 Zustand/Jotai. Context는 한정적 전역 컨트롤(테마, 라우팅)을 위해 사용.
-
훅 설계(예: useAddProduct API):
- 나쁜 예: useAddProduct(addNotification) — 구체적 이름이 내부 행위를 고정
- 좋은 예: useAddProduct({ onSuccess?, onError?, onFinally? }) — 호출자에게 사이드 이펙트 책임 위임
-
패키지화(모듈화) 준비:
- 엔티티 계산 함수(계산 로직)는 pure 함수로 분리하고 의존성은 explicit 하게 주입(옵션/팩토리).
- Hooks는 최소한의 외부 의존만 갖도록 추상화된 인터페이스를 사용.
상세 피드백 — 개념 정의 및 권장 AS-IS/TO-BE 예시
- 개념 정의
- 응집도(Cohesion): 파일/모듈이 하나의 책임(관심사)을 얼마나 잘 모으고 있는가. 실무에서의 측정 기준(제안):
- 변경에 대한 동선이 얼마나 짧은가(한 변경을 위해 수정해야 할 파일 수와 깊이).
- 라이브러리/패키지로 떼어낼 때 매끄럽게 떨어지는가(외부와의 의존 경계가 명확한가).
- 결합도(Coupling): 모듈/함수들이 서로 얼마나 강하게 연결되어 있는가. 낮은 결합도는 인터페이스(명확한 타입/콜백)를 통해 통신하는 것이다.
- 문제 정의(일반적으로 자주 보이는 문제)
- 도메인 계산이 컴포넌트 내부에 직접 존재 -> 재사용/테스트 곤란.
- 훅이 내부에서 구체적인 side-effect(예: addNotification API)를 직접 호출 -> 훅 재사용성 저하.
- 전역 상태 노출 범위가 과도 -> 모듈 떼어내기 어려움.
- 파일/모듈 경계가 희미 -> 응집도 낮음.
- 문제 상황 (AS-IS) — 예시 1: 훅이 구체적 사이드이펙트에 결합된 경우
- AS-IS (나쁜 예)
// useAddProduct.ts
export const useAddProduct = (addNotification: (msg: string) => void) => {
const add = (product: Product) => {
api.addProduct(product).then(() => {
addNotification('상품이 추가되었습니다');
}).catch(() => {
addNotification('실패');
});
};
return { add };
};문제: 훅 내부에서 addNotification이라는 구체적 함수 이름에 결합되어 있다. 호출자 관점에서 알림 전략을 바꾸려면 훅 자체를 수정해야 할 수도 있음.
- TO-BE (좋은 예)
// useAddProduct.ts
type Options = {
onSuccess?: (product: Product) => void;
onError?: (err: unknown) => void;
};
export const useAddProduct = ({ onSuccess, onError }: Options = {}) => {
const add = async (product: Product) => {
try {
const res = await api.addProduct(product);
onSuccess?.(res);
} catch (err) {
onError?.(err);
}
};
return { add };
};이점: 훅은 결과만 전달하고, 알림/추가 효과는 호출자가 결정 -> 결합도 낮아짐, 재사용성 증가.
- 문제 상황 (AS-IS) — 예시 2: 엔티티 계산이 컴포넌트 내부에 있는 경우
- AS-IS
// CartView.tsx
function CartView({ cart }) {
const total = cart.items.reduce((acc, it) => acc + it.price * it.qty, 0);
return <div>Total: {total}</div>;
}- TO-BE
// calculateCartTotal.ts (pure)
export const calculateCartTotal = (cart: Cart) =>
cart.items.reduce((acc, it) => acc + it.price * it.qty, 0);
// CartView.tsx
import { calculateCartTotal } from './calculateCartTotal';
function CartView({ cart }) {
const total = calculateCartTotal(cart);
return <div>Total: {total}</div>;
}이점: calculateCartTotal을 단위 테스트 가능, 다른 곳에서 재사용 가능, 뷰는 오직 렌더링 책임만 가짐 -> 응집도 상승.
- 상태관리 라이브러리 교체 시나리오
-
시나리오 A: Jotai -> Zustand
- 문제 포인트: atom 기반 코드가 많은 경우 atom 생성/구독 방식이 직접적인 의존점.
- 권장 패턴: 상태 접근을 내부 복잡성으로 캡슐화한 domain hook을 유지하고, 실제 구현에서 Jotai/Zustand를 바꾼다.
- 예시 (인터페이스 추상화)
- AS-IS (직접 Jotai atoms 노출)
// store/atoms.ts (bad) export const cartAtom = atom<Cart>(initialCart);
- TO-BE (추상화)
// domain/cartService.ts export type CartService = { getCart(): Cart; subscribe(cb: (cart: Cart) => void): () => void; addItem(item: CartItem): void; }; // implementation/jotaiCart.ts import { cartAtom } from './atoms'; export const jotaiCartService: CartService = { ... }; // implementation/zustandCart.ts export const zustandCartService: CartService = { ... }; // useCart.ts export const useCart = (service: CartService = defaultService) => { ... };
- 이렇게 하면 swap 시 서비스 구현만 바꾸면 됨.
-
시나리오 B: Jotai -> React Query
- React Query는 서버 상태용으로 권장. 로컬 엔티티 상태(로컬인게 많다면)에는 맞지 않을 수 있음.
- 권장: 도메인 훅을 "서버 상태"와 "클라이언트 UI 상태"로 분리. server fetch는 React Query, client-only는 Jotai/Zustand.
- 패키지화(모듈화) 시나리오
- 요구사항: 응집도가 높고 결합도가 낮은 모듈은 쉽게 떼어낼 수 있다.
- 권장 구조(예)
- packages/
- entities-cart/ (calculateCartTotal, types, validation)
- features-cart/ (useCart, cart service 구현)
- ui-cart/ (CartView, CartItemView)
- packages/
- AS-IS (모듈화 전): 컴포넌트, 훅, 타입이 섞여있는 src/ 폴더
- TO-BE (모듈화 후): 각 레이어(entities/features/ui)가 독립 package로 분리. interface/타입을 명확히 노출.
- 응집도 평가 규칙(제안)
- 변경 동선: 한 도메인 변경(예: cart 할인 정책 수정)에 대해 수정해야 할 파일 수 <= 3이면 양호.
- 분리 가능성: 해당 폴더를 루트로 복사/추출할 때 추가 의존(외부 전역 상태, 구체적 UI 등)이 없다면 높은 응집도.
- 테스트 범위: 모듈 단위로 독립 테스트가 가능해야 함.
- 결합도 낮추는 구체적 방법
- 훅은 최소한의 인터페이스(매개변수 객체 + 콜백)만 노출.
- 훅 내부에서 사용하는 외부 사이드 이펙트는 주입 가능하게 설계(의존성 주입).
- types/tiny interfaces를 통해 교체 가능한 구현체를 지정.
구체적인 개선 코드 스니펫 모음 (AS-IS → TO-BE)
- addNotification 의존을 제거하는 패턴
- AS-IS
const useAddProduct = (addNotification) => {
const add = (p) => {
api.add(p).then(() => addNotification('ok'));
};
return { add };
};- TO-BE
const useAddProduct = ({ onSuccess, onError } = {}) => {
const add = async (p) => {
try {
const res = await api.add(p);
onSuccess?.(res);
} catch (err) {
onError?.(err);
}
};
return { add };
};- 계산 로직 추출과 테스트 가능성
- AS-IS
// ProductList.tsx
const subtotal = products.reduce(...);- TO-BE
// entities/product.ts
export const calcSubtotal = (items: ProductItem[]) => items.reduce(...);
// ProductList.tsx
import { calcSubtotal } from '../entities/product';- 전역 상태 추상화하여 라이브러리 교체 용이
- AS-IS (direct atom usage)
// components/Cart.tsx
const cart = useAtomValue(cartAtom);- TO-BE (domain hook)
// domain/cart/index.ts
export const createCartAdapter = (impl: CartService) => {
return {
useCart: () => { ... } // 내부에서 impl 사용
};
};
// app init
const cartAdapter = createCartAdapter(jotaiImpl);앱 init에서만 구현을 바꾸면 됨.
체크리스트(본 PR/과제 기준) — 권장 작업
- 각 컴포넌트에서 비즈니스 로직이 hook/유틸로 분리되었는가?
- 계산/엔티티 전용 함수는 순수함수로 작성되어 단위 테스트 가능한가?
- 훅 API는 호출자에게 사이드 이펙트(알림, 라우팅 등)를 위임하는가(onSuccess/onError)?
- 전역 상태 사용시 server vs client 상태 구분이 명확한가? (React Query vs Jotai/Zustand)
- 모듈(entities/features/ui)이 명확히 분리되어 패키지화 가능성이 높은가?
- 변경 시 수정해야 할 파일 수가 적은가(응집도 체크)?
PR에 남길 수 있는 구체적 코멘트(예시)
- "현재 PR 파일 변경은 주석 추가만 포함되어 있습니다. 실제 로직/구조 변경이 목적이라면 더 많은 파일과 변경사항을 포함해 주세요."
- "과제 설명은 잘 정리되어 있는데, 본인의 '셀프 회고'와 '내가 제일 신경 쓴 부분'이 PR 본문에 없네요. 회고를 추가해 주시면 설계 의도와 trade-off를 더 정확히 검토할 수 있습니다."
- "핵심 개선 포인트: 훅 API를 onSuccess/onError 방식으로 바꿔 호출자에게 사이드 이펙트 책임을 위임하세요. 또한 엔티티 계산 로직은 별도 파일로 옮겨 단위 테스트를 추가해 주세요."
마무리(요약 및 다음 단계 제안)
- 현재 PR 자체는 코드 변경이 거의 없어 구조적 판단을 내리기 어렵습니다. 다만 과제 요구사항에 맞춰 리팩토링/패키지화/상태관리 교체를 염두에 둔 설계 원칙과 구체적 패턴(위 예시)을 제시했습니다.
- 다음 단계 제안:
- PR 본문에 본인 회고(무엇을 신경 썼고 왜 그렇게 했는지)를 추가하세요.
- 한 두가지 핵심 리팩토링(예: calculateCartTotal 분리, useAddProduct 콜백 패턴 적용)을 실제 코드로 적용한 커밋을 올려 주세요.
- 전역 상태 교체 시나리오를 검증하려면 domain 훅을 인터페이스로 추상화한 후, jotai와 zustand 각각으로 구현한 샘플을 만들어 비교하는 작은 PR을 올려보세요.
필요하시면 현재 저장소의 실제 파일들(핵심 훅, 컴포넌트, 상태 정의)을 붙여주시면 그 코드 기반으로 더 구체적이고 라인 단위의 PR 리뷰 코멘트를 드리겠습니다.
과제의 핵심취지
과제에서 꼭 알아가길 바라는 점
기본과제
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는 잘 제거했나요?
전체적으로 분리와 재조립이 더 수월해진 결합도가 낮아진 코드가 되었나요?