과제 링크
https://yuyeol.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과 라이브러리 훅과의 계층의 성격이 다르다는 것을 이해하고 적용했는가?
-
엔티티 순수함수와 유틸리티 함수의 계층의 성격이 다르다는 것을 이해하고 적용했는가?
과제 셀프회고
과제를 하면서 내가 알게된 점, 좋았던 점은 무엇인가요?
엔티티라는 개념을 제대로 알지 뭇했었고, 처음으로 엔티티를 고려한 리팩토링을 진행 해 보았습니다.
제가 이해한 엔티티의 핵심은 두가지입니다.
-
엔티티에 귀속된 비즈니스 로직들은 어떤 프레임워크로 마이그레이션 하더라도 그대로 사용할 수 있는 로직을 온전히 떼어 두는 것일 것이다.
예를 들면, 이번 베이직 과제에서 프롭스 드릴링으로 상태를 전달하는 방식으로 개발 한 것을 조타이로 마이그레이션 할 때에 편의성을 체감 할 수 있었어야 했습니다.
그런 측면에서 엔티티를 잘 분리했는가 스스로 회고를 해 보았을때, 만족스러운 수준으로 성공하지 못한 것 같습니다. 리팩토링을 하는 과정중에 저도 모르게 상당량의 순수함수를 다시 조타이의 액션함수로 이전하고 있었습니다. 그리고 결과적으로 비즈니스 로직이 명확하게 분리되지도 못한 아쉬움이 있었습니다.
계층 현재 상태 이상적 상태 Jotai Atoms 비즈니스 로직 + 상태관리 혼재 순수 상태관리만 Models 단순한 CRUD 함수들 풍부한 비즈니스 로직 프레임워크 독립성 ❌ Jotai에 종속적 ✅ 완전 독립적 -
엔티티는 도메인 모델링과 대응되는 개념이며 도메인은 생각보다 독립적이지 않았다.
처음에는 엔티티 나누는거 뭐 별거 있겠나 하면서 호기롭게 보이는 엔티티부터 쑥쑥 뽑아서 분리해보자. 라고 생각했지만 옷걸이 더미에서 옷걸이 하나만 들어올리려다 다 딸려올라오는 듯한 코드뭉치들을 보고 잠시 내려두고 고민에 잠겼던것 같습니다.
예를들면, 카트라는 거대 도메인을 분리하고 살펴보면 그 안에는 쿠폰도 있고, 또 프로덕트도 존재했던 것입니다. 이 과제에서는 엔티티를 나누라고 했었고, 리팩토링 힌트를 참고 해 보아도 카트 / 쿠폰 / 프로덕트로 엔티티를 나누라는 가이드를 제시해 주었습니다. 애초에 이건 카트라는 하나의 도메인이면 되는거 아닌가? 이걸 꼭 억지로 나누어야 하는걸까? 라는 생각이 들기도 했습니다.
그런 중에도 힌트 대로 카트, 쿠폰, 프로덕트로 구분하여 엔티티를 나누어보았고, 최종적으로는 카트와 쿠폰 엔티티는 하나의 엔티티로 합치게 되었습니다.
쿠폰의 본질적 특성: 쿠폰은 장바구니라는 맥락 없이는 검증조차 불가능
const applyCoupon = (coupon: Coupon) => { const cartTotal = calculateCartTotal(cart, null); // 카트 의존성 필수 if ( coupon.discountType === "percentage" && cartTotal < MIN_ORDER_AMOUNT_FOR_PERCENTAGE_COUPON ) { return { success: false, message: "최소 주문 금액을 충족하지 않습니다" }; } setSelectedCoupon(coupon); };이러한 쿠폰의 특성을 고려한 것이었고, 로직의 규모 자체도 카트와 병합하기에 부담스럽지 않은 수준이어서였습니다. 다만, 훅만은 분리하여서 역할이 명확하게 구분되게 의도하였습니다.
그리고 이런 접근이 괜찮은 것인지는 모르겠지만, 어느 모델에도 로직을 두기 어려운 엔티티가 혼합된 로직은 model/index.ts 파일에 두어 관리했습니다.
// model/index.ts // 개별 아이템의 할인 적용 후 총액 계산 (discount + product 조합) export function calculateItemTotal( item: CartItem, cart: CartItem[] ): number { const { price } = item.product; const { quantity } = item; const discountRate = discount.getMaxApplicableDiscount(item, cart); return Math.round(price * quantity * (1 - discountRate)); }
결론적으로 엔티티를 접하면서 '고민'만큼은 많이 할 수 있었던 과제였다고 생각하고, 완성도는... 많이 아쉬운것 같습니다. 그리고 저 스스로가 1차원적인 리팩토링을 하는 것에 그치지 않고 더 성장할 수 있는 가능성을 볼 수 있었던 기회가 되었다고 생각합니다.
솔직히 나만의 기준을 가지고 정돈을 했다 라는 것에 의의가 있는 것이지, 남들이 보기에 편하거나 납득할만한 구조로 작성한 것인지 확신이 들지는 않는것 같습니다.
이번 과제에서 내가 제일 신경 쓴 부분은 무엇인가요?
리팩토링 힌트 파일구조에 있는 형태를 최대한 따라 해보고 조금씩 변형해보는 방식으로 과제를 진행했습니다. 내가 평소에 하고 있던 방식으로 리팩토링 해봤자, 배움이 있기보다는 익숙한 것을 되풀이할 뿐이라는 생각이 있었기 때문입니다.
힌트 구조를 따라하면서 확실히 단순한 파일 정리를 넘어서 논리적인 구조를짜는 방식을 유도하는 과제구나 하는 것을 느꼈습니다. model / hooks / utils 계층에 대한 가이드는 확실한 의도를 전달 해 주고 있었고, 그대로 따르지는 못했지만, 왜 그렇게 해야하는지에 대해서 몸소 깨달을 수 있는 점이 좋았던 것 같습니다.
이번 과제를 통해 앞으로 해보고 싶은게 있다면 알려주세요!
회사 프로젝트의 구조를 비교적 자유롭게 바꿀 수 있는 환경입니다. 이번 과제를 통해 얻은 인사이트로 회사 프로젝트에서 엔티티개념을 적용한 리팩토링을 진행해보고 싶습니다.
-
종류는 적지만, 도메인 하나하나가 복잡한 로직과 다양한 상태관리가 혼재되어 있어서 엔티티 분리를 적용하면 좋은 개선이 될 것 같습니다.
-
Zunstand를 사용하고 있는데 상태관리 복잡도가 높고, 순수한 상태 변경 로직과 비즈니스 검증 로직이 뒤섞여 있습니다.
-
백엔드 API 스펙이 매우 자주 변경되는 상황입니다. 그러면서도 중간 레이어를 두지 않아 API 변경 시 여러 곳을 수정해야 합니다. 수정사항에 대한 리소스를 줄일 수 있을 것 같습니다.
-
테스트하기 어려운 로직들이 많습니다. 복잡한 계산 로직들이 React 컴포넌트 안에 섞여 있어서 단위 테스트를 작성하기 어렵습니다. 이를 순수 함수로 잘 분리하면 테스트 도입도 고려해볼 수 있을 것 같습니다.
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문 편하게 남겨주세요 :)
"과제를 하면서 내가 알게된 점, 좋았던 점은 무엇인가요?"의 2번 항목에 기술한 내용들에 대한 질문이 있습니다.
-
cart와 coupon 엔티티를 나눌지 분리할지에 대한 고민을 적었습니다. 일반적으로 이정도 규모의 도메인에서 cart/coupon/product 엔티티를 정교하게 나누는게 유효한 전략인지 1차적으로 궁금하고, 제가 cart와 coupon은 결합된 정도와 코드 분량을 고려했을때 병합하는게 좋다고 판단했는데 옳은 판단이었을지 궁금합니다.
-
어느 모델에 둬도 애매한것 같은 로직을 model/index.ts 파일에 shared 개념으로 위치시킨 것에 대한 내용을 적었습니다. 제가 로직을 잘 나눴으면 모델이 혼합된 로직을 만들지 않았을 지도 모르겠지만, 모델이 혼합될 수 밖에 없는 로직은 종종 만날 수 있다고 생각합니다. 하지만 제가 작성한 방식이 좋은 방식이었는지는 확신이 안가는데 어떻게 생각하시는지 궁금합니다.
과제 피드백
고생하셨습니다 유열님! 이번주도 잘 정리해주셨네요. 회고를 보면 이번 과제에서 뭔가 고민을 굉장히 많이 하시고 그만큼 얻어가시는게 많았던 주차셨을 것 같아요. '옷걸이 더미에서 옷걸이 하나만 들어올리려다 다 딸려오는 듯한' 은 저도 매우 공감을 많이 하는 비유였네요 ㅎㅎ사실 말씀해주셨던 주제들이 한 주 한다고, 그리고 명확한 규칙이 만들어지는 주제들은 아닌것 같아요. 상황에 따라 매번 고민해야 되는 주제들이니 프로젝트를 보는 좋은 관점 하나가 추가로 생겼다 라고 생각해주심 좋을것 같네요 ㅎㅎ
질문 주신거 답변드려보면요!
cart와 coupon 엔티티를 나눌지 분리할지에 대한 고민을 적었습니다.
유효한 전략이라고 생각합니다. 그리고 합치는 판단도 나쁘지 않았던 것 같아요. 유열님 생각에서는 "쿠폰은 장바구니라는 맥락없이는 검증이 불가능하다"라고 말슴해주셨는데, 굳이 분리하고 관리하는게 복잡도를 올리는 관점도 있을 수 있어요. 늘 정답에 해당하는 내용은 아니고 맥락에 맞춰볼때 좋은 선택이였다 라고 볼 수 있을 것 같습니다.
어느 모델에 둬도 애매한것 같은 로직을 model/index.ts 파일에 shared 개념으로 위치시킨 것에 대한 내용을 적었습니다.
아뇨! 혼합된 로직은 언제든지 생길수 있습니다. 자주 발생하는 시나리오에요. 지금의 방식처럼 모아서 관리하는 것도 방법일 수 있고, 모델이 아닌 비즈니스 로직관점에서 여러 로직을 함께 조합해서 사용해야 하는 경우에는 helper나 service같이 분리해서 관리하는것도 방법일 수 있어요.
잘 정리해주셨고 다음주도 화이팅입니다!