Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[7팀 김원표] [Chapter 2-2] 디자인 패턴과 함수형 프로그래밍 #19

Open
wants to merge 38 commits into
base: main
Choose a base branch
from

Conversation

pitangland
Copy link

@pitangland pitangland commented Jan 14, 2025

과제 체크포인트

기본과제

  • React의 hook 이해하기

  • 함수형 프로그래밍에 대한 이해

  • Component에서 비즈니스 로직을 분리하기

  • 비즈니스 로직에서 특정 엔티티만 다루는 계산을 분리하기

  • Component에서 사용되는 Data가 아닌 로직들은 hook으로 옮겨졌나요?

  • 주어진 hook의 책임에 맞도록 코드가 분리가 되었나요?

  • 계산함수는 순수함수로 작성이 되었나요?

심화과제

  • 뷰데이터와 엔티티데이터의 분리에 대한 이해

  • 엔티티 -> 리파지토리 -> 유즈케이스 -> UI 계층에 대한 이해

  • Component에서 사용되는 Data가 아닌 로직들은 hook으로 옮겨졌나요?

  • 주어진 hook의 책임에 맞도록 코드가 분리가 되었나요?

  • 계산함수는 순수함수로 작성이 되었나요?

  • 특정 Entitiy만 다루는 함수는 분리되어 있나요?

  • 특정 Entitiy만 다루는 Component와 UI를 다루는 Component는 분리되어 있나요?

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

과제 셀프회고

단순히 재미를 넘어, 개발에 대한 열정을 다시금 되살릴 수 있는 계기가 되었다. 항해를 진행하며 개발과 멀어지려 했던 내 스스로를 반성하게 되었고, 다시 가까워진 것 같아 좋았다.

개념과 원리를 명확히 모르고 있어 더 알려고 하지 않고 멀어지려 했던 것 같다. 코드가 읽히지 않는 문제는 물론 더러운 코드도 있겠지만 개념도 부족하다 느꼈다. 과제를 진행하며 단순히 문제를 해결하기에 급급했던 태도를 벗어나 개념을 제대로 이해하려는 노력의 중요성을 느끼고 개념을 이해하려 노력했다. 단순히 결과물 뿐 아니라 기본기와 학습 태도의 중요성을 다시한번 느끼고 노력하여 용기를 낼 것이다.

4주차때는 타입스크립트 문법에 스스로 아쉬움을 느꼈는데 5주차때는 뿌듯함을 느꼈다. 한층 성장한 듯 싶다! :)

과제에서 좋았던 부분

4주차에 비해 가이드가 명확하게 제시된 덕분에 접근 방식이 보다 수월하게 느껴졌다. 리액트의 폴더 구조나 작성 방식을 그동안 "다들 이렇게 쓰니까 나도!" 하고 따라만 쓰던 부분이 많았는데, 이번 과제를 통해 그 구조와 개념의 이유를 이해할 수 있었다. 익숙함에 속아 소중함을 잃어버린 느낌이었는데 소중함을 찾고 기초를 다질 수 있었다.

과제를 하면서 새롭게 알게된 점

  1. 테스트의 중요성

    "프론트엔드는 보이는 화면만 제대로 나오면 끝 아닌가? 왜 테스트" 라는 생각을 가지고 있었다. 계산 로직이 많지 않다고 여겼는데 실제로 살펴보니 생각보다 많은 계산과 상태 관리가 필요하다는 것을 알게되었다.
    테스트가 왜 중요해? 라는 생각을 넘어 함수형 프로그래밍에 대해 알아보게 되었다. 입력값과 출력값이 명확해야 테스트하기 쉽다는 사실을 깨달았고 어떻게 만들지? 라는 생각을 하게 되었다.

  2. 함수형 프로그래밍의 원칙

    함수형 프로그래밍의 주요 특징인 순수성, 불변성, 선언적 패턴 중 현재는 순수성 개념만 제대로 이해한 듯 싶다. (하지만, 이 생각이 적당히 적절히를 생각못하고 너무 순수하게만 간 것 같기도 하다.) 아직 불변성과 선언적 패턴에 대해서는 왜 중요한지에 대한 "왜?"를 명확하게 해결하지 못했지만, 앞으로 더 깊이 알아갈 것이다.

    (쏙쏙 함수형 프로그래밍 책을 사진 않고 테오 블로그로 맛보기했다..!)

  3. 나만의 리팩토링 기준

    4주차 멘토링 피드백의 리팩토링 방식이 도움이 되었고 나만의 기준을 세울 수 있었다. 이전에는 클린 코드라고 하면 막막한 게 앞서 단순히 변수명 수정이나 포맷팅만 눈에 보였지만 이번 과제를 진행할 때는 아키텍처 레벨에서의 구조 분리, 함수 레벨 리팩토링을 진행할 수 있었다.

    • 큰 구조에서 작은 구조로 분리:
      먼저 큰 아키텍처의 레벨을 나누고, 점진적으로 세부적인 부분을 리팩토링을 진행하였다.

      1. 로직 배치
      2. 컴포넌트 분리
      3. 상태와 로직 분리 :
        컴포넌트는 UI와 데이터 렌더링만 수행하도록 설계
        Props를 통해 상태와 액션 전달
        계산이나 상태 변경은 외부 로직으로 분리
    • 변수명 정리의 순서:
      변수명은 코드 커밋 단위라고 해야할까, 기능 구현 리팩토링 후 마지막에 정리해도 늦지 않다는 점을 깨달았다. 단, 이해하기 힘든 변수명은 중간에 정리하며 진행했다.

과제를 진행하면서 아직 애매하게 잘 모르겠다 하는 점, 혹은 뭔가 잘 안되서 아쉬운 것들

현재 useAppState 훅에서 productList와 couponList 상태를 함께 관리하고 있는 구조를 사용하고 있다. 이 상태를 useProductState와 useCouponState로 분리했더라면, 확장성을 고려한 더 좋은 코드가 될 수 있지 않았을까 하는 생각이 들었다.

코드가 간결하면 이해하기 쉽다는 생각에 나누지 않았지만, 실제로는 간결함이 꼭 확장성과 가독성을 보장하지 않는다는 점을 깨달았다. 코드의 “짧음”만을 기준으로 적절성을 판단했던 것 같다.

또 한편으로는, 현재 프로젝트의 복잡도를 고려하면 지금과 같은 통합 구조가 간단하고 유지보수가 쉬울 수도 있다고 생각했다. 하지만, 상태를 명확히 분리하지 않음으로써 관심사가 제대로 나뉘지 않은 것은 아닌지 다시 고민하게 된다..

결국, 코드의 “적절함”의 기준이란 주관적일 수 밖에 없고 팀원들과 많은 얘기를 나누며 적절함의 기준을 찾아가야겠다고 생각했다.

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

순수 함수의 나눔 기준에 대해

모든 하위 로직을 함수로 나누는 것이 꼭 좋은 설계라고 할 수 있는 것일까요?
적절히 나누는 것이 중요하다 생각이 드는데 특정 함수가 상위 함수 외부에서 재사용되지 않는다면, 이를 분리하지 않고 상위 함수에 통합하는 것이 나은 선택일지도 궁금합니다. 또한, 함수 분리의 적절한 기준에 대해 조언을 듣고 싶습니다.

아래 코드를 분리하다 의문이 들었습니다.

...cart.ts
// 할인 적용 전 총 금액 계산
export const calculateTotalBeforeDiscount = (cart: CartItem[]) => {
return cart.reduce((total, item) => {
    return total + item.product.price * item.quantity;
}, 0);
};

// 할인 적용 후 총 금액 계산
export const calculateTotalAfterDiscount = (cart: CartItem[]) => {
return cart.reduce((total, item) => {
    return total + calculateItemTotal(item);
}, 0);
};

// 쿠폰 적용
export const applyCoupon = (totalAfterDiscount: number, selectedCoupon: Coupon | null): number => {
if (!selectedCoupon) return totalAfterDiscount;

if (selectedCoupon.discountType === 'amount') {
    return Math.max(0, totalAfterDiscount - selectedCoupon.discountValue);
} else {
    return totalAfterDiscount * (1 - selectedCoupon.discountValue / 100);
}
};

// 장바구니 전체 금액 및 할인 계산
export const calculateCartTotal = (cart: CartItem[], selectedCoupon: Coupon | null) => {
const totalBeforeDiscount = calculateTotalBeforeDiscount(cart);
let totalAfterDiscount = calculateTotalAfterDiscount(cart);

// 쿠폰 적용
totalAfterDiscount = applyCoupon(totalAfterDiscount, selectedCoupon);

// 총 할인 금액 계산
let totalDiscount = totalBeforeDiscount - totalAfterDiscount;

return {
    totalBeforeDiscount: Math.round(totalBeforeDiscount),
    totalAfterDiscount: Math.round(totalAfterDiscount),
    totalDiscount: Math.round(totalDiscount),
};
};

- prettier 설치 및 설정
- eslint 설정 변경
- tsconfig 설정 변경
- `calculateItemTotal` : 개별 항목 총 금액 계산
- `getMaxApplicableDiscount` : 개별 항목에서 적용 가능한 최대 할인율 계산
- `calculateCartTotal` : 장바구니 전체 금액 및 할인 계산
- `updateCartItemQuantity` : 장바구니 항목의 수량 업데이트
- `addToCart`: 장바구니 항목 추가
- `removeFromCart`: 장바구니 항목 제거
- `updateQuantity`: 수량 업데이트
- `applyCoupon`: 쿠폰 적용
- `calculateTotal`: 총합 계산 함수
- `getRemainingStock`: 항목의 남은 재고 확인
- .ts와 .tsx 확장자 삭제로 경로 간소화
- " -> ' 변경으로 따옴표 스타일 통일
- .ts와 .tsx 확장자 삭제로 경로 간소화
- " -> ' 변경으로 따옴표 스타일 통일
- `calculateItemTotal` 로직 수정: 할인율을 반영한 총 금액 계산으로 변경
- `updateCartItemQuantity` 함수 선언 방식을 테스트 코드와 통일 및 개선
- `useCart` 훅 수정: 함수 선언 방식을 테스트 코드와 통일 및 개선
- 주석 수정 : 삭제 예정
- `초기 제품 목록을 상태로 관리하는 `useProducts` 추가
- `updateProduct`:특정 제품 정보를 업데이트
- `addProduct`:새로운 제품 추가`
- `초기 쿠폰 목록을 상태로 관리하는 `useCoupons` 추가
- `addCoupon`: 새로운 쿠폰을 추가`
- basic test 통과 및 따옴표 스타일 통일
- 아직 리팩토링 전 기능 구현만.. 일단 체크!
   : 순수함수에 집중해서 리팩토링할 예정
- CartPage를 더 작은 단위의 컴포넌트(ProductList, CartList, CouponSelector, OrderSummary)로 분리
- 각 컴포넌트가 독립적으로 역할을 수행하도록 구조 변경
- 코드 가독성 및 재사용성을 높이고 유지보수성을 개선
- 변수명 products => productList, coupons => couponList 변경 (팀 변수명 규칙에 의해)
- CartPage와 AdminPage를 pages 폴더로 이동
- 페이지와 컴포넌트를 구분
- 내부 로직(getMaxDiscount)을 외부로 분리하여 props로 전달받도록 수정
- ProductList 컴포넌트는 이제 입력값(props)에 따라 동일한 결과를 반환하는 순수 함수로 동작
- 내부 로직(getAppliedDiscount)을 외부로 분리하여 props로 전달받도록 수정
- CartList 컴포넌트는 이제 입력값(props)에 따라 동일한 결과를 반환하는 순수 함수로 동작
- 단일 JSX 반환 구조를 사용하여 코드 간결화
- return 키워드 및 중괄호 삭제
- 기존 props로 전달받던 totalBeforeDiscount, totalAfterDiscount, totalDiscount 제거
- calculateTotal 함수를 props로 받아 내부에서 필요한 데이터를 직접 계산
- calculateRemainingStock,
   calculateCartTotal,
   getAppliedDiscount 비즈니스 로직을 models 디렉토리로 분리
- 장바구니 비즈니스 로직의 관심사 분리
- `calculateCartTotal` 함수의 역할을 세분화하여 단일 책임 원칙 준수:
  - `calculateTotalBeforeDiscount` 함수 추가
  - `calculateTotalAfterDiscount` 함수 추가
  - `applyCoupon` 함수 추가
- Cart 관련 컴포넌트 components/Cart 로 이동
- Cart와 Admin 으로 폴더 구조 변경 관리
✏️ 변수명 가독성을 위한 수정
- getMaxApplicableDiscount => calculateItemMax
- 그에 맞게 basic.test 도 수정
- useAppState 훅으로 상태 관리 로직 분리
- NavBar 컴포넌트로 네비게이션 로직 분리
- 초기 데이터 파일(data/initialData.ts)로 이동
- AdminPage를 ProductForm, ProductAccordion(ProductEdit), CouponForm, CouponList로 분리
- `calculateItemMaxDiscount`: 장바구니 항목의 최대 할인율 계산 테스트
- `calculateItemTotal`: 할인율이 적용된 총 금액 계산 테스트
- `calculateMaxDiscount`: 할인 목록에서 최대 할인율 계산 테스트
- `$product-1`을 `$product-p1`으로 수정
- interface 명 수정
- ProductForm 컴포넌트 리팩토링
- CouponForm 컴포넌트 리팩토링

.. 하다보니 Form 이 다 비슷해서 합쳤어도 됐나? 하는 생각이 든다..!
- 이건 사실 안해도 되는 것 같긴한데 일단 분리해봄
--> 다시 리팩토링 예정!
- test를 it으로 변경

<왜 바꿨어?에 대한 답변>
- it: BDD 스타일을 따르거나, 테스트가 자연어처럼 읽히기를 원할 때.
- test: 테스트 코드임을 명확히 하고자 할 때, 또는 개인적으로 선호할 때.
@pitangland pitangland changed the title [WIP] [7팀 김원표] [Chapter 2-2] 디자인 패턴과 함수형 프로그래밍 [7팀 김원표] [Chapter 2-2] 디자인 패턴과 함수형 프로그래밍 Jan 17, 2025
Copy link

@ywkim95 ywkim95 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이번 주차 고생 많으셨습니다..!
이번 주에 새벽까지 계속 깨있으시던데 건강도 챙기면서 진행 하셨으면 좋겠습니다!
분리가 굉장히 깔끔하게 잘 된 것 같아요!
다음 번에는 UI와 로직을 더 나눠보는 연습은 어떠세요?

})
})
});
});

describe('자유롭게 작성해보세요.', () => {
test('새로운 유틸 함수를 만든 후에 테스트 코드를 작성해서 실행해보세요', () => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

엇...! 이부분이 정상동작을 하던가요...?
일반적으로 describe내에 test가 들어가는 것으로 아는데 궁금하네요!

그리고 테스트코드를 굉장히 많이 작성해주셨네요! 고생하셨어요!

Comment on lines +11 to +23
<div className='min-h-screen bg-gray-100'>
<NavBar isAdmin={isAdmin} toggleAdmin={toggleAdmin} />
<main className='container mx-auto mt-6'>
{isAdmin ? (
<AdminPage
products={products}
coupons={coupons}
productList={productList}
couponList={couponList}
onProductUpdate={updateProduct}
onProductAdd={addProduct}
onCouponAdd={addCoupon}
/>
) : (
<CartPage products={products} coupons={coupons}/>
<CartPage productList={productList} couponList={couponList} />
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코드가 굉장히 깔끔해서 좋네요! 한눈에 어떤 것들이 있는지 명확하게 파악하기 좋은듯해요!

<button
data-testid='modify-button'
className='bg-blue-500 text-white px-2 py-1 rounded hover:bg-blue-600 mt-2'
onClick={() => {}}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 클릭 이벤트가 등록이 안되어있네요!

Comment on lines +10 to +26
const [editedProduct, setEditedProduct] = useState(product);
const [newDiscount, setNewDiscount] = useState<Discount>({ quantity: 0, rate: 0 });

const handleUpdate = () => {
onProductUpdate(editedProduct);
};

const handleAddDiscount = () => {
const updatedDiscounts = [...editedProduct.discounts, newDiscount];
setEditedProduct({ ...editedProduct, discounts: updatedDiscounts });
setNewDiscount({ quantity: 0, rate: 0 });
};

const handleRemoveDiscount = (index: number) => {
const updatedDiscounts = editedProduct.discounts.filter((_, i) => i !== index);
setEditedProduct({ ...editedProduct, discounts: updatedDiscounts });
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기존에 있던 이벤트 핸들러들을 전부 없애시고 바로 등록하는 방식을 택하셨군요!
저는 로직과 UI 가 혼재되는게 싫어서 전부 분리하였는데 이렇게 보니까 한 눈에 파악하기에는 더 좋은듯합니다!

applyCoupon: (coupon: Coupon) => void;
}

export const CouponSelector = ({
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 Selector라는 이름으로 정하셨군요..!
저는 개인적으로는 Selector라는 명칭이 함수처럼 느껴져서 저라면 Dropdown이나 List 등으로 변경할 듯해요!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오호... initialData영역을 두 가지로 나누는건 또 다른 관점이네요!
명확하게 admin에서만 사용하기 때문에 그러신거겠죠...?

src/refactoring/models/cart.ts Outdated Show resolved Hide resolved
pitangland and others added 2 commits January 19, 2025 17:19
- 불변성 유지 : let을 const로 변경
(const로 선언하고 싶었는데 왜 안되는지 싶었는데 영우님의 도움으로 해결.. 이제야 let과 const를 이해했다!)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants