[5팀 여찬규] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍#53
[5팀 여찬규] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍#53chan9yu wants to merge 38 commits intohanghae-plus:mainfrom
Conversation
- Notification 범용 UI 컴포넌트 분리 - NotificationList를 app/components로 이관
|
해냈네.. |
컨피던스 레벨 10.... 🫠 |
JunilHwang
left a comment
There was a problem hiding this comment.
안녕하세요! PR 잘 정리하셨고 리팩토링의 의도와 결과를 아주 잘 문서화해주셨네요. 아래 피드백은 제공하신 PR 파일 변경 목록과 PR 본문(셀프회고, 고민 등)을 바탕으로 작성한 코드/아키텍처 관점의 리뷰입니다. 이 리뷰를 그대로 GitHub PR Review로 붙여 넣으셔도 괜찮게 마크다운 형식으로 정리했습니다.
참고: 이 피드백은 AI가 생성한 내용입니다 🤖
질문에 대한 간단 답변 (PullRequestBody에서 요청하신 부분)
-
Notification을 domain에 넣어야 하나?
- 권장: shared(공통, UI/infra)로 두는 편이 좋습니다. Notification은 cross-cutting concern으로 여러 도메인에서 공통으로 사용되므로 shared/ui + shared/store(또는 shared/notifier) 형태로 관리하세요. 다만 Notification 관련 *컨슈머(표시 방식)*는 shared에, 각 도메인의 비즈니스 로직에서 발생시키는 이벤트는 도메인 내부에서 처리하되 shared의 notifier API(또는 이벤트 인터페이스)를 통해 호출하도록 분리하면 적절합니다.
-
공통 기능의 경계 정의(Toast, Modal 등)
- UI 컴포넌트(Toast, Modal) + UI 전용 상태(visible 등)는 shared에,
- 비즈니스에서 발생하는 “언제 보여줄지” 결정은 각 도메인 훅/서비스가 담당하되, shared의 노티파이어 API(또는 이벤트)만 호출하도록 설계하세요.
종합 피드백 (요약 키워드 및 PR 본문에 대한 피드백)
-
핵심 키워드
- 도메인 중심 구조화 (domains/*)
- 전역 상태: Jotai 도입 (atom / atomWithStorage / 파생 상태)
- 컴포넌트/훅 분리 (Container / Presentational)
- 배럴(Barrel) 사용의 장단점 및 순환 의존성 관리
- Notification: cross-cutting vs domain
- 응집도(높음) / 결합도(낮추기) — 대부분 의도대로 정리됨
- 모듈화(패키징) 가능성 — 일부 공개 API 정리 필요
-
PR 본문(과제 셀프회고 / 신경 쓴 점)에 대한 인사이트 피드백
- 잘한 점
- 대형 App.tsx를 기능별/역할별로 명확히 쪼갠 점: domains, shared, app 구조는 유지보수성/탐색성을 크게 개선합니다.
- Jotai를 도입해 전역 상태(도메인별 atom)를 도메인 내부로 넣은 점: 응집도가 높아지고 각 도메인의 “공개 API(hook)”로 소비하는 방식은 바람직합니다.
- 배럴을 이용하면서 순환 의존성을 경험하고 규칙을 세운 점: 실무에서도 중요한 경험입니다.
- 더 생각해볼 점(질문)
- 각 도메인의 공개 API(도메인 루트 index.ts)에 어떤 항목만 노출할지 규칙이 있나요? (public API 정의 필요)
- Jotai atom이 비즈니스 모델(엔티티)과 UI 상태를 섞지 않도록 경계 규칙을 더 명확히 할 수 있나요?
- Notification을 shared로 옮겼을 때 "도메인의 내부 이벤트가 Notification에 직접 의존"하는 경우를 어떻게 방지할 계획인가요?
- 잘한 점
-
PR 본문의 "리뷰 받고 싶은 내용"에 대한 답변(요약)
- 현재의 타입별 분류(Shared 안에 components/hooks/store/types로 분류)는 적절합니다. 다만 notification 관련 파일이 분산된 점(components/hooks/store/types에 분산)은 UX 개발자가 전체 흐름 파악 시 불편하니, 다음 중 하나를 권합니다:
- shared/notifications 폴더를 만들어 shared/ui에 라우팅되는 컴포넌트들(Toast, NotificationList)은 shared/components/ui에 두고, notification 전용 훅/atoms/타입을 shared/notifications 내부에 모아 두되, UI만 공유하는 규칙으로 관리 (즉, UI는 ui/에, 상태·훅은 notifications/에).
- 또는 shared/internal-notification/ 처럼 “공통이지만 하나의 기능으로 묶는” 중간 위치를 허용.
- 결론: 현재 방식(타입별 분류)은 일관성이 있어도 "관련 파일이 분산"되는 단점이 있으니, Notification 같은 cross-cutting concern은 묶어두되 내부 구조는 철저히 public(노출) vs internal(비공개)로 나누세요.
- 현재의 타입별 분류(Shared 안에 components/hooks/store/types로 분류)는 적절합니다. 다만 notification 관련 파일이 분산된 점(components/hooks/store/types에 분산)은 UX 개발자가 전체 흐름 파악 시 불편하니, 다음 중 하나를 권합니다:
상세 피드백 — 개념 정의, 문제, AS-IS, TO-BE
먼저 피드백에서 다룰 개념 정의(간단)
- 응집도(cohesion): 한 단위(파일/폴더/모듈)가 얼마나 관련된 책임을 묶어 가지고 있는가. (여기서 측정 기준: 변경 동선의 짧음, 모듈화/패키징 시 분리 용이성)
- 결합도(coupling): 모듈 간 의존성의 강도. 낮을수록 좋음. 좋은 결합도는 인터페이스(명확한 함수/훅 시그니처)를 통해 모듈을 연결하는 것.
1) 상태관리 라이브러리가 달라지는 경우 (jotai → tanstack-query / zustand / redux)
정의: 상태관리 라이브러리 교체는 public API(도메인 훅/액션)를 얼마나 깔끔하게 유지했는지에 따라 영향도가 달라집니다. 결합도가 낮을수록 교체 작업 범위가 작습니다.
문제: 현재 도메인 컴포넌트/훅이 Jotai API(useAtom/useAtomValue/atomWithStorage) 직접에 의존하면, 라이브러리 교체 시 도메인 내부 구현 전부를 수정해야 합니다. 반대로, 도메인이 "훅 인터페이스"로 은닉한다면 교체는 훨씬 쉬워집니다.
AS-IS (예시)
- domains/adminMode (현재)
// domains/shared/store/adminModeAtom.ts
import { atom } from 'jotai';
export const adminModeAtom = atom(false);
// 사용 예 (Header)
import { useAtom } from 'jotai';
const [isAdmin, setIsAdmin] = useAtom(adminModeAtom);문제점: useAtom / atom 타입에 직접 결합됨 — Jotai를 사용하는 컴포넌트가 많으면 교체 범위가 넓음.
TO-BE (인터페이스 캡슐화 — Jotai 내부는 숨기기)
- domains/shared/store/adminMode.ts (공개 API)
// public API (이 파일만 다른 모듈에서 import)
export function useAdminMode() : [boolean, (v: boolean) => void, () => void] { ... }
// 내부 구현 (Jotai 기반)
import { useAtom } from 'jotai';
import { adminModeAtom } from './adminModeAtom';
export function useAdminMode() {
const [isAdmin, setIsAdmin] = useAtom(adminModeAtom);
const toggle = () => setIsAdmin(prev => !prev);
return [isAdmin, setIsAdmin, toggle];
}- Jotai → Zustand로 전환 시:
- 변경 범위: ONLY domains/shared/store/adminMode.ts 내부만 변경. useAdminMode 호출부(컴포넌트)는 수정 불필요.
Zustand 변환 내부 (TO-BE 구현 예)
// zustand-store.ts (internal)
import create from 'zustand';
export const useAdminStore = create(set => ({
isAdmin: false,
setAdmin: (v) => set({ isAdmin: v }),
toggleAdmin: () => set(s => ({ isAdmin: !s.isAdmin }))
}));
// public hook (same signature)
export function useAdminMode() {
const isAdmin = useAdminStore(s => s.isAdmin);
const setIsAdmin = useAdminStore(s => s.setAdmin);
const toggle = useAdminStore(s => s.toggleAdmin);
return [isAdmin, setIsAdmin, toggle] as const;
}- TanStack Query는 서버 상태용이므로 로컬 UI/엔티티 상태를 저장하기 위해서는 별도 store 필요(예: Zustand) — 설계상 고려 요소입니다.
요약 권장사항
- 모든 전역 상태는 "domain의 public hook" (useXxx)으로 감싸서 외부에서 라이브러리에 의존하지 않도록 하세요.
- public hook의 시그니처는 단순하고 명확하게: 반환 값과 액션(함수)을 제공. 내부에서 라이브러리 교체가 필요하면 내부 구현만 바꾸면 됩니다.
2) 모듈화를 하여 패키지로 배포해야 하는 경우
목표: domains/* 를 독립 패키지로 떼어내는 시나리오에서의 준비와 점검.
점검 관점
- 응집도가 높은가? (파일 변경/추적 경로가 짧은가)
- 어떤 코드들이 서로 엮여있는가? (외부 의존성/순환 참조)
문제 정의
- 현재 domains 에 대한 배럴(Re-export) 패턴을 사용 중. 이게 편하지만, domain 간 순환 import가 쉽게 발생할 수 있음(이미 PR 본문에서 경험하셨음).
- Notification처럼 cross-cutting concern은 scattered(분산)되어 있어 패키지로 떼기 어렵다.
AS-IS (문제 보이는 구조 예)
domains/cart/
hooks/useCartAtom.ts // exports
index.ts // export * from './hooks/useCartAtom'
domains/coupon/
hooks/useCouponAtom.ts
index.ts // export * from '../cart' ? (순환 발생 가능)
문제: domain A의 index가 domain B를 re-export할 때 순환이 발생하면 빌드/타입 시스템/런타임에서 문제.
TO-BE(패키징 준비)
- 규칙: 도메인 수준에서만 배럴 파일을 제공하고, 배럴은 "public API만" 노출. 내부 경로(internal/ 또는 private/ 폴더)는 외부에 내보내지 말 것.
- 의존성 규칙: domains/* → shared/* → app/* 순으로만 의존. 도메인끼리는 가급적 직접 의존 금지(혹은 명확한 의존성 방향을 문서화).
- internal 폴더를 사용해 비공개 구현을 숨김:
domains/cart/
src/
index.ts // export { useCart, CartSidebar, types... } -- public
internal/
useCartAtomImpl.ts
utils/
- 패키지화 시 public index.ts 내용만 패키지에 포함. 내부는 publish 제외.
패키징 시 노출 항목 예
- exports:
- types: CartItem, CartProduct
- hooks: useCart(), useCartActions()
- components: CartSidebar, CartItemView
- 비노출: 상세 구현 유틸(helpers), atom 구현 파일, 내부 컴포넌트
응집도/결합도 검사 체크리스트 (패키지화 관점)
- 변경 시 파일 동선이 짧은가? (예: cart의 할인 로직 수정 → cart 패키지 내부에서만 수정으로 충분한가)
- external에 노출된 API가 명확하고 최소한인가?
- domain이 shared에만 의존하고 다른 domain에 의존하지 않는가?
- 배럴은 public API에만 한정되어 있는가?
3) Notification 구조(구체적인 제안)
문제 요약: Notification은 UI(Toast) + 상태 + 소비 발신자(도메인 훅/서비스) 관련 파일이 흩어져 있음. PR 본문에서도 고민하셨듯 "어디에 넣을 것인가"의 딜레마.
권장 아키텍처(간단)
- shared/notifications/
- components/Notification.tsx (Toast 구현, UI)
- service/notifier.ts (notify API, 이벤트 버스)
- store/notificationAtoms.ts (Jotai 등으로 단순 구현 — 단, 도메인에 노출 X)
- hooks/useNotifications.ts (NotificationList 소비용)
- types.ts
패턴 1 (간단): shared의 notifier 함수를 도메인이 불러서 사용
// shared/notifier.ts
type NotifyOpts = { message: string, level?: 'info'|'error' };
export const notify = (opts: NotifyOpts) => {
// 내부: jotai atom 업데이트 or event emitter publish
};- 도메인에서 사용:
import { notify } from '../../shared/notifier';
function someAction() {
try { ... } catch(e) {
notify({ message: '실패', level: 'error' });
}
}- 장점: 간단, 전역 호출 가능
- 단점: 도메인이 shared notifier에 의존 → 약한 결합 발생 (이건 acceptable: notifier는 infra/utility)
패턴 2 (DI/콜백 권장 — 결합도 최소화)
- 도메인 훅은 UI(알림) 로직을 받지 않음. 도메인의 public API(예: addProduct)를 호출한 상위 레이어가 결과(성공/실패)에 따라 notify를 호출.
// domains/product/useProductActions.ts
export function useProductActions() {
const addProduct = async (p) => {
// 반환값으로 success/fail, 혹은 Promise reject
try {
// do work
return { ok: true, product: created };
} catch(e) {
return { ok: false, error: e };
}
};
return { addProduct };
}
// UI
const { addProduct } = useProductActions();
const res = await addProduct(payload);
if(res.ok) notify({message: '추가됨'});
else notify({message: '실패', level: 'error'});- 장점: 도메인이 Notification에 의존하지 않고, 호출자(컴포넌트)에서 알림을 결정 -> 결합이 낮음
추천: 대부분의 경우 패턴 2를 기본으로 삼고, 자동 알림이 반드시 필요한 경우 shared/notifier를 사용할 수 있도록 옵션 제공.
상세 코드 제안 (AS-IS vs TO-BE) — 실제 PR에서 적용 가능한 예제
- adminMode 예제 (AS-IS: Jotai direct → TO-BE: Hook 캡슐화)
AS-IS:
// Header.tsx
import { useAtom } from 'jotai';
import { adminModeAtom } from '../../shared/store/adminModeAtom';
const [isAdmin, setIsAdmin] = useAtom(adminModeAtom);TO-BE:
// shared/store/useAdminMode.ts (public API)
import { useAtom } from 'jotai';
import { adminModeAtom } from './adminModeAtom';
export function useAdminMode() {
const [isAdmin, setIsAdmin] = useAtom(adminModeAtom);
const toggle = () => setIsAdmin(v => !v);
return { isAdmin, setIsAdmin, toggle };
}
// Header.tsx
import { useAdminMode } from '../../shared/store/useAdminMode';
const { isAdmin, toggle } = useAdminMode();- 장점: 내부 구현을 Jotai → Zustand 등으로 바꿀 때 Header는 손댈 필요 없음.
- useAddProduct 결합도 개선 (나쁜 예 vs 좋은 예)
나쁜 예 (결합도 높음)
// BAD: addNotification을 직접 파라미터로 받음 — 상위 레이어 용어(addNotification)을 고정해서 결합 발생
const useAddProduct = (addNotification) => {
const add = (p) => {
// 로직
addNotification('상품 추가됨');
};
return { add };
};좋은 예 (결합도 낮음)
// GOOD: 결과를 반환하거나 onSuccess/onError 콜백을 받음
const useAddProduct = ({ onSuccess, onError } = {}) => {
const add = async (p) => {
try {
// 비즈니스 로직
onSuccess?.();
return { ok: true };
} catch (e) {
onError?.(e);
return { ok: false, error: e };
}
};
return { add };
};
// 호출부(UI)
const { add } = useAddProduct({
onSuccess: () => notify({ message: '상품 추가됨' }),
onError: (e) => notify({ message: e.message, level: 'error' })
});- 또는 Promise 반환 방식:
const res = await add(product);
if(!res.ok) notify(...);둘 중 하나의 규칙(콜백 or 결과 반환)을 프로젝트 규칙으로 정하면 결합도를 낮출 수 있습니다.
- 도메인 패키지화 대비 public index 예시
AS-IS (prone to circular):
// domains/cart/index.ts
export * from './hooks/useCartAtom';
export * from './components/CartSidebar';
export * from './types';TO-BE (public API 만)
// domains/cart/index.ts (public)
export { CartSidebar } from './components/CartSidebar';
export type { CartItem, CartProduct } from './types';
export { useCart } from './hooks/useCart'; // useCart는 내부 구현을 숨긴다
// 내부 구현은 internal/ 아래에 두고 export하지 않음.- 빌드/패키징 시 index.ts만 패키지에 포함.
PR 파일별(변경사항) 상세 코멘트 & 개선 제안
-
App 통째 제거하고 app/ 폴더로 분리한 것 — 매우 좋음
- App.tsx의 모든 로직을 app/{components,pages}와 domains로 분리한 것은 SRP, 응집도 관점에서 매우 바람직.
- 개선 제안: app의 public index(App export)와 각 도메인의 public API(export 할 것) 문서(README) 작성 권장.
-
Jotai 의존 추가 (package.json, pnpm-lock 등) — 괜찮음
- 권장: 각 도메인에서 atom을 직접 export하지 말고 public hook에 캡슐화하세요.
- 권장: atomWithStorage를 사용하는 경우, serialization boundary를 명확히 하고 migration 전략(version)을 고려하세요.
-
Barrel(도메인/공유) 사용 & 순환 의존성 관리
- PR 본문에 이미 규칙을 만드신 점 좋습니다. 추가로 다음을 권장합니다:
- ESLint plugin(import/no-cycle) 또는 자체 module-boundary-check(예: dependency-cruiser) 적용.
- 도메인 간 의존은 interface/adapter로만 허용(예: domains/product가 domains/cart를 직접 import 하지 않음).
- PR 본문에 이미 규칙을 만드신 점 좋습니다. 추가로 다음을 권장합니다:
-
Notification 위치 관련
- 현재 shared에 분산돼 있음. 위에서 제안한 shared/notifications 폴더로 모으는 것을 추천.
- UI/서버/도메인 사이의 notifier contract(예: notify({message, level}))를 정의하고, 도메인은 이 contract만 호출하거나(권장하지 않음) 반환값 패턴으로 알림을 호출하도록 유도.
-
ESLint/Tooling/CI 변경
- eslint.config.js로 전환하신 것은 모던한 방식. 주의: 프로젝트 팀원들이 로컬에서 동일하게 작동하도록 README에 lint setup 문서화 필요.
- lefthook 사용: pre-commit 훅 설명을 README에 추가하세요.
응집도(정의 + 현재 코드가 규칙에 부합하는지)
정의(요청하신 규칙을 포함)
- 응집도란: "변경에 대한 파일/코드의 추가/수정/삭제 등이 얼마나 짧은 동선으로 일어나는가"와 "라이브러리로 떼어낼 때 매끄럽게 분리되는가"로 평가.
현재 상태 평가
- 장점(응집도 ↑)
- domains 폴더 구조로 비즈니스 관련 코드(컴포넌트/훅/스토어/유틸)가 모여 있음 — 특정 도메인을 변경할 때 동선이 짧아짐.
- 도메인별 store/ hooks/ components 분리로 이해하기 쉬움.
- 단점(응집도 저하 가능)
- Notification 관련 파일이 shared의 서로 다른 폴더에 흩어져 있어, Notification 관련 변경 시 동선이 길어질 수 있음.
- 배럴로 광범위하게 re-export 하면 내부 구현이 노출/의존되어 응집도 역효과 발생 가능.
결론: 대부분 응집도가 높아졌고 훌륭하지만 Notification과 같은 cross-cutting concern은 한 곳으로 묶어 응집도를 높이세요.
결합도(정의 + 현재 코드가 규칙에 부합하는지)
정의: 모듈 간 직접적인 구현 의존(함수명·구조·라이브러리 API에 대한 의존)이 적을수록 좋다. 인터페이스(훅 시그니처)를 통해 의존하면 결합도가 낮아짐.
현재 상태 평가
- 장점(결합도 ↓ 시도)
- app 레이어는 domains의 public 컴포넌트/훅만 사용하고, 세부 구현은 숨김(일부). Header가 useAdminAtom를 외부 hook으로 바꾼 점 등은 좋음.
- 문제점(개선 필요)
- 일부 파일에서 Jotai API(useAtom, useAtomValue)를 직접 사용하고 있음 → 외부 컴포넌트가 라이브러리에 결합될 가능성 있음.
- 배럴 사용 시 "어디서 왔는지" 추적이 힘들어져 의존 추론이 어려워짐.
권장 개선
- 모든 전역 상태 접근은 public hook(예: useCart(), useAdminMode())으로 캡슐화.
- 도메인 훅은 호출자에게 Promise/결과를 반환하거나 onSuccess/onError 인터페이스를 제공하고, addNotification 같은 구체적 이름은 인자로 받지 않도록 설계.
마무리 정리 — 우선순위 액션 아이템
-
public hook 캡슐화 규칙 적용
- 모든 domain atoms/스토어는 domain 내부에서만 접근하고, 외부는 useXxx public hook을 호출.
- 문서화: domain마다 PUBLIC_API.md 작성.
-
Notification 재구성
- shared/notifications 폴더로 묶기.
- notify(contract) 하나만 공유하거나, 도메인에서는 결과 반환 패턴을 사용.
-
배럴(Barrel) 규칙 강제화
- 도메인 레벨에서만 배럴 제공.
- 내부(impl) 파일은 export 하지 않음.
- ESLint 또는 dependency-cruiser로 순환 의존성 검사 추가.
-
상태관리 라이브러리 교체 대비
- 현재 Jotai 기반 구현을 public hook으로 감싸기만 하면 라이브러리 교체는 한 파일 또는 한 모듈 수준에서 가능.
-
패키징 준비
- 각 domain의 index.ts에 public API만 남기고 internal 파일을 숨기는 구조로 정리.
- 패키지화 시 포함/제외 목록 정리(package.json exports, .npmignore).
마지막으로 — PR 본문(셀프회고)에 대한 코멘트 & 질문 제안
잘 하셨습니다. 특히 도메인 중심, 배럴 규칙 수립, Jotai 학습/도입, Props 제거 과정을 체계적으로 진행하신 점이 인상적입니다. 다음 질문을 스스로/팀과 토론해보세요:
- domain의 public API는 어떤 규칙으로 선정하나? (컴포넌트/훅/타입/유틸 중 무엇을 공개할지)
- Notification은 shared로 두되, 도메인에서 UI를 트리거해야 하나? 아니면 호출자(컨테이너)에서 제어해야 하나?
- 배럴 사용 시 “개발 단계(명확 경로 사용) vs 배포 단계(배럴 사용)” 워크플로를 팀 규칙으로 정할 것인가?
원하시면 아래 작업을 실제 코드로 만들어 드립니다:
- useAdminMode / useCart public hook 리팩토링 패치 예시
- Notification shared 모듈화(구조 + 간단 구현)
- ESLint rule / dependency-cruiser 설정 예시로 순환 의존 검사 추가
원하시는 것을 알려주세요. 다시 한번 잘 정리하신 PR 축하드리고, 아주 좋은 방향으로 진행되고 있습니다!
|
|
||
| export const couponNotificationService = { |
There was a problem hiding this comment.
쿠폰 관련 알림을 서비스로 빼신게 너무 인상적이에요 보기에 너무 깔끔합니다,, 🤩
| import { tv } from "tailwind-variants"; | ||
|
|
||
| const paymentInfoLine = tv({ |
There was a problem hiding this comment.
찬규님 코드 덕에 tv라는 헬퍼 함수를 처음 알게 되었네요!! 감사합니다 👏
| <main className="mx-auto max-w-7xl px-4 py-8"> | ||
| {isAdminMode ? <AdminPage /> : <CartPage />} | ||
| </main> | ||
| <NotificationList notifications={notifications} onRemove={removeNotification} /> |
There was a problem hiding this comment.
혹시 notifications, removeNotification 를 App 에서 만들고 주입한 이유가 있나요?
그냥 NotificationList 안에서 만들면 어떨까 싶어서유
| <nav className="-mb-px flex space-x-8"> | ||
| <button | ||
| onClick={handleProductsTabClick} | ||
| className={tabButton({ active: activeTab === "products" })} |
There was a problem hiding this comment.
소소하게 아래처럼 해도 좋았을 것 같아유
className={tabButton({ activeTab: "products" })}
|
|
||
| if (value === "" || /^\d+$/.test(value)) { |
There was a problem hiding this comment.
요렇게 가독성을 좀 더 높여봐도 좋을 것 같습니다.
| if (value === "" || /^\d+$/.test(value)) { | |
| const isNumbericString = /^\d+$/.test; | |
| if (value === "" || isNumberString(value)) { |
|
저번 주차 고생하셨습니다, 이번 주차도 화이팅 |
과제의 핵심취지
과제에서 꼭 알아가길 바라는 점
기본과제
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는 잘 제거했나요?
전체적으로 분리와 재조립이 더 수월해진 결합도가 낮아진 코드가 되었나요?
배포 링크
https://chan9yu.github.io/front_6th_chapter2-2/
과제 셀프회고
지난주 과제에서 아쉬웠던 점을 바탕으로, 이번 주는 먼저 리팩토링 계획을 세운 뒤 순차적으로 문제를 해결하는 방식으로 과제를 진행했습니다!
Setp 1. 모놀리식 분해 시작
Setp 2. 도메인 중심 아키텍처 구축
Setp 3. Props Drilling 문제 인식
Setp 4. Advanced - 전역 상태 관리 전환
Setp 5: Advanced - 점진적 Props Drilling 제거
Setp 6. Advanced - Hook 아키텍처 정립
Setp 7. Advanced - 구조 최적화
Setp 8. Advanced - 배포 준비
과제를 하면서 내가 알게된 점, 좋았던 점은 무엇인가요?
폴더구조
폴더 구조에 특히 신경을 기울였으며, 파일의 위치와 계층 구조가 높은 응집도를 가질 수 있도록 하는 관점에서 작업을 진행했습니다.
도메인 중심 vs 기술 중심 구조 분리
Barrel Export 패턴 사용
각 폴더마다 index.ts를 두어서 깔끔한 import 경로를 만들 수 있었습니다.
배럴파일 사용하면서 고민했던 점
순환 의존성 문제와의 씨름
Import 지옥 vs 명확성의 트레이드오프
도메인별 배럴파일 vs 기능별 배럴파일
결론적으로 제가 나름 정한 배럴파일 사용 원칙
이런 고민들을 통해 깔끔한 import 경로와 명확한 모듈 경계를 동시에 얻을 수 있었습니다!
Jotai
atom과atomWithStorage의 간결함과 직광성이 좋아서 놀랐습니다 Redux보다 훨씬 간단하면서도 TypeScript와의 호환성이 뛰어났습니다atom((get) => ...)형태로 자연스럽게 표현할 수 있어서 코드가 매우 깔끔해졌습니다Props Drilling 제거 효과
도메인 중심 설계
이번 과제에서 내가 제일 신경 쓴 부분은 무엇인가요?
전역 상태와 로컬 상태의 경계? 정하기
useProductForm같은 로컬 훅으로 분리했습니다기존 테스트가 깨지지 않도록 주의
이번 과제를 통해 앞으로 해보고 싶은게 있다면 알려주세요!
Jotai의 고급 기능
atomFamily나selectAtom같은 기능들도 사용하고 싶습니다 (조타이는 이번에 처음이라서..)더 복잡한 상태 관리 시나리오
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문 편하게 남겨주세요 :)
파일 분리를 하면서 제일 고민이 되었던 부분은
Notification를 어떻게 하냐 였는데요Notification에 대한 로직들을 domain/notification 폴더로 만들어 관리를 하니 나름 응집도 레벨도 올라가고 보기좋았지만 Notification이 domain에 들어가야되나..? 라는 고민을 하게되었습니다. 결국 현재는 shared안에 componenet, hooks, types,
state ... 이런식으로 분리가되어 버렸는데 Notification을 어떻게 구조를 가져가야될 고민입니다!
첫 번째 시도: domains/notification/ (잘못된 접근)
처음에는 "응집도 높이기"라는 명목으로 notification 관련 모든 것을 한 곳에 모았습니다. 하지만 곧 근본적인 문제를 발견했어요:
"Cart, Coupon, Product vs Notification의 본질적 차이"
이때 깨달은 건, **"응집도가 높다고 모두 같은 곳에 두는 게 아니다"**라는 점이었습니다.
두 번째 시도: shared/notifications/ (구조적 불일치)
이번엔 shared로 옮겼지만 또 다른 문제가 생겼습니다. shared 영역은 타입별로 분류해야 하는데 기능별로 분류한 거죠. 나중에 Button, Modal, Toast 같은 다른 공통 컴포넌트가 추가되면 일관성이 깨질 거라 생각했어요.
최종 결론: shared/ 내 타입별 분류
현재 구조에 대한 의문과 대안 고민
현재 구조는
같은 의도로 만들어지긴 했습니다.
하지만 여전히 남은 의문:
궁금한 점들