[10팀 홍혜원] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍#60
Open
Wonny-ing wants to merge 20 commits into
Open
Conversation
|
이틀만에 한게 맞나 싶을정도로 잘하시네요,,, |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
과제의 핵심취지
https://wonny-ing.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과 라이브러리 훅과의 계층의 성격이 다르다는 것을 이해하고 적용했는가?
엔티티 순수함수와 유틸리티 함수의 계층의 성격이 다르다는 것을 이해하고 적용했는가?
과제 셀프회고
과제를 하면서 내가 제일 신경 쓴 부분은 무엇인가요?
과제를 다시 해보면 더 잘 할 수 있었겠다 아쉬운 점이 있다면 무엇인가요?
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문 편하게 남겨주세요 :)
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문
🎯 함수형 프로그래밍을 적용한 리팩토링 여정
프론트엔드 개발을 하다 보면 점점 커져가는 코드베이스 속에서 “어떻게 하면 코드 품질을 유지하면서 더 쉽게 유지보수할 수 있을까?”라는 고민이 자연스럽게 생기게 됩니다.
특히 컴포넌트를 설계할 때 가장 큰 고민 중 하나는 “어디까지를 컴포넌트에 맡겨야 하는가?”입니다. UI와 상태, 그리고 그 상태를 만드는 비즈니스 로직이 한데 섞여 있을 경우, 코드의 재사용성과 유지보수성이 빠르게 떨어지는 것 같습니다.
그래서 이번 과제에서는 UI와 비즈니스 로직의 관심사를 명확히 분리하는 방향으로 리팩토링을 진행하려고 했습니다!
📦 1. 관심사 분리로 코드 구조 재정비하기
🔄 기존 문제점
리팩토링 이전 코드는 다음과 같은 문제를 안고 있었습니다.
이 문제들을 해결하기 위해 프로젝트의 구조 자체를 손보기로 했습니다.
1.1 폴더 구조 작성
리팩토링의 시작은 폴더 구조를 새로 짠 게 아니라, 기존에 어느 정도 갖춰져 있던 기본 구조 위에서
components폴더 내부를 정리하는 것부터였습니다.components안에 페이지 단위(admin, cart 등) 와 그 안에서 다시 엔티티 단위(coupon, product 등) 로 컴포넌트들을 나눴습니다.src/ ├── components/ │ ├── admin/ │ │ ├── coupon │ │ ├── product │ │ └── ... │ ├── cart/ │ │ ├── CartLineItem.tsx │ │ ├── OrderSummary.tsx │ │ └── ... │ └── ui/ │ ├── Button.tsx │ └── ... ├── hooks/ │ ├── useCart.ts │ ├── useProductEdit.ts │ └── ... ├── models/ │ ├── cart.ts │ ├── product.ts │ └── ... └── contexts/ ├── CartContext.tsx ├── ProductContext.tsx └── ...admin/에는 관리자 페이지에서 쓰이는 컴포넌트들이 들어가고, 그 안에서coupon,product등으로 더 잘게 나눴습니다.cart/는 장바구니 페이지에서 쓰이는 UI 컴포넌트들이 있습니다.ui/는 버튼이나 인풋처럼 재사용 가능한 공통 컴포넌트들이 있습니다.어떤 페이지에서 어떤 컴포넌트를 쓰는지 한눈에 파악할 수 있게 됐고, 나중에 새로운 기능을 추가할 때도 어디에 코드를 둬야 할지 명확해져서 훨씬 수월해졌습니다.
1.2 컴포넌트 분해: 하나의 책임만 갖도록!
그 다음은 컴포넌트의 책임을 명확히 분리하는 작업이었습니다.
리팩토링 전에는 모든 로직을 하나의 컴포넌트(
AdminPage)에서 처리하고 있었습니다. 상태 관리, 렌더링, 이벤트 처리까지… 모든 게 한 파일 안에 들어 있다 보니 유지보수가 정말 어려웠습니다.이를 아래처럼 역할에 따라 나눈 특화된 컴포넌트들로 쪼갰습니다.
이제
ProductManagement는 상품 관련 기능만,CouponManagement는 쿠폰 관련 기능만 담당하게 되었습니다. 컴포넌트마다단일 책임 원칙을 지키도록 리팩토링을 했습니다.🧼 2. 순수 함수 중심으로 로직 정리하기
함수형 프로그래밍의 또 하나의 핵심은
순수 함수(Pure Function)입니다.이러한 성질 덕분에 테스트가 쉬워지고, 예측 가능한 코드 흐름을 만들 수 있습니다.
예시: 장바구니 금액 계산
기존 코드에서는 장바구니 금액을 컴포넌트 안에서 계산하고 있었습니다. 이를 별도의 순수 함수로 분리해 더 깔끔하게 정리했습니다.
이제 이 함수는 어디서든 재사용할 수 있고, 테스트도 간편해졌습니다.
🧩 3. 상태 관리 로직의 추상화
복잡한 컴포넌트 로직을 효과적으로 관리하기 위해 상태 관리 코드를 추상화하는 작업을 진행했습니다.
주된 방법으로는 커스텀 훅을 통한 로직 분리, 그리고 Context API를 활용한 전역 상태 관리가 있습니다. 이를 통해 코드의 응집도는 높이고, UI와 로직을 명확히 분리하여 가독성과 유지보수성을 개선할 수 있었습니다!
3.1 커스텀 훅으로 로직 분리
컴포넌트를 작성하다 보면 비즈니스 로직과 뷰가 한 파일에 뒤섞이는 일이 흔합니다. 처음에는 이 방식이 빠르고 직관적이지만, 기능이 늘어나면서 로직과 UI가 얽혀버려 재사용이 어렵고 테스트하기도 힘든 코드가 된다고 느꼈습니다.
컴포넌트 내부에 비즈니스 로직과 UI 렌더링 코드가 혼재되어 있을 경우, 기능의 확장이나 테스트가 어려워지는 문제가 있습니다. 이를 해결하기 위해, 로직을
useXXX형태의 커스텀 훅으로 추출하고 컴포넌트는 오직 UI를 표현하는 데 집중하도록 리팩토링했습니다.변경 전
변경 후
이와 같이 로직을 분리하면, 컴포넌트는 오직 UI 렌더링에만 집중할 수 있어 훨씬 더 직관적인 코드가 됩니다.
또한 훅 단위의 테스트도 가능해져, 유지보수가 쉬워지는 장점이 있었습니다.
3.2 Context API를 활용한 전역 상태 관리
컴포넌트 트리의 깊이가 깊어질수록 props를 여러 단계로 전달하는 일명 "props drilling" 문제가 발생할 수 있습니다. 이를 해결하기 위해 Context API를 활용해 공통 상태를 전역으로 관리하도록 구성했습니다.
Context 정의
사용 예시
Context를 도입함으로써 데이터 흐름이 간결해지고, 각 컴포넌트는 필요한 데이터에만 의존할 수 있게 되어 결합도를 낮출 수 있었습니다.
🖊️ 4. 순수 함수를 통한 비즈니스 로직 분리
실제 서비스를 만들다 보면, 단순한 계산이라도 특정 엔티티의 규칙을 정확히 반영해야 하는 경우가 많습니다.
예를 들어 "장바구니 금액 계산", "쿠폰 할인 적용", "배송비 판단" 등은 모두 개별 엔티티에 대한 명확한 도메인 규칙이 필요한 영역이라 이런 계산들을 일반적인 유틸 함수처럼 뭉뚱그려 처리하면 엔티티의 의미가 흐려지고 이후 규칙이 바뀌었을 때 영향을 파악하기 어려워진다고 생각합니다.
그래서
calculateDiscountedPrice,getEligibleCoupons,isFreeShippingAvailable등의 유틸을 엔티티별로 명확히 나누고, 해당 로직에 필요한 구조를 중심으로만 작성했습니다.그리고 복잡한 계산 로직이나 상태 변형 로직은 컴포넌트와 분리하여
순수 함수(pure function)로 작성하도록 했습니다. 왜냐하면 순수 함수는 동일한 입력에 항상 동일한 출력을 보장하고, 외부 상태를 변경하지 않기 때문에 테스트와 디버깅이 훨씬 수월하기 때문입니다.4.1 계산 로직을 순수 함수로 분리
순수 함수로 분리된 로직은 단위 테스트 작성이 매우 쉬워지고, 다른 모듈에서도 재사용할 수 있어 개발 효율성을 높일 수 있습니다.
4.2 재사용 가능한 유틸리티 함수 추출
복잡한 로직을 직접 컴포넌트나 훅 내부에서 작성하다 보면 테스트가 어렵고 사이드이펙트를 유발하기 쉽습니다. 그래서 자주 사용되는 반복 로직을 순수 유틸 함수로 추출했습니다.
변경 전
변경 후
유틸리티 함수로 추출함으로써 동일한 로직을 여러 곳에서 일관되게 사용할 수 있고, 향후 변경이 필요할 경우 한 곳만 수정하면 되므로 유지보수성이 향상될거라 예상됩니다.
5. 상태 지속을 위한 로컬 스토리지훅 활용
사용자 경험을 향상시키기 위해 장바구니 상태와 쿠폰 선택 정보를 로컬 스토리지에 저장하는 기능을 도입했습니다. 이를 위해 재사용 가능한
useLocalStorage커스텀 훅을 만들고, 상태 관리를 담당하는 훅 내부에서 활용하도록 구성했습니다.이 훅을 통해 로컬 스토리지와 상태 관리를 연결할 수 있습니다.
이 로직으로 인해 페이지를 새로 고치거나 브라우저를 닫았다가 다시 열어도 장바구니와 쿠폰 정보가 유지되도록 해줍니다!
6. 공통 UI 컴포넌트 추출
재사용성과 유지보수성을 높이기 위해 프로젝트 전반에서 반복적으로 사용되는 UI 요소들을 식별하여 공통 컴포넌트로 분리했습니다.
이처럼 공통 컴포넌트를 구성해두면 추후 디자인 수정이나 기능 확장 시에도 하나의 컴포넌트만 수정하면 되므로 생산성과 안정성이 모두 높아질 수 있을거라 생각합니다.
7. 테스트코드 작성
엔티티 관련 유틸리티 함수들을 테스트하면서 느낀 점은 처음에 비즈니스 로직을 순수 함수로 잘 분리해두었기 때문에 테스트 코드 작성이 굉장히 수월했다는 것입니다!
예를 들어,
getAppliedDiscount,getRemainingStock,findCartItemByProductId와 같은 함수들은 모두 입력값만으로 동작하고, 외부 상태에 의존하지 않습니다. 그래서 테스트 케이스를 작성할 때 별다른 설정 없이 간단히 데이터를 제공하고 예상 결과만 체크하면 되었습니다.위 코드에서처럼,
getAppliedDiscount함수는CartItem객체와Product객체만을 사용하여 할인 금액을 계산합니다. 이 함수는 외부 환경에 영향을 받지 않고, 제공된 데이터만으로 결과를 도출해서 테스트도 단순히 "이 입력이 들어가면 이렇게 나와야 한다"는 방식으로 작성할 수 있었습니다.🌊 회고
이번 과제는 엔티티별로 순수 함수와 액션 함수를 분리하는 과정에서 많이 고생했습니다. 처음에는 비즈니스 로직을 어떻게 분리해야 할지 감이 잘 안 오고, 각 엔티티별로 적절히 함수들을 나누는 게 쉽지 않았습니다.
하지만 순수 함수로 작성하다 보니 테스트 코드를 잘 모름에도 시나리오 별로 테스트코드를 작성하는게 훨씬 수월해졌고, 로직 검증도 간단하게 할 수 있었던 것 같아요.
정말 고민하면서 진행하느라 시간이 매우 많이 소모되었는데 현업에서도 과연 이런식으로 구현 및 리팩토링을 할 수 있을지 고민됩니다😱 이건 익숙해지는 방법 밖에는 없겠죠..?ㅎ
👾 리뷰받고 싶은 내용
1️⃣ 폴더 구조에 대한 고민
이번 과제에서 FSD 방식으로 폴더 구조를 적용하려다 시도해 보니 몇 가지 불편한 점이 있었습니다. 예를 들어,
공용 컴포넌트나유틸리티 함수들을 관리하는 방식에서 일부 기능이 중복되는 문제나, 공유되는 타입 정의를 어디에 두어야 할지에 대한 고민이 생겼다. 또한, 각 기능을 독립적으로 관리하다 보면,기능 간 의존성 관리나공통 로직 처리에 있어 코드가 분산되면서 오히려 더 복잡해지는 느낌을 받기도 해서 결국 평소 자주 사용하던 폴더 구조에 엔티티별로 폴더링을 더해주는 방식으로 진행을 했습니다.실제 현업에서 FSD 방식의 폴더 구조를 많이 사용하고 있나요? 그리고 이 구조의 장점과 단점은 무엇인지, 그리고 실제 개발 현장에서 어떤 방식으로 이 구조를 관리하고 활용하는지에 대해 궁금합니다!
2️⃣ useCart에서 쿠폰 관련 로직을 분리하는 것에 대한 고민
처음 useCart 훅에서 쿠폰 관련 로직을 분리하려고 했는데 분리하는 것이 맞나 싶은 고민들이 있었는데 고민을 끝내지 못해 결국 useCart 훅안에 뒀습니다...!
위 코드를 보면 applyCoupon 함수는 쿠폰 상태를 변경하는 함수이므로 별도의 커스텀 훅으로 분리하는 것이 맞다고 생각합니다. 하지만 문제는 calculateTotal 함수입니다. 이 함수는 장바구니(cart)와 쿠폰(selectedCoupon) 두 가지 상태에 의존하고 있어서 단순 계산 로직이므로 유틸 함수의 성격을 갖는다고 판단할 수 있으나 상태에 의존하고 있어 완전한 유틸 함수로 분리하기 어렵다고 판단했습니다.
제가 생각한 가능한 해결책들:
커스텀 훅은 "도메인 데이터나 상태를 다루는 로직"을 관리하고, 유틸 함수는 "상태나 사이드 이펙트 없이 동작하는 순수 함수"라는 기준을 지키면서도 코드의 유지보수성과 재사용성을 높일 수 있는 최선의 구조가 무엇일까요?