chan9yu 님의 상세페이지[5팀 여찬규] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍

과제의 핵심취지

  • React의 hook 이해하기
  • 함수형 프로그래밍에 대한 이해
  • 액션과 순수함수의 분리

과제에서 꼭 알아가길 바라는 점

  • 엔티티를 다루는 상태와 그렇지 않은 상태 - cart, isCartFull vs isShowPopup
  • 엔티티를 다루는 컴포넌트와 훅 - CartItemView, useCart(), useProduct()
  • 엔티티를 다루지 않는 컴포넌트와 훅 - Button, useRoute, useEvent 등
  • 엔티티를 다루는 함수와 그렇지 않은 함수 - calculateCartTotal(cart) vs capaitalize(str)

기본과제

  • 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. 모놀리식 분해 시작

  • 거대한 App.tsx 분석: 400줄이 넘는 파일의 모든 로직이 집중된 상태
  • 페이지 분리: AdminPage, CartPage로 UI 관심사 분리
  • 컴포넌트 추출: Header, 각종 UI 컴포넌트들 분리

Setp 2. 도메인 중심 아키텍처 구축

  • Cart 도메인 분리: domains/cart/ 생성, 계산 로직과 타입 분리
  • Coupon 도메인 분리: domains/coupon/ 생성, 유효성 검사 로직 독립화
  • Product 도메인 분리: domains/product/ 생성, CRUD 로직 체계화
  • Shared 영역 정리: domains/notification/ → shared/ 이동 (관심사 분리)

Setp 3. Props Drilling 문제 인식

  • Props 폭증 발견: 컴포넌트 분리하면서 props가 12개, 10개씩 증가
  • 유지보수성 저하: 새 기능 추가 시 여러 컴포넌트 수정 필요
  • 재사용성 부족: props 의존성으로 인한 컴포넌트 결합도 증가

Setp 4. Advanced - 전역 상태 관리 전환

  • Jotai 도입: Context API 대신 더 가벼운 Jotai 선택
  • Atom 설계: atomWithStorage로 localStorage 동기화, 파생 상태 구현
  • 도메인별 Store: domains/*/store/atoms.ts 구조로 도메인 독립성 확보

Setp 5: Advanced - 점진적 Props Drilling 제거

  • 각종 컴포넌트 10개 정도되는 props → 0개 props
  • 컴포넌트 자립화: 각 컴포넌트가 필요한 상태만 직접 구독

Setp 6. Advanced - Hook 아키텍처 정립

  • 전역 상태 Hook: useCartAtom, useProductAtom, useCouponAtom
  • 로컬 상태 Hook: useProductForm, useCouponForm (폼 전용)
  • 역할 분리: 전역 비즈니스 데이터 vs 로컬 UI 상태 명확히 구분

Setp 7. Advanced - 구조 최적화

  • 불필요한 추상화 제거: useDebounceSearchAtom, useAdminModeAtom 등 정리
  • Import 경로 정리: 도메인별 barrel export 패턴 적용
  • Hook 위치 재조정: store/hooks/ → hooks/ 폴더로 이관

Setp 8. Advanced - 배포 준비

  • 빌드 시스템 최적화: pnpm build:advanced로 선택적 빌드
  • Vite 설정 통합: 조건부 설정으로 구성
  • 파일명 표준화: index.advanced.html → index.html 자동 변환

과제를 하면서 내가 알게된 점, 좋았던 점은 무엇인가요?

폴더구조

폴더 구조에 특히 신경을 기울였으며, 파일의 위치와 계층 구조가 높은 응집도를 가질 수 있도록 하는 관점에서 작업을 진행했습니다.

도메인 중심 vs 기술 중심 구조 분리
  • 처음에는 모든 것을 기술별로 분류하려 했지만(components/, hooks/, utils/), 실제로는 도메인별 분류가 훨씬 직관적이고 유지보수하기 쉬웠습니다.
  • domains/cart/에서 장바구니 관련 모든 것(컴포넌트, 훅, 타입, 유틸)을 찾을 수 있어서 개발 속도가 빨라졌습니다.
  src/advanced/
  ├── domains/                    # 비즈니스 도메인 (기능별)
  │   ├── cart/
  │   │   ├── components/        # Cart 전용 컴포넌트
  │   │   ├── hooks/             # Cart 전용 훅
  │   │   ├── store/             # Cart 전용 상태
  │   │   ├── services/          # Cart 비즈니스 로직
  │   │   ├── types/             # Cart 타입
  │   │   └── utils/             # Cart 계산 함수
  │   ├── coupon/                # 쿠폰 도메인
  │   │   ├── components/
  │   │   ├── hooks/
  │   │   ├── store/
  │   │   └── ...
  │   └── product/               # 상품 도메인
  │       ├── components/
  │       ├── hooks/
  │       ├── store/
  │       └── ...
  ├── shared/                    # 공통 기능 (타입별)
  │   ├── components/           # 범용 UI 컴포넌트
  │   │   ├── ui/              # Button, Input 등
  │   │   └── icons/           # 아이콘들
  │   ├── hooks/               # 범용 훅
  │   ├── store/               # 전역 UI 상태
  │   ├── types/               # 공통 타입
  │   └── utils/               # 공통 유틸리티
  └── app/                      # 애플리케이션 레이어
      ├── components/          # 앱 레벨 컴포넌트
      ├── pages/               # 페이지 컴포넌트
      └── App.tsx              # 라우팅 & 전역 설정

Barrel Export 패턴 사용

각 폴더마다 index.ts를 두어서 깔끔한 import 경로를 만들 수 있었습니다.

// Before: 복잡한 경로들
import { CartItem } from "../domains/cart/types/entities";
import { useCartActions } from "../domains/cart/hooks/useCartActions";
import { calculateTotal } from "../domains/cart/utils/calculators";

// After: 깔끔한 단일 import
import { CartItem, useCartActions, calculateTotal } from "../domains/cart";

배럴파일 사용하면서 고민했던 점

순환 의존성 문제와의 씨름

  • 처음에는 모든 도메인에서 배럴파일을 무분별하게 사용하다가 순환 의존성 문제를 마주했습니다.
// domains/cart/index.ts에서
export * from "./hooks/useCartAtom";

// domains/cart/hooks/useCartAtom.ts에서
import { useCouponAtom } from "../../coupon"; // 순환 의존성 위험!
  • 이를 해결하기 위해 계층별 의존성 규칙을 정했습니다: domains/ → shared/ → app/ 순으로만 의존하도록 제한했습니다.
Import 지옥 vs 명확성의 트레이드오프
  • 배럴파일을 사용하면 import가 간결해지지만, 실제로 어느 파일에서 가져온 건지 추적하기 어려워집니다.
// 간결하지만 출처가 불명확
import { useCartAtom, CartItem, calculateCartTotal } from "../domains/cart";

// 장황하지만 명확한 출처
import { useCartAtom } from "../domains/cart/hooks/useCartAtom";
import { CartItem } from "../domains/cart/types/entities";
  • 개발 단계에서는 명확한 경로를, 완성 후에는 배럴파일을 사용하는 방식으로 절충했습니다.
도메인별 배럴파일 vs 기능별 배럴파일
  • 처음에는 각 하위 폴더마다 배럴파일을 만들었습니다
    domains/cart/
    ├── components/index.ts
    ├── hooks/index.ts
    ├── types/index.ts
    └── index.ts # 모든 것을 다시 re-export
    
  • 하지만 이는 과도한 중첩을 만들어서, 결국 도메인 레벨에서만 배럴파일을 유지하는 것으로 정리했습니다.
결론적으로 제가 나름 정한 배럴파일 사용 원칙
  1. 도메인 레벨에서만 사용 (하위 폴더마다 만들지 않음)
  2. 공개 API만 export (내부 구현체는 숨김)
  3. 순환 의존성 체크 필수

이런 고민들을 통해 깔끔한 import 경로와 명확한 모듈 경계를 동시에 얻을 수 있었습니다!

Jotai

  • atomatomWithStorage의 간결함과 직광성이 좋아서 놀랐습니다 Redux보다 훨씬 간단하면서도 TypeScript와의 호환성이 뛰어났습니다
  • 파생 상태를 atom((get) => ...) 형태로 자연스럽게 표현할 수 있어서 코드가 매우 깔끔해졌습니다

Props Drilling 제거 효과

  • 단순히 props 개수만 줄어든 것이 아니라, 컴포넌트가 훨씬 독립적이고 재사용 가능해졌습니다.
  • 새로운 기능을 추가할 때도 부모 컴포넌트를 건드릴 필요 없이 해당 컴포넌트에서 필요한 상태만 구독하면 되니 개발 속도가 빨라졌습니다.

도메인 중심 설계

  • 도메인별로 atom과 hook을 분리하니 코드의 응집도가 높아지고, 각 도메인의 로직을 이해하기 쉬워졌습니다.

이번 과제에서 내가 제일 신경 쓴 부분은 무엇인가요?

전역 상태와 로컬 상태의 경계? 정하기

  • 모든 상태를 전역으로 만들면 오히려 복잡해질 수 있어서, 정말 여러 컴포넌트에서 공유되는 상태만 전역으로 관리했습니다
  • 폼 상태 같이 특정 컴포넌트에서만 사용되는 것은 useProductForm 같은 로컬 훅으로 분리했습니다

기존 테스트가 깨지지 않도록 주의

  • 테스트가 모두 통과하도록 하면서 리팩토링을 진행하는 것이 가장 까다로웠던거 같습니다

이번 과제를 통해 앞으로 해보고 싶은게 있다면 알려주세요!

Jotai의 고급 기능

  • atomFamilyselectAtom 같은 기능들도 사용하고 싶습니다 (조타이는 이번에 처음이라서..)
  • Suspense와 함께 사용하는 async atom도 써보고싶어요

더 복잡한 상태 관리 시나리오

  • 현재는 단순한 CRUD이지만, 실시간 데이터 동기화나 낙관적 업데이트 같은 복잡한 시나리오에서 Jotai를 어떻게 활용할 수 있을지 궁금합니다.

리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문 편하게 남겨주세요 :)

파일 분리를 하면서 제일 고민이 되었던 부분은Notification를 어떻게 하냐 였는데요 Notification에 대한 로직들을 domain/notification 폴더로 만들어 관리를 하니 나름 응집도 레벨도 올라가고 보기좋았지만 Notification이 domain에 들어가야되나..? 라는 고민을 하게되었습니다. 결국 현재는 shared안에 componenet, hooks, types, state ... 이런식으로 분리가되어 버렸는데 Notification을 어떻게 구조를 가져가야될 고민입니다!

첫 번째 시도: domains/notification/ (잘못된 접근)

domains/notification/
├── components/
│ ├── NotificationList.tsx
│ └── index.ts
├── hooks/
│ ├── useNotifications.ts
│ └── index.ts
├── types/
│ ├── entities.ts
│ └── index.ts
├── constants/, models/, utils/ # 빈 디렉토리들
└── index.ts

처음에는 "응집도 높이기"라는 명목으로 notification 관련 모든 것을 한 곳에 모았습니다. 하지만 곧 근본적인 문제를 발견했어요:

"Cart, Coupon, Product vs Notification의 본질적 차이"

  • Cart/Coupon/Product: 비즈니스 도메인 전문가가 존재하는 실제 업무 영역
  • Notification: 모든 도메인에서 공통으로 사용하는 기술적/UI 관심사

이때 깨달은 건, **"응집도가 높다고 모두 같은 곳에 두는 게 아니다"**라는 점이었습니다.

두 번째 시도: shared/notifications/ (구조적 불일치)

shared/notifications/ # ❌ 기능별 분류
├── components/
├── hooks/
└── types/

이번엔 shared로 옮겼지만 또 다른 문제가 생겼습니다. shared 영역은 타입별로 분류해야 하는데 기능별로 분류한 거죠. 나중에 Button, Modal, Toast 같은 다른 공통 컴포넌트가 추가되면 일관성이 깨질 거라 생각했어요.

최종 결론: shared/ 내 타입별 분류

shared/
├── components/
│ ├── ui/
│ │ ├── Notification.tsx # ✅ 다른 UI 컴포넌트와 함께
│ │ └── Button.tsx
│ └── NotificationList.tsx
├── hooks/
│ ├── useNotifications.ts # ✅ 다른 공통 훅들과 함께
│ └── useDebounceValue.ts
├── store/
│ └── notificationAtoms.ts # ✅ 전역 UI 상태
└── types/
└── notification.ts # ✅ 공통 타입들과 함께

현재 구조에 대한 의문과 대안 고민

현재 구조는

  • 타입별 분류로 일관성 확보
  • 다른 공통 기능들과 함께 관리되어 찾기 쉬움
  • 확장성 좋음 (Toast, Modal 등 추가 시)

같은 의도로 만들어지긴 했습니다.

하지만 여전히 남은 의문:

  1. 흩어져 있는 느낌: Notification 관련 파일들이 4군데(components, hooks, store, types)로 분산되어 있어서 처음 보는 사람이 전체 그림을 파악하기 어려울 수 있어요.
  2. 중간 지점은 없을까: 완전히 흩어트리지도, 무리하게 묶지도 않는 절충안이 있을지 궁금합니다.

궁금한 점들

  1. 현재의 타입별 분류가 맞는 접근일까요? 아니면 notification처럼 관련성이 높은 기능들은 어느 정도 묶어두는 게 나을까요?
  2. "공통 기능"의 경계를 어떻게 정의해야 할까요? Toast, Modal 같은 것들도 notification과 비슷한 딜레마에 빠질 것 같아요.

과제 피드백

안녕하세요 찬규님! 5주차 과제 잘 진행해주셨네요 ㅎㅎ 고생하셨습니다!!

배럴파일 사용하면서 고민했던 점: 순환 의존성 문제와의 씨름 처음에는 모든 도메인에서 배럴파일을 무분별하게 사용하다가 순환 의존성 문제를 마주했습니다.

맞아요. 배럴파일을 사용할 때 이런 문제를 마주할 수 있답니다 ㅎㅎ

https://www.npmjs.com/package/madge

이런 라이브러리를 사용하면 아예 순환의존성을 검사할 수 있답니다!

현재의 타입별 분류가 맞는 접근일까요? 아니면 notification처럼 관련성이 높은 기능들은 어느 정도 묶어두는 게 나을까요?

shared/ui/notification

이런 폴더를 하나 만들어서 다 몰아넣으면 어떨까 싶어요 ㅎㅎ

아니면 아예 components/Notification 에 몰아넣는다거나?

결국 공용 컴포넌트로 쓰이는거니까요!

"공통 기능"의 경계를 어떻게 정의해야 할까요? Toast, Modal 같은 것들도 notification과 비슷한 딜레마에 빠질 것 같아요.

저라면 아마

components/Toast/types.ts components/Toast/hooks/use....ts components/Toast/utils/... components/Toast/ToastProvider components/Toast/Toast

이런식으로 컴포넌트 영역에 분리시켜놓을 것 같아요! 그 다음에 componund 방식으로 묶어서 사용하는거죠


이번주 멘토링 시간이 스쳐지나가네요. 꼭 현재 상황 잘 해결했으면 좋겠어요. 응원합니다 찬규님!! 화이팅!!