[3팀 유한별] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍 - #7
Conversation
Refactor/advanced
suinkimme
left a comment
There was a problem hiding this comment.
한별님 코드는 볼때마다 제가 성장되는 느낌이네요! 늘 배워갑니다!
| export const useAccordion = <T extends string | number>() => { | ||
| const [openIds, setOpenIds] = useState<Set<T>>(new Set()); | ||
|
|
||
| const toggle = (id: T) => { | ||
| setOpenIds((prev) => { | ||
| const newSet = new Set(prev); | ||
| if (newSet.has(id)) newSet.delete(id); | ||
| else newSet.add(id); | ||
| return newSet; | ||
| }); | ||
| }; | ||
|
|
||
| const isOpen = (id: T) => openIds.has(id); | ||
|
|
||
| return { | ||
| openIds, | ||
| isOpen, | ||
| toggle, | ||
| }; | ||
| }; |
There was a problem hiding this comment.
우왓.! 이렇게까지 디테일하게 나눠볼 수도 있는거였군요!ㅜㅜ 저는 왜ㅠㅠ 몰랐을까요
| export const useValidation = () => { | ||
| const withValidation = | ||
| (field: ValidationField, handler: (value: string) => void) => | ||
| (e: React.ChangeEvent<HTMLInputElement>) => { | ||
| const result = validateInput(field, e.target.value); | ||
| if (!result.isValid && result.message) { | ||
| alert(result.message); | ||
| return; | ||
| } | ||
| handler(e.target.value); | ||
| }; | ||
|
|
||
| return { withValidation }; | ||
| }; |
There was a problem hiding this comment.
입력값이 유효할 때만 핸들러를 실행하게해주는 Hook이군요! 왕! 신기하다
There was a problem hiding this comment.
사실 요건.. 심화에서 custom util이랑 custom hook 추가하래서 억지로 만들어봤던...
| export const ProductManagementProvider = ({ | ||
| children, | ||
| onProductUpdate, | ||
| onProductAdd, | ||
| }: ProductManagementProviderProps) => { | ||
| const accordion = useAccordion<string>(); | ||
| const productEdit = useProductEdit({ onProductUpdate }); | ||
| const discount = useProductDiscount(); | ||
| const productCreate = useProductCreate({ onProductAdd }); |
There was a problem hiding this comment.
Context를 여러개 생성하지 않고 이렇게 처리해도 좋겠네요!!! 저는 여러개로 나눠서 머리 아팠습니다!
There was a problem hiding this comment.
애시당초 useProduct 관련한 훅이 하나로 뭉쳐있어서, context도 하나면 충분하다고 생각했습니당!
| const colorStyles: Record<ButtonColor, string> = { | ||
| white: "bg-white text-blue-600 hover:bg-blue-100", | ||
| blue: "bg-blue-500 text-white hover:bg-blue-600 disabled:bg-gray-300 disabled:text-gray-500", | ||
| green: "bg-green-500 text-white hover:bg-green-600", | ||
| red: "bg-red-500 text-white hover:bg-red-600", | ||
| gray: "bg-gray-300 text-gray-500 hover:bg-gray-400", | ||
| none: "", | ||
| }; | ||
|
|
||
| const widthStyles: Record<ButtonWidth, string> = { | ||
| full: "w-full", | ||
| third: "w-1/3", | ||
| default: "", | ||
| }; | ||
|
|
||
| const textStyles: Record<ButtonText, string> = { | ||
| "left-semibold": "text-left font-semibold", | ||
| default: "", | ||
| }; | ||
|
|
||
| const disabledStyles = disabled ? "cursor-not-allowed" : ""; |
There was a problem hiding this comment.
Button의 재사용성을 높이려고 했을 때, class로 다 받아버리면 분리하는 의미가 없어지는 것 같아서 억지로 꾸역꾸역 넣었습니다..
There was a problem hiding this comment.
버튼을 모양 별로 컴포넌트를 만드는 방법은 비추천하시나요? 한별님은 어떻게 생각하시는지 궁금합니다!
There was a problem hiding this comment.
음... 모양이라는게 정확히 어떤 기준일까요?
저는 이번 과제에서는 모양별?로 나누기가 애매해서 (서로 스타일링이 다 애매하게 다르더라구요) 우선 이렇게 호로록 처리해버렸습니다ㅠ
There was a problem hiding this comment.
아 WhiteButton 이런식으로 말씀하시는걸까?
그러면 진짜 공통 요소만 Button으로 만들고 상속받아서 모양별로 만들어도 좋을 것 같네요!
There was a problem hiding this comment.
저는 버튼을 스타일 정의할 때 버튼의 모양, 크기, 색상 이런 부분들로 정의를 내리는 것은 보통 어드민에서 그렇게 많이 내리는 편이었던 것 같고, 사용자화면에서 있는 버튼들은 버튼의 쓰임으로 구분을 많이 했던 것 같아요. 버튼의 스타일로 정의를 나누게 되면 디자인이 바뀌게 되는 경우 그 기준이 많이 틀어지는 경험들을 했어서...ㅠ 쓰임으로 한다는 것의 예로는 페이지 하단에 들어가는 메인버튼이다, 테이블위에 위치하는 유틸버튼이다 등등 이런식으로 나눴던 것 같아요! 그치만 아직도 어떤 게 맞다! 이런 거는 모르겠네요ㅠ 오래 고민해왔지만 어려운 부분인 것 같아요!ㅠㅠ
There was a problem hiding this comment.
이준님은 오히려 역할을 기준으로 나누셨군요..! 해당 접근도 좋은 것 같습니다!
그런데 그렇게 되면 재사용성이 조금 떨어질 것 같은데, 혹시 재사용성을 높이기 위해 추가적인 방안이 있을까요?
There was a problem hiding this comment.
그래서 재사용성이 높도록 역할이 분명한 버튼들을 잘 골라내는 작업이 중요한 것 같다고 생각해요.. 기준을 애매하게 나눠버리면 정말 재사용성이 떨어질 위험이 있어서 어려운 부분이네요ㅠ
| const accordion = useAccordion<string>(); | ||
| const productEdit = useProductEdit({ onProductUpdate }); | ||
| const discount = useProductDiscount(); | ||
| const productCreate = useProductCreate({ onProductAdd }); |
There was a problem hiding this comment.
Context에 사용한 것과는 어떤 차이가 있나요...? 뭔가 알 것 같긴한데... 명확하게 잘 모르겠습니다...! 알려주실 수 있으신가요...!
There was a problem hiding this comment.
앗, 기존에 useProductManagement로 사용하던걸 context로 변경하고 기존 코드를 안지운겁니다... 덕분에 발견했네요. 감사합니다!!
| <PageLayout title="장바구니"> | ||
| <SectionLayout title="상품 목록"> | ||
| <ProductCardList | ||
| products={products} | ||
| cart={cart} | ||
| onAddToCart={addToCart} | ||
| /> | ||
| </SectionLayout> | ||
| <SectionLayout title="장바구니 내역"> | ||
| <CartItemCardList | ||
| cart={cart} | ||
| updateQuantity={updateQuantity} | ||
| removeFromCart={removeFromCart} | ||
| /> | ||
|
|
||
| <CouponSection | ||
| coupons={coupons} | ||
| selectedCoupon={selectedCoupon} | ||
| onApplyCoupon={applyCoupon} | ||
| /> | ||
|
|
||
| <OrderSummary | ||
| totalBeforeDiscount={totalBeforeDiscount} | ||
| totalDiscount={totalDiscount} | ||
| totalAfterDiscount={totalAfterDiscount} | ||
| /> | ||
| </SectionLayout> | ||
| </PageLayout> | ||
| ); |
| product: Product; | ||
| } | ||
|
|
||
| export const ProductManagementItemDiscount = ({ |
There was a problem hiding this comment.
전체 페이지의 대부분의 마크업을 컴포넌트화 하신 것 같아요!(제 생각이 맞다면..?) 그런데 제가 고민인 부분이 h태그의 제목이나 큰 페이지의 레이아웃을 구성하는 부분들은 사실 컴포넌트처럼 여러 페이지에서 재사용되기 어려운 부분들도 있을 것 같은데, 혹시 전부 컴포넌트화 하신 이유가 있을까요??? 저는 이부분에서 기준을 잡기가 어렵더라구요. 어디까지 컴포넌트화하고 어디는 컴포넌트화하지 않아야 하는지...ㅠ
There was a problem hiding this comment.
h태그의 제목이나 큰 페이지의 레이아웃을 구성하는 부분 << 이 부분들도 layout으로 최대한 공통화 할 수 있지 않을까해서 분리하려고 노력했습니다!
사실 명확한 디자인이 없는 상황에서 기준을 나누기는 어렵다고 생각하는데, 저는 해당 뷰나 엔티티에 종속되지 않는 요소라면 최대한 분리하려고 합니다!
| width = "default", | ||
| text = "default", | ||
| className = "", | ||
| disabled, |
There was a problem hiding this comment.
근데 정말 보니까 컴포넌트화에 신경을 많이 쓰신 것 같네요. 이런 타이틀, 버튼 요소들까지 정의를 내려서 구분하시구.. 반성하게 됩니다.
There was a problem hiding this comment.
사실... 이부분은 디자인 시스템이나 명확한 디자인 가이드가 있었다면 고려할 부분이 아니라고 생각되는데요...
억지로 공통 컴포넌트화한 경향이 있습니다ㅠㅠ
| @@ -0,0 +1,43 @@ | |||
| import { Coupon, Product } from "../../types"; | |||
|
|
|||
| export const INITIAL_PRODUCTS: Product[] = [ | |||
There was a problem hiding this comment.
components 폴더 내에서 consts 폴더에 const를 모아두시려는 이유가 있을까요?? 이 부분에서 파일별로 정의를 나눠서 그 안에 페이지 전체에 필요한 consts 객체? 변수?들을 모아두실 생각이시려는 걸까욥?? 장점도 궁금합니다!!
There was a problem hiding this comment.
엇, components 디렉토리 아래에 있는게 아니라, 전역 consts입니다!
사실 저는 data 개념이 아직 와닿지 않아서 습관적으로 이렇게 처리했는데요.
이준님 코드를 보니 data 디렉토리를 만들어 엔티티별로 분리했어야할 것 같습니다..!
과제 배포 링크
https://hanbeulyou.github.io/front_5th_chapter2-2/index.refactoring.html
과제의 핵심취지
과제에서 꼭 알아가길 바라는 점
기본과제
Component에서 비즈니스 로직을 분리하기
비즈니스 로직에서 특정 엔티티만 다루는 계산을 분리하기
뷰데이터와 엔티티데이터의 분리에 대한 이해
entities -> features -> UI 계층에 대한 이해
Component에서 사용되는 Data가 아닌 로직들은 hook으로 옮겨졌나요?
주어진 hook의 책임에 맞도록 코드가 분리가 되었나요?
계산함수는 순수함수로 작성이 되었나요?
Component에서 사용되는 Data가 아닌 로직들은 hook으로 옮겨졌나요?
주어진 hook의 책임에 맞도록 코드가 분리가 되었나요?
계산함수는 순수함수로 작성이 되었나요?
특정 Entitiy만 다루는 함수는 분리되어 있나요?
특정 Entitiy만 다루는 Component와 UI를 다루는 Component는 분리되어 있나요?
데이터 흐름에 맞는 계층구조를 이루고 의존성이 맞게 작성이 되었나요?
심화과제
재사용 가능한 Custom UI 컴포넌트를 만들어 보기
재사용 가능한 Custom 라이브러리 Hook을 만들어 보기
재사용 가능한 Custom 유틸 함수를 만들어 보기
그래서 엔티티와는 어떤 다른 계층적 특징을 가지는지 이해하기
UI 컴포넌트 계층과 엔티티 컴포넌트의 계층의 성격이 다르다는 것을 이해하고 적용했는가?
엔티티 Hook과 라이브러리 훅과의 계층의 성격이 다르다는 것을 이해하고 적용했는가?
엔티티 순수함수와 유틸리티 함수의 계층의 성격이 다르다는 것을 이해하고 적용했는가?
과제 셀프회고
🚀 들어가며
과제를 받았을 때 솔직히 금방 끝낼 수 있을 거라 생각했다. "비즈니스 로직을 컴포넌트에서 빼서 훅이랑 함수로 옮기면 되는 거 아냐?"라는 생각이었다. 하지만 막상 손을 대고 보니, 단순히 '나누는 것'보다 '어떻게 나눌지', 그리고 '왜 그렇게 나누는지'가 훨씬 더 어렵다는 걸 느꼈다.
이번 과제는 단순한 코드 정리가 아니라, React의 계층적 설계, 함수형 프로그래밍(FP)의 원칙, 그리고 재사용성과 유지보수성까지 고민하게 만들었다. 결국 목표는 "돌아가는 코드"가 아니라, 이해하기 쉽고, 확장 가능한 구조를 만드는 것이었다. 그 과정에서 예상하지 못한 고민들이 쌓였고, 이번 회고에서는 그 선택의 이유와 과정을 정리하고자 한다.
🔍 문제 인식
코드를 분리하기로 마음먹고 구조를 살펴봤을 때 가장 먼저 보인 것은 하나의 컴포넌트 안에 상태, 로직, UI가 모두 뒤섞여 있다는 점이었다. 상태 관리와 비즈니스 로직, 뷰 렌더링이 구분 없이 작성되어 덩치만 커진 코드가 눈앞에 있었다.
컴포넌트를 기능별로 분리하기 시작하면서 예상하지 못한 문제가 생겼다. 오히려
props drilling이 심해졌고 핸들러를 각 컴포넌트로 전달하는 과정도 복잡해졌다. 여기에 여러 훅을 조합해야 하는 상황까지 겹치면서 구조를 개선하려 했지만 오히려 복잡해지는 결과를 낳았다.이 과정에서 단순히 코드를 나눈다고 해결되지 않는다는 걸 느꼈다. 상태와 로직, UI를 어떤 기준으로 분리할지 그리고 각 계층의 역할을 어떻게 정의할지에 대한 고민이 본격적으로 시작됐다.
🧩 함수형 프로그래밍 적용
과제 요구사항 중 하나는 함수형 프로그래밍(FP)의 원칙에 따라 로직을 분리하는 것이었다. 계산 로직을 순수 함수로 빼고, 상태 변경 등의 비즈니스 로직은
action으로 구분하면 될 거라고 생각했다. 하지만 실제로 적용해보니data,calculation,action의 경계를 명확하게 구분하는 게 쉽지 않았다.특히
data의 개념에서 많은 고민이 생겼다. 단순히const로 선언된 상수만을data라고 해야 하는지, 아니면 React의state도data로 봐야 하는지 명확하지 않았다. FP에서 말하는 "불변 데이터" 개념을 React 환경에 그대로 적용하는 게 맞는지 의문이 들었고, 굳이data를 별도로 분리하는 게 실용적인가라는 생각도 하게 됐다.결국 FP 원칙을 완전히 소화하지 못한 채, 일단 적용해보는 데 그쳤다. 제대로 이해하고 활용하기 위해선 더 많은 고민이 필요하다고 느꼈다.
🪝 훅 분리 vs 통합
기능별로 코드를 정리하기 위해 처음에는 훅을 작은 단위로 분리했다.
useProductEdit,useProductDiscount,useAccordion처럼 역할별로 나누면 관리가 쉬워질 거라 생각했다. 하지만 실제로 적용해보니 분리된 훅들 사이에서 상태와 로직을 서로 참조해야 하는 상황이 반복되었고, 자연스럽게 의존성 문제가 발생했다.의존성을 줄이기 위해 여러 훅을 하나로 통합한 것이
useProductManagement였다. 이 훅은 단순히 하위 훅을 묶는 것에 그치지 않고, 각각의 훅에서 발생하는 로직을 조율하기 위한 추가 핸들러(handleDiscountAdd,handleDiscountRemove)까지 포함하게 됐다.통합 훅 덕분에 상호작용이 많은 로직을 한 곳에서 관리할 수 있었고,
props drilling문제도Context를 도입해 해결했다. 결과적으로 컴포넌트 구조는 깔끔해졌지만, 상태 관리, 이벤트 핸들러, 하위 훅 호출이 모두 집중되면서 훅 자체가 비대해지는 문제가 생겼다. 반환값이 점점 커지고, 하나의 훅이 너무 많은 책임을 가지게 되면서 이 구조가 과연 유지보수에 적합한지에 대한 고민이 계속됐다.🧱 컴포넌트 구조화
컴포넌트를 분리할 때는 단순히 화면 기준으로 쪼개면 될 줄 알았다. 처음에는 레이아웃이나 기능 단위로 나누면서 구조가 점점 정리되는 것 같았다. 그런데 로직을 엔티티 기준으로 관리하다 보니, 어느 순간부터 화면 기준과 로직 기준이 어긋나는 상황이 자주 발생했다.
예를 들어,
Product관리 화면을 분리하다 보면 화면 구조상 하나의 컴포넌트로 묶는 게 자연스러운데, 내부에서는Product나Discount같은 엔티티 로직이 얽혀서 컴포넌트가 어디까지 책임져야 할지 애매해지는 경우가 많았다.재사용을 위해 컴포넌트를 분리하긴 했지만, 로직과 UI의 경계를 어떻게 깔끔하게 유지할지 계속 고민해야 했다.
그래서 결국 컴포넌트는 UI에 집중하고, 로직과 상태 관리는 훅이나
Context로 넘기는 방향으로 정리했다. 다만, 이번 과정을 통해 느낀 건 무조건 분리하는 게 답은 아니라는 것이었다. 구조를 깔끔하게 만드는 것도 중요하지만, 결국은 각 컴포넌트가 어떤 역할을 가져가야 하는지 명확하게 정의하는 것이 더 중요하다고 생각하게 됐다.🤔 아쉬운 점
과제를 마무리하면서 가장 크게 느낀 건 "과연 내가 제대로 이해하고 적용한 걸까?" 하는 의문이었다.
함수형 프로그래밍이나 계층 분리를 나름대로 정리하긴 했지만 여전히 흉내만 낸 게 아닐까 하는 생각이 남았다.
특히 훅을 통합하면서 비대한 구조가 만들어졌을 때 그걸 어떻게 풀어야 할지 명확한 기준을 세우지 못한 게 아쉬웠다.
Context도입도props drilling은 해결했지만 이게 정말 좋은 선택인지 확신은 없었다.결국 이번 과제는 단순히 코드를 정리하는 걸 넘어서 '어떻게 나누고, 왜 그렇게 설계해야 하는지'에 대한 기준을 세우지 못한 채 끝났다는 점이 가장 큰 아쉬움으로 남았다.
🧠 느낀점
이번 과제를 통해 느낀 건 코드를 나누는 것보다 그 이유를 설명할 수 있는 게 더 중요하다는 점이었다.
처음엔 그냥 원칙대로 분리하고 통합하면 되는 줄 알았지만 막상 손을 대다 보니 모든 선택마다 이유가 필요하다는 걸 절실히 느꼈다.
함수형 프로그래밍도 그렇고, 훅과 컴포넌트 구조도 그렇고 정답이 있는 게 아니라 상황에 따라 계속 고민해야 하는 문제였다.
깔끔해 보이는 구조를 만들기보다 유지보수하기 쉽고, 내가 납득할 수 있는 설계가 훨씬 중요하다는 걸 알게 됐다.
물론 아직 명확한 기준이 잡힌 건 아니다. 이번에도 완벽하게 정리했다고 말하긴 어렵다.
하지만 앞으로 비슷한 상황이 온다면 '왜 이렇게 만들었는지' 설명할 수 있는 구조를 지향하려고 한다.
결국 계속 질문하고, 그 과정을 통해 기준을 만들어가는 수밖에 없다고 생각한다.
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문
1. 컴포넌트 분리 기준
컴포넌트를 화면 기준으로 분리하다가 재사용성을 고려해 자잘하게 쪼갰는데, 오히려 로직 전달이 복잡해지고 가독성이 떨어지는 경험을 했습니다.
처음엔 UI 컴포넌트 분리가 고민이었지만, 디자인 시스템이 있다면 이런 문제는 자연스럽게 해결된다는 걸 느꼈습니다.
다만, 여전히 비즈니스 로직과 밀접한 컴포넌트를 분리할 때는 재사용성과 가독성 사이에서 기준 잡기가 어렵다고 생각이 들었습니다.
이런 상황에서 어떤 기준으로 균형을 맞추시는지, 기억에 남는 사례가 있다면 듣고 싶습니다.
2. 함수형 프로그래밍 원칙과 실용성 사이의 균형
함수형 프로그래밍 원칙을 적용해보면서 불변성과 순수함수를 지키려다 보면 오히려 코드가 복잡해질 수도 있겠다는 생각이 들었습니다.
혹시 실무에서 비슷한 고민이나 경험을 해보신 적이 있으신가요?
그런 상황을 어떻게 풀어나가셨는지, 타협하거나 기준을 세운 경험이 있다면 공유해주시면 좋을 것 같습니다.
3. 컴포넌트 분리 시 UI와 로직 경계 설정
컴포넌트를 UI 기준으로 분리하다 보니, 내부에 엔티티 로직이 얽히면서 컴포넌트의 책임이 애매해질 수 있겠다는 고민이 들었습니다.
이런 경우 실무에서는 로직과 UI의 경계를 어떻게 설정하시는지, 혹은 이런 충돌을 보셨을 때 보통 어떻게 풀어나가시는지 궁금합니다.