[4팀 안소은] Chapter3-2. 디자인 패턴과 함수형 프로그래밍 그리고 상태 관리 설계 #22
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
과제의 핵심취지
과제에서 꼭 알아가길 바라는 점
기본과제
Component에서 비즈니스 로직을 분리하기
비즈니스 로직에서 특정 엔티티만 다루는 계산을 분리하기
뷰데이터와 엔티티데이터의 분리에 대한 이해
entities -> features -> UI 계층에 대한 이해
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는 잘 제거했나요?
전체적으로 분리와 재조립이 더 수월해진 결합도가 낮아진 코드가 되었나요?
과제 셀프회고
배포 링크
설계 관점에서 중점적으로 진행한 부분
이번 과제에서 가장 중점적으로 신경 쓴 부분은 각 도메인에 대한 서비스 객체를 제공하여 가독성과 사용성 측면에서 이점을 누리고자 한 설계입니다. Context를 통해 단순히 데이터와 함수를 제공하는 것이 아니라, 각 엔티티가 자신의 동작을 메서드로 가지는 인스턴스 객체를 제공하는 방식으로 구현했습니다.
서비스 객체 패턴 적용 전후 비교
1. 장바구니 아이템 수량 업데이트
적용 전:
적용 후 (서비스 객체 패턴):
가독성 향상 효과:
item.updateQuantity()를 보면 "이 아이템의 수량을 업데이트한다"는 의미가 메서드 이름에 그대로 드러납니다.updateCartItemQuantity(cart, item.product.id, ...)처럼 여러 인자를 전달해야 하고, "어떤 장바구니의 어떤 아이템을" 수정하는지 파악하기 위해 인자를 모두 읽어야 합니다.item이라는 주체가 명확하므로, 메서드만 봐도 "이 아이템에 대한 동작"임을 즉시 알 수 있습니다.2. 상품 가격 포맷팅
적용 전:
적용 후 (서비스 객체 패턴):
가독성 향상 효과:
product.priceLabel()은 "이 상품의 가격 레이블을 가져온다"는 의미가 직관적입니다.formatProductPrice(product, ...)처럼 상품을 인자로 전달해야 하지만, 서비스 객체 패턴에서는 상품이 이미 주체이므로 메서드 호출만으로 충분합니다.product.priceLabel()은 "상품이 자신의 가격을 포맷팅한다"는 자연스러운 표현이 되어, 코드를 읽는 사람이 "상품이 가격 정보를 제공한다"는 도메인 개념을 바로 이해할 수 있습니다.3. 장바구니 아이템 삭제
적용 전:
적용 후 (서비스 객체 패턴):
가독성 향상 효과:
item.delete()는 "이 아이템을 삭제한다"는 의미가 메서드 이름에 완벽하게 드러납니다.deleteCartItem(cart, item.product.id)처럼 "어떤 장바구니에서 어떤 아이템을" 삭제하는지 명시해야 하지만, 서비스 객체 패턴에서는item이 이미 자신의 컨텍스트를 알고 있으므로 메서드 호출만으로 충분합니다.onDelete: () => item.delete()같은 코드를 보면, "이 아이템을 삭제하는 핸들러"라는 의미가 한눈에 들어옵니다.4. 상품 수정
적용 전:
적용 후 (서비스 객체 패턴):
가독성 향상 효과:
product.update(updates)는 "이 상품을 업데이트한다"는 의미가 명확합니다.설계 의도와 효과
이러한 서비스 객체 패턴을 적용한 이유는 다음과 같습니다:
의도 명확성:
item.updateQuantity(5)를 보면 "이 아이템의 수량을 5로 업데이트한다"는 의도가 메서드 이름에 그대로 드러납니다. 반면 함수형 접근인updateCartItemQuantity(cart, itemId, 5)는 여러 인자를 읽어야 의도를 파악할 수 있습니다.컨텍스트 내재화: 각 인스턴스가 자신의 데이터와 동작을 함께 가지고 있어, 외부에서 컨텍스트를 전달할 필요가 없습니다. 예를 들어
item.delete()는item이 이미 자신이 어떤 장바구니에 속해있는지 알고 있으므로, 별도로 장바구니나 아이템 ID를 전달할 필요가 없습니다.도메인 모델과의 일치: "장바구니 아이템이 자신의 수량을 업데이트한다"는 도메인 개념이 코드에서
item.updateQuantity()로 자연스럽게 표현되어, 코드를 읽는 사람이 비즈니스 로직을 이해하기 쉬워집니다.코드 간결성: 함수형 접근에서는 여러 인자를 전달해야 하지만, 서비스 객체 패턴에서는 메서드 호출만으로 충분하여 코드가 간결해집니다.
객체지향과 함수형 프로그래밍의 멀티패러다임 접근
서비스 객체 패턴을 적용하면서도, 내부적으로는 순수 함수를 적극적으로 활용하여 두 패러다임의 장점을 모두 가져가고자 했습니다.
순수 함수와 비순수 함수의 구분 기준
코드를 작성할 때 다음과 같은 기준으로 순수 함수와 비순수 함수를 구분했습니다:
순수 함수 (Pure Functions): 계산 로직, 변환 로직
calculateItemTotalPrice,applyCouponToTotalPrice,getMaxApplicableDiscount,getRemainingStock,hasBulkPurchase비순수 함수 (Impure Functions): 상태 변경 로직
item.updateQuantity(),item.delete(),product.update()구체적인 구현 예시
멀티패러다임 접근의 이점
테스트 용이성: 순수 함수는 독립적으로 테스트하기 쉽습니다.
반면
item.updateQuantity()같은 메서드는 React 상태 관리와 결합되어 있어 테스트가 복잡하지만, 내부에서 사용하는 순수 함수들은 쉽게 테스트할 수 있습니다.재사용성: 순수 함수는 다양한 컨텍스트에서 재사용할 수 있습니다.
가독성과 유지보수성의 균형:
item.updateQuantity(5)처럼 객체지향의 이점을 누립니다.calculateItemTotalPrice(item, bulkPurchase)처럼 순수 함수를 사용하여 로직이 명확하고 테스트하기 쉽습니다.책임 분리:
디버깅 용이성: 순수 함수는 입력과 출력이 명확하므로, 문제가 발생했을 때 어느 단계에서 문제가 생겼는지 추적하기 쉽습니다. 예를 들어
totalPrice가 잘못 계산되었다면,calculateItemTotalPrice함수만 확인하면 됩니다.설계 철학
이러한 멀티패러다임 접근은 다음과 같은 철학을 따릅니다:
이렇게 하면 객체지향의 가독성과 함수형의 테스트 용이성, 재사용성을 모두 가져갈 수 있습니다.
과제를 하면서 내가 알게된 점, 좋았던 점은 무엇인가요?
멀티패러다임 프로그래밍의 가독성 효과: 함수형, 객체지향, 선언적 프로그래밍을 적절히 조합하여 코드 가독성을 크게 향상시킬 수 있었습니다.
calculateItemTotalPrice,applyCouponToTotalPrice,getMaxApplicableDiscount같은 계산 로직을 순수 함수로 분리하니, 입력과 출력이 명확해서 함수만 봐도 "무엇을 하는지" 즉시 이해할 수 있었습니다. 사이드 이펙트가 없어서 테스트하기도 쉽고, 함수 이름만 봐도 역할을 파악할 수 있어 코드를 읽는 시간이 단축되었습니다.CartItemInstance,ProductItemInstance처럼 관련 데이터와 메서드를 하나의 객체로 묶으니, "장바구니 아이템을 수정한다"는 개념이item.updateQuantity(),item.delete()같은 메서드로 자연스럽게 표현되어 코드의 의도가 명확해졌습니다. 특히item.updateQuantity()를 호출하는 코드를 보면 "이 아이템의 수량을 업데이트한다"는 의미가 바로 전달되어, 여러 파일을 오가며 로직을 추적할 필요가 없어졌습니다.<CartItemListSection items={cartItems} />를 보면 "장바구니 아이템 목록을 보여준다"는 의도가 바로 드러나, 복잡한 DOM 조작 로직을 읽을 필요가 없어졌습니다.Hook의 책임 분리: 컴포넌트에서 비즈니스 로직을 분리하여 hook으로 옮기니 컴포넌트가 훨씬 깔끔해지고 테스트하기 쉬워졌습니다. 특히 상태를 관리하는
useProductForm,useCouponForm같은 hook들은 폼 관련 로직이 한 곳에 모여있어 유지보수가 편했습니다.Context를 통한 전역 상태 관리: Context API를 사용하여
CartContext,ProductsContext,CouponsContext를 만들면서 props drilling 문제를 해결했습니다. 각 도메인별로 Context를 분리하여 관심사 분리도 잘 되었고, 컴포넌트 트리에서 어디서든 필요한 상태에 접근할 수 있어 코드가 간결해졌습니다.도메인별 폴더 구조:
domains/cart,domains/products,domains/coupon으로 도메인별로 폴더를 나누니 코드를 찾기 쉽고 유지보수가 편해졌습니다. 각 도메인의 Context, utils, hooks가 한 곳에 모여있어 관련 코드를 빠르게 찾을 수 있었습니다.이번 과제에서 내가 제일 신경 쓴 부분은 무엇인가요?
서비스 객체 패턴을 통한 가독성 향상: 각 도메인 엔티티(CartItem, Product 등)가 자신의 동작을 메서드로 가지는 인스턴스 객체를 제공하여, 코드의 의도를 명확하게 표현하고자 했습니다.
item.updateQuantity(),item.delete(),product.priceLabel()같은 메서드는 "이 엔티티가 자신의 동작을 수행한다"는 의미가 메서드 이름에 그대로 드러나, 함수형 접근(updateCartItemQuantity(cart, itemId, ...))보다 훨씬 읽기 쉽고 이해하기 쉬웠습니다. 특히 컴포넌트에서 사용할 때onDelete: () => item.delete()같은 코드는 "이 아이템을 삭제한다"는 의도가 한눈에 들어와, 여러 파일을 오가며 로직을 추적할 필요가 없어졌습니다.멀티패러다임 프로그래밍을 통한 가독성 향상:
calculateItemTotalPrice(item, hasBulkPurchase)같은 함수는 입력만 받아 결과를 반환하므로, 함수 시그니처만 봐도 "장바구니 아이템과 대량 구매 여부를 받아 총액을 계산한다"는 의미가 명확합니다. 함수 내부를 읽지 않아도 역할을 파악할 수 있어 가독성이 향상되었습니다.item.updateQuantity(5)를 보면 "이 아이템의 수량을 5로 업데이트한다"는 의미가 직관적으로 전달됩니다. 반면 함수형 스타일인updateCartItemQuantity(cart, itemId, 5)보다 더 자연스럽고 읽기 쉬웠습니다. 특히item.delete()같은 메서드는 "이 아이템을 삭제한다"는 의도가 메서드 이름에 그대로 드러나 코드를 읽는 사람이 즉시 이해할 수 있었습니다.<CartItemListSection items={cartItems} />처럼 컴포넌트를 선언적으로 사용하니, JSX만 봐도 "장바구니 아이템 목록을 보여준다"는 의도가 명확합니다. 복잡한 조건문이나 반복문을 읽을 필요 없이 구조를 한눈에 파악할 수 있어 가독성이 크게 향상되었습니다.Hook은 상태 관리가 있을 때만 사용: 초기에는 모든 로직을 hook으로 분리하려 했지만, 상태를 관리하지 않는 단순 함수 호출(
usePurchase,useCouponSelection등)은 hook으로 만들 필요가 없다는 것을 깨달았습니다. 상태를 관리하는useProductForm,useCouponForm만 hook으로 유지하니, hook의 역할이 명확해지고 코드가 더 간결해졌습니다.도메인 props vs UI props 구분: 도메인 컴포넌트(
CartItem,ProductCard)에는 도메인 관련 props만 남기고, Context를 통해 전역 상태를 직접 접근하여 불필요한 props drilling을 제거했습니다. 이렇게 하니 컴포넌트가 받는 props가 줄어들어 컴포넌트의 책임이 명확해졌습니다.계산 함수의 순수성: 모든 계산 함수들이 사이드 이펙트 없이 입력에 대해 항상 같은 결과를 반환하도록 작성했습니다. 이렇게 하니 함수를 독립적으로 테스트할 수 있고, 함수의 역할을 이해하기 쉬워졌습니다.
이번 과제를 통해 앞으로 해보고 싶은게 있다면 알려주세요!
함수형 프로그래밍에 대해 찾아보다보니 es-toolkit이나 lodash와 같은 유틸리티 라이브러리를 사용하면 의미가 더 명확한 코드를 더 간결하게 작성할 수 있을 것 같았습니다. 이러한 유틸리티 라이브러리에서 주로 사용되는 함수들에 대해 공부하고 실무에서 더 폭 넓게 활용해보고 싶습니다.
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문 편하게 남겨주세요 :)
멀티패러다임 프로그래밍의 적절성: 함수형(순수 함수), 객체지향(인스턴스 메서드), 선언적(React 컴포넌트) 패러다임을 함께 사용했는데, 각 패러다임을 언제 사용하는 것이 가장 적절한지 궁금합니다. 예를 들어 계산 로직은 순수 함수로, 엔티티의 동작은 인스턴스 메서드로 분리한 것이 가독성 측면에서 적절한 선택이었는지 의견이 궁금합니다. 특히, 각 도메인에 대한 훅이 service 객체를 제공함을 통해 객체지향의 이점을 가져와 사용하고자 하였는데 사용하는 입장에서 편리하다고 느껴지기는 하였으나 훅이 너무 거대하고 많은 역할을 하고 있는 것은 아닌 지 의문이 들기도 하였습니다.
Hook 분리 기준: 컴포넌트에서 hook으로 로직을 분리할 때, 상태 관리가 없는 단순 함수 호출은 hook으로 만들지 않는 것이 맞다고 생각합니다. 하지만 데이터 변환 로직(
cart.list.map(...)) 같은 경우는 컴포넌트에 두는 것이 맞는지, 아니면 별도의 함수로 분리하는 것이 맞는지 의견이 궁금합니다. 저는 우선 컴포넌트에 둔 경우가 많은데, 오히려 과도하게 분리하면 코드를 옮겨다니며 로직을 파악하는 게 어려워질 수 있다고 생각했기 때문입니다.