minjaeleee 님의 상세페이지[6팀 이민재] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍

배포링크

https://minjaeleee.github.io/front_6th_chapter2-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는 분리되어 있나요?

  • 데이터 흐름에 맞는 계층구조를 이루고 의존성이 맞게 작성이 되었나요?

심화과제

  • 재사용 가능한 Custom UI 컴포넌트를 만들어 보기

  • 재사용 가능한 Custom 라이브러리 Hook을 만들어 보기

  • 재사용 가능한 Custom 유틸 함수를 만들어 보기

  • 그래서 엔티티와는 어떤 다른 계층적 특징을 가지는지 이해하기

  • UI 컴포넌트 계층과 엔티티 컴포넌트의 계층의 성격이 다르다는 것을 이해하고 적용했는가?

  • 엔티티 Hook과 라이브러리 훅과의 계층의 성격이 다르다는 것을 이해하고 적용했는가?

  • 엔티티 순수함수와 유틸리티 함수의 계층의 성격이 다르다는 것을 이해하고 적용했는가?

과제 셀프회고

과제를 하면서 내가 제일 신경 쓴 부분은 무엇인가요?

사실, 이런 계층 분리와 설계화를 해본 적이 없었고 프로젝트 규모에 맞는 적절한 설계라는 것이 정말 힘들었습니다. 고민에 고민을 반복하다가 기존 코드의 문제점을 분석하고, 과제의 요구사항 위주로 먼저 설계하자고 생각했습니다.

기존 코드의 문제점

  1. 단일 책임 원칙 위반: 상품관리, 장바구니, 쿠폰, 알림, 검색 등 모든 기능이 한 컴포넌트에 집중
  2. 관심사 분리 부족: UI 로직과 비즈니스 로직이 섞여있음
  3. 상태 관리 복잡성: 10개 이상의 서로 다른 상태가 한 곳에서 관리됨
  4. 재사용성 부족: 모든 로직이 컴포넌트에 강결합
  5. 테스트 어려움: 거대한 컴포넌트로 인한 테스트 복잡성

과제의 요구사항

엔티티를 분류 기준으로 해서 주요 책임 계층을 나눌 것.

따라서, 엔티티와 순수 함수 분리를 기준으로 구조화하자라고 목표를 세웠습니다.

시행착오

  • 처음엔 냅다 도메인별로 분리해봤고, 아래와 같은 구조였습니다.

    src/basic/
    ├── models/          # 도메인 타입 정의
    │   ├── product.ts   # 상품 도메인
    │   ├── cart.ts      # 장바구니 도메인  
    │   ├── coupon.ts    # 쿠폰 도메인
    │   └── notification.ts # 알림 도메인
    ├── hooks/           # 비즈니스 로직 훅
    │   ├── useCart.ts   # 장바구니 로직
    │   ├── useProducts.ts # 상품 관리 로직
    │   ├── useCoupons.ts  # 쿠폰 로직
    │   └── useNotifications.ts # 알림 로직
    ├── components/      # 컴포넌트 계층 분리
    │   ├── features/    # 기능별 중간 레벨
    │   │   ├── Header/
    │   │   ├── ProductList/
    │   │   ├── CartSidebar/
    │   │   └── AdminPanel/
    │   └── ui/         # 재사용 가능한 UI 컴포넌트
    ├── utils/          # 순수 함수들
    │   ├── calculations.ts # 계산 로직
    │   ├── formatters.ts   # 포맷팅
    │   └── validators.ts   # 검증 로직
    └── constants/      # 상수 데이터
        ├── products.ts
        └── coupons.ts
    
  • 도메인 별로 분리하다 보니 아래와 같은 단점이 생겼습니다.

    1. 혼재로 인한 모호성
      • hooks 내부에서 상태 관리, 비즈니스 로직, 데이터 접근 모두 혼재되어 많은 책임이 있어서 엔티티 기준으로 분리하고 다른 비즈니스 로직들을 순수 함수로 분리하는데 구조상 어려움이 있었습니다.
    2. 관련 코드 분산
      • 하나의 엔티티코드를 수정하려면 ui 컴포넌트, feature 컴포넌트, utils, constants … 여러 폴더를 뒤져야하는 어려움이 있었습니다.
    3. 재사용성 판단의 어려움
      • “utils 내부에 있는 함수를 보고 어떤 도메인에 특화된 로직인지 찾을 수 있을까?” 하는 생각이 들었습니다. 폴더 계층이 없다보니, 어디에서 사용되는 util 함수인지 알 수가 없으니까 사용 여부를 판단하는데 제약이 있습니다.

엔티티와 순수 함수 분리를 기준으로 다시 한번 구조화

폴더 구조

basic/
├── domain/                    # 엔티티 관련 코드
│   ├── product/
│   │   ├── models/           # Product, Discount, ProductWithUI
│   │   ├── hooks/            # useProducts
│   │   ├── components/       # ProductList, ProductCard
│   │   └── utils/            # calculateProductDiscount, calculateItemPrice
│   ├── cart/
│   │   ├── models/           # CartItem
│   │   ├── hooks/            # useCart
│   │   ├── components/       # CartSidebar
│   │   └── utils/            # cart 관련 계산 함수들
│   ├── coupon/
│   │   ├── models/           # Coupon
│   │   ├── hooks/            # useCoupons
│   │   └── components/       # 쿠폰 관련 컴포넌트
│   └── notification/
│       ├── models/           
│       ├── hooks
│       └── components/       
├── shared/                   # 공통 유틸리티
│   ├── hooks/               # useLocalStorage, useSearch* 
│   ├── components/          # Header, SearchInput, Button│   ├── utils/               # formatters, validators (순수 함수)
│   └── constants/           # 범용 상수들
└── features/                # 페이지/기능별 조합
    ├── admin/
    └── shop/

이렇게 의도한대로 엔티티와 순수 함수 분리를 기준으로 구조화 할 수 있었습니다.

오직 계산만 하는 로직, 오직 상태 관리만 하는 로직, UI 관리만 하는 로직 등 단일 책임 원칙을 준수하며 순수 함수를 분리할 수 있었고 명확한 책임의 분리가 가능해졌습니다.

또, 응집된 코드를 재배치하면서 재사용성이 높아졌습니다.

과제를 다시 해보면 더 잘 할 수 있었겠다 아쉬운 점이 있다면 무엇인가요?

  1. 높은 결합도
    • 메인 App.tsx에서 custom hooks들을 모두 인자로 주입하며 관리를 해서 엔티티 간의 결합도가 높은 상태였습니다. 사실, 심화과제를 완성하지 못한 것도 이 높은 결합도 때문입니다. 이것이 바로 props drilling의 핵심 단점인데.. 몸소 뼈저리게 깨달았습니다.
  2. 상태관리 일관성 부족
    • jotai를 통해 전역 상태를 관리하려고 했으나 높은 결합도 때문에 완성하지 못했습니다. 결국엔 일부는 jotai의 store를 공유하고 있지만, 일부는 내부에서 useState로 상태가 관리가 됩니다. 그래서 상태는 뒤죽박죽이 되어버렸습니다.

내부적으로는 많은 문제가 있었지만 결국에는 해결하지 못했습니다. basic에 많은 시간을 쏟아 부었는데 제가 생각했던 “엔티티와 순수 함수 분리를 기준” 목표로 잘게 쪼개는 것보다, 엔티티를 기준으로 분리한 다음에 전역 상태로 관리할 수 있게 데이터를 단방향으로 흐르게 하여 advanced에서 필요한 범위 내에서 상태가 공유될 수 있도록 store를 관리하고, 이후 순수 함수를 쪼개면서 분리를 했으면 더 수월했을 것 같습니다.

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

리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문

  1. 구조 설계 우선순위
    • 프로젝트 초기에 아키텍처를 설계하시거나, 프로젝트 규모에 따라서 다시 재설계를 하실때 어떤 순서로 접근하시는지 궁금합니다!
    • 만약에 프로젝트 규모에 따라서 재설계를 한다면 점진적으로 개선하는 전략이 있으신지 궁금합니다!
  2. 컴포넌트 책임 분리
    • Feature 컴포넌트(ProductList, CartSidebar)의 적절한 책임범위는 어디까지일까요?? UI만 담당해야 할까요?? 아니면 해당 도메인의 비즈니스 로직까지 포함해도 될까요??

과제 피드백

수고했어요! 이번 과제는 결합도를 낮추는 방법을 이해하고 코드에서 제어하기 쉬운 순수함수를 분리하고 대형 컴포넌트들을 관리가능한 수준의 크기로 분리를 해보는 과제였습니다. 시행착오의 과정에서는 폴더구조에 대한 고민이 많았었군요. 잘했습니다.

왜 이렇게 폴더구조를 만드는가? 생각을 해보면 결국 코드에 하나의 책임만 두는게 이해하기가 더 쉽다는 것을 느낄수있고 단일 책임이라는 것을 무슨 기준으로 분리해낼까 고민하다보면서 순수함수, 엔티티, 파생 데이터, UI 로직 유틸등의 체계가 생겨났지요.

하지만 결국 분리를 하더라도 하나의 서비스가 되기 위해서는 서로에게 필요한 의존성등을 정돈하고 단방향의 데이터 흐름을 유지하도록 만들어야 하는데 지금의 코드에서는 분리한 체계가 일관성이 없어서 결합도가 완전히 낮아지는 방향이 되지는 못했습니다.

추상화의 수준을 어떻게 맞출수 있을지? 무엇은 props로 빼어서 재사용과 유연함을 보강하고 무엇은 내재화해서 함께 관리하는게 좋을지라는 측면을 한번 잘 고민해보시면 좋겠네요.

Q) 프로젝트 초기에 아키텍처를 설계하시거나, 프로젝트 규모에 따라서 다시 재설계를 하실때 어떤 순서로 접근하시는지 궁금합니다! 만약에 프로젝트 규모에 따라서 재설계를 한다면 점진적으로 개선하는 전략이 있으신지 궁금합니다!

=> 가급적 코드에서 관리하기 어렵지 않은 부분과 관리하기 어려운 부분들을 분리하는게 중요합니다. 관리가 어려운 부분은 상태이고 상태란 현재의 값이 보관되고 수정이 되는 과정에서 값의 변경이 계속 있기 때문에 어려워지는 것이죠.

반면 계산이나 데이터 UI 등은 상태가 아니기에 관리하고 테스트하기에 수월한 코드입니다. 이러한 코드들을 분리해두면 거대한 프로젝트라도 실제 복잡도를 관리해야하는 코드의 양이 줄어드는 것이죠.

이렇게 분리를 잘 해두고나면 개발하는 과정에서 자주 쓰이는 것들끼리 서로 관련있는 것들끼리 조금씩 모아가는 과정을 만들게됩니다. 이렇게 컨벤션이 잡히게 되면 다음번에는 알아서 그 컨벤션에 따라 관리를 하다가 새로운 레이어나 모듈의 영역이 발견이 되게 되면 또 분리를 진행하는 식으로 진행하게 됩니다.

Q) Feature 컴포넌트(ProductList, CartSidebar)의 적절한 책임범위는 어디까지일까요?? UI만 담당해야 할까요?? 아니면 해당 도메인의 비즈니스 로직까지 포함해도 될까요??

=> features는 하나의 기능이 되기 위해 필요한 모든 범위를 말합니다. 가령 zep에서는 화상회의, 채팅, 캐릭터, 지도등의 큰 기능들이 있죠. 화상회의를 하기 위해서는 라이브러리, UI, 행동, 데이터 모두가 필요합니다. 하지만 이는 features로 분리해두면 관리하기가 훨씬 좋겠죠. 아마 캐릭터이동이라는 기능과 겹치는 부분이 별로 없을테니까요.

=> 그렇다고 feature가 이렇게 커야하는건 아니구요. 적절한 책임 범위란 이들을 레이어나 역할로 펼쳐두는 것 보다 모아두는게 더 낫다고 판단되는 경우입니다. 절대적인 기준은 없고 프로젝트의 논리와 사람들이 프로젝트에 대해서 어떠한 추상적인 표현들을 쓰는가에 따른 멘탈 모델이므로 그에 따라 적절히 사용하면 됩니다.

=> 잘 했는지 여부는 굳이 features로 나눴더니 더 불편해졌다면 잘 못된 방식이고 이로 인해 생각하기가 더 편해졌다면 좋은 방식이라는걸로 기억해주세요.

수고하셨습니다. 6주차 과제도 화이팅입니다.