배포 주소
https://ldhldh07.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는 분리되어 있나요?
-
데이터 흐름에 맞는 계층구조를 이루고 의존성이 맞게 작성이 되었나요?
심화과제
-
이번 심화과제는 Context나 Jotai를 사용해서 Props drilling을 없애는 것입니다.
-
어떤 props는 남겨야 하는지, 어떤 props는 제거해야 하는지에 대한 기준을 세워보세요.
-
Context나 Jotai를 사용하여 상태를 관리하는 방법을 익히고, 이를 통해 컴포넌트 간의 데이터 전달을 효율적으로 처리할 수 있습니다.
-
Context나 Jotai를 사용해서 전역상태관리를 구축했나요?
-
전역상태관리를 통해 domain custom hook을 적절하게 리팩토링 했나요?
-
도메인 컴포넌트에 도메인 props는 남기고 props drilling을 유발하는 불필요한 props는 잘 제거했나요?
-
전체적으로 분리와 재조립이 더 수월해진 결합도가 낮아진 코드가 되었나요?
과제 셀프회고
이번 과제는 보다 폭넓게 클린코딩을 해야했던 지난과제와 달리 명확한 방향성을 알려주었습니다. 그 방향성에 철저하게 맞춰서 개발을 하는 것을 목표로 했습니다.
특정 스타일, 팀의 컨벤션에 맞춰서 개발을 해야하는 경험 또한 좋은 경험이라 생각합니다. 그런 관점에서 과제 자료에 있는 요청사항이나, refactoring(hint)폴더의 구조도 그대로 따르고자 했습니다.
과제를 하면서 내가 알게된 점, 좋았던 점은 무엇인가요?
과제에 대한 설명을 해주시면서 반복해서 강조한 지향점이 있었습니다.
- 오버엔지니어링을 경험해봐야 한다.
- 과할 정도로 분리해라
함수형 코딩의 장점을 강조하면서도, '프론트엔드 개발에서 지나치게 함수형 코딩에 과몰입하는 것은 부작용이 있다'라는 경계를 가지고 있었습니다.
실제로 잘못됐다고 알려진 패턴들을 의도했든 의도치 않았든 하고 났을 때 배운 게 많았습니다. 좋은 쪽으로든 나쁜 쪽으로든 실제로 해보고 직접 느껴야 한다 생각하고 이번에도 이런 것을 느끼고자 했습니다.
기본 딘어 개념
먼저 관련된 단어의 개념을 확실히 하고 싶었습니다. 관련해서 몇가지 단어들이 혼란스럽게 쓰였습니다
계산 (Calculation) vs 액션 (Action)
계산 = 순수한 변환
- 계산은 입력을 받아 출력으로 변환하는 순수한 과정
- 외부 세계와 격리된 수학적 연산
const calculateTotal = (price: number, quantity: number): number =>
price * quantity;
const getDiscount = (total: number, rate: number): number =>
total * rate;
액션 = 외부 세계와 상호작용
- 액션은 외부 세계에 영향을 주거나 외부 세계에 의존하는 행위
- 시점과 횟수에 따라 결과가 달라짐
const saveToStorage = (data: any): void => {
localStorage.setItem('cart', JSON.stringify(data)); // 외부 영향
};
const getCurrentTime = (): number => {
return Date.now(); // 외부 의존
};
모델 (Model) vs 엔티티 (Entity)
엔티티 = 추상화 정의
- 엔티티는 추상화를 정의해 놓은 형태
- "무엇인가?"의 구조와 규칙 명시
모델 = 실제 구현 객체
- 모델은 그것을 실제로 구현해놓은 객체
- "어떻게 다루는가?"의 실제 동작
도메인 (Domain)
도메인 = 문제 영역 도메인은 해결하고자 하는 비즈니스 영역
- Cart
- Product
- Coupon
순수 함수 - 예측 가능한 함수
- 순수 함수는 부수효과가 없고 참조 투명성을 가진 함수
- 같은 입력 → 항상 같은 출력, 외부에 영향 없음
const add = (a: number, b: number): number => a + b;
const calculateDiscount = (price: number, rate: number): number =>
Math.round(price * (1 - rate));
// 언제 호출해도 결과 동일, 외부 변화 없음
부수 효과 = 외부 세계 변화
- 부수 효과는 함수의 주목적 외에 일어나는 관찰 가능한 변화
function saveToCart(product: Product): void {
cart.push(product); // 상태 변경
localStorage.setItem('cart', JSON.stringify(cart)); // I/O
console.log('저장됨:', product.name); // 출력
}
function getCurrentTime(): number {
return Date.now(); // 시간 의존
}
// 실행할 때마다 외부 세계가 변함
이번 과제에서 내가 제일 신경 쓴 부분은 무엇인가요?
순수 함수 영역 늘리기
- 계산과 액션을 구분하고 순수 함수의 영역을 최대한 높게 잡아라
계산과 액션, 엔티티를 다루는 함수와 그렇지 않은 함수를 구분하는 것에 신경을 많이 썼습니다. 발제 자료도 반복해서 읽어보고 적용해보려는 시도를 했습니다.
그래서 다른 형식의 리팩토링에는 드는 수고와 시간을 줄이고 위 내용에 대한 고민을 하는 데에 기력을 집중했습니다.
// 계산 함수
const calculateBulkDiscount = (baseDiscount: number, bonusRate: number = 0.05): number =>
baseDiscount + bonusRate;
const applyDiscountCap = (discountRate: number, maxDiscount: number = 0.5): number =>
Math.min(discountRate, maxDiscount);
const calculateRemainingStock = (stock: number, usedQuantity: number): number =>
stock - usedQuantity;
export const applyDiscountPercentToTotal = (
price: number,
quantity: number,
discountRate: number
) => Math.round(price * quantity * (1 - discountRate));
// 계산 함수의 조합
const getBaseDiscount = (
discounts: { quantity: number; rate: number }[],
quantity: number
): number =>
discounts.reduce(
(maxDiscount, discount) =>
quantity >= discount.quantity && discount.rate > maxDiscount
? discount.rate
: maxDiscount,
0
);
const getMaxApplicableDiscount = (
baseDiscount: number,
hasBulkPurchase: boolean
): number => {
if (hasBulkPurchase) {
const bulkDiscount = calculateBulkDiscount(baseDiscount);
return applyDiscountCap(bulkDiscount);
}
return baseDiscount;
};
// 도메인이 관여된 변환 함수
const hasBulkPurchase = (cartItems: CartItem[], bulkThreshold: number): boolean =>
cartItems.some((cartItem) => cartItem.quantity >= bulkThreshold);
export const getRemainingStock = (product: Product, cart: CartItem[]): number => {
const cartItem = cart.find((item) => item.product.id === product.id);
const usedQuantity = cartItem?.quantity || 0;
return calculateRemainingStock(product.stock, usedQuantity);
};
// 도메인 관여도가 높은
export const calculateItemTotal = (item: CartItem, allCartItems: CartItem[]): number => {
const { price, discounts } = item.product;
const { quantity } = item;
const baseDiscount = getBaseDiscount(discounts, quantity);
const discount = getMaxApplicableDiscount(
baseDiscount,
hasBulkPurchase(allCartItems, 10)
);
return applyDiscountPercentToTotal(price, quantity, discount);
};
순수 함수를 최대한 쪼개고 도메인 요소, 부수 효과가 일어나는 동작을 최대한 논리적으로 후반부 영역에서 일괄적으로 처리하고자 했습니다. 명쾌하게 답을 냈다는 느낌은 못받았습니다. 하지만 과정을 반복하면서 느낀 점이 있습니다 순수 함수를 독립했을 때 책임이 명확해지고 더 추적이 명료해진다는 것입니다.
코드를 이해하거나 디버깅을 해야할 때 함수형 코딩이 필요하다 이야기되는 이유를 체감할 수 있었습니다.
핸들러 함수 전략
핸들러 함수를 어디에 위치할지 여러 경우의 수가 떠올랐습니다.
- 관련 도메인의 useHook
- 컴포넌트에 정의
- 핸들러만 모은 hook
선택- UI 컴포넌트 최대한 분리하자는 과제의 취지에 맞춰서 훅으로 분리할까 싶었습니다. 일단 분리는 함수쪽에서 집중적으로 하고 다른 영역에서는 기존에 추구하던 방향으로 개발을 해서 컴포넌트에 핸들러를 구현했습니다.
- 비즈니스 로직: Hook에서 액션 함수로 제공
- UI 핸들러: 실제 사용하는 컴포넌트에서 정의
- 이벤트 핸들러: Presenter layer에서 분리 (인라인 X)
function ProductForm() {
// 비즈니스 액션은 Hook에서
const { addProduct } = useProducts();
const { onError } = useNotifications();
// UI 핸들러는 컴포넌트에서
const handleSubmit = (formData: ProductFormData) => {
if (!validateProduct(formData)) {
onError(ERROR_MESSAGES.INVALID_PRODUCT_DATA);
return;
}
addProduct(formData); // 비즈니스 액션 호출
};
return <form onSubmit={handleSubmit} />;
}
UI 이벤트를 처리하는 핸들러는 UI 컴포넌트의 책임입니다. 그렇기 떄문에 컴포넌트와 물리적 거리가 멀어져서는 안된다 생각합니다.
다만 내부에서 동작하는 비즈니스 로직들이 데이터를 조작하는 경우에는 그 동작들을 훅으로 빼어서 핸들러 내부에서 동작하게끔 설계했습니다.
대신 렌더링과 최적화의 관점에서 핸들러가 매번 생성되지 않도록 조절하기 위한 고민은 필요하다 생각됩니다.
props drilling
프롤 드릴링 지지협회의 일원으로서 기본과제의 주제는 흥미로웠습니다. 사실 아직 props drilling의 단점을 제대로 못느껴봐서 문제라고 생각 못하는 것이라고 생각이 있었습니다.
단순히 가독성면, 귀찮은 면에서는 이번 과제를 하면서도 여전히 문제점보다는 장점이 많다 생각이 들었습니다. 손이 많이 가긴 하지만 인지 부하가 들지 않는 단순한 타이핑이라서 데이터 흐름을 명시하고 작성하면서 그걸 파악할 수 있는 점은 좋았습니다.
다만 새롭게 고려해본 관점들이 있었습니다.
- 삭제
- 테스트 가능성
이는 새롭고 실효적이다 싶은 문제점들이었습니다. 동시에 아직까지는 많이 겪어보지 못한 일이었습니다.
- 삭제의 복잡성 - 연쇄 의존성 문제 어느 기능을 삭제해야할 때 수월하게 삭제할 수 있어야 한다. 이 이야기는 새롭게 클린코드를 바라볼 수 있는 관점이었습니다.
작성/해석의 관점에서의 편의성만 보던 입장에서와는 또 다른 요소를 따져봐야했습니다.
만약 전역상태관리를 적용하기 전에 addNotification이 관련된 로직을 제거해야 한다고 했으면 컴포넌트 분리도에 따라 다르지만 거의 모든 컴포넌트에 접근해서
- props 인터페이스
- 컴포넌트 인자
- 컴포넌트 속성
전부 접근해서 수정해야 합니다.
- 테스트 가능성
테스트 가능성에 대한 이야기도 와닿았습니다. props drilling이 있을 경우
describe('ProductForm', () => {
test('상품 추가 테스트', () => {
const mockOnSuccess = jest.fn();
const mockOnError = jest.fn();
const mockProducts = [...];
const mockSetProducts = jest.fn();
render(
<ProductForm
products={mockProducts} // 모킹 1
setProducts={mockSetProducts} // 모킹 2
onSuccess={mockOnSuccess} // 모킹 3
onError={mockOnError} // 모킹 4
productForm={mockProductForm} // 모킹 5
setProductForm={mockSetProductForm} // 모킹 6
editingProduct={null} // 모킹 7
setEditingProduct={mockSetEditing} // 모킹 8
/>
);
// 8개의 props을 모두 올바르게 모킹해야 함
});
});
- 과도한 모킹: 실제 사용하지 않는 props도 모킹 필요
- 모킹 일관성: 모든 테스트에서 동일한 모킹 구조 반복
- 변경 취약성: props 변경 시 모든 테스트 수정 필요
- 실제 동작과 괴리: 모킹된 함수가 실제 비즈니스 로직과 다를 수 있음
mock의 경우에는 최대한 간단하게 최소한의 구현만으로 수행하는 것이 관건이라고 생각합니다. 그런 점에서 위 경우처럼 불필요한 과정이 소모된다는 것은 치명적이라 느꼈습니다
jotai 적용시
describe('ProductForm', () => {
test('상품 추가 테스트', () => {
render(
<Provider>
<ProductForm />
</Provider>
);
});
});
반면 jotai는 불필요한 모킹데이터를 prop으로 주입해주지 않아도 테스트가 가능하다.
이런 문제점들을 과제를 하면서 느꼈습니다. 실제 개발을 하면서는 테스트 경험도 많이 없고 이미 있는 기능을 삭제해야하는 경험이 없었습니다.
이런 경험들을 더 큰 규모, 더 복잡한 상황에서 해보고 싶다는 욕심이 생겼습니다. 실제 서비스에서 이런 차이가 어떤 구체적인 생산성 향상으로 이어지는지 경험해보고 싶습니다.
이번 과제를 통해 앞으로 해보고 싶은게 있다면 알려주세요!
디자인 패턴 중 Decorator 패턴을 간단하게나마 경험해봤습니다. 멘토링 중 코치님이 작성한 코드를 통해서도 접했습니다.
이 패턴을 통한 코드 작성에 익숙해진다면 더 좋은 코드를 작성할 수 있을 것 같아서 활용해보고 싶다 느꼈습니다.
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문 편하게 남겨주세요 :)
코드 내부에 검색창의 로직이 컴포넌트에 있는 안티패턴 이런 주석이 달려있어서 단순히 인라인에 있는 것을 분리했습니다.
해당 안티패턴에 대해 정확히 이해한 느낌이 아니여서 관련해서 리뷰받아봤으면 합니다.
과제 피드백
두현~ 수고했습니다! 이번 과제는 React의 hook과 함수형 프로그래밍을 깊이 이해하고, 액션과 순수함수를 분리하면서 엔티티와 UI를 체계적으로 구분하는 것이 목표였습니다. 작성한 회고를 보니 진짜 본인의 성찰과 인사이트를 많이 느낀것 같아서 뿌듯하네요
"'프론트엔드 개발에서 지나치게 함수형 코딩에 과몰입하는 것은 부작용이 있다'라는 경계를 가지고 있었습니다." 맞아요. 실제로 그렇죠. 그렇지만 무엇이 부작용을 가져오는지 무엇을 이점이 가져오는지는 겪어봐야만 알 수 있는 법이죠. 이번 과제만으로는 다 이해하기는 어렵겠지만 많은 시행착오등을 겪어보기를 바래요.
props drilling에 대한 고민도 흥미롭게 읽었습니다. "프롤 드릴링 지지협회의 일원" 이라니 훌륭합니다! 삭제의 복잡성과 테스트 가능성이라는 실무적 관점에서 대해서 새로운 시야가 생겼다는 점이 인상깊네요. 프롤 드릴링 지지협회의 일원으로써 좋은 props drilli만 많이 남겨주길 바래요!
핸들러 함수 전략에서 비즈니스 로직은 Hook으로, UI 핸들러는 컴포넌트에 두는 방식으로 구분한 것도 좋은 선택이었습니다. UI 이벤트 처리는 확실히 UI 컴포넌트의 책임이죠. 도메인 컴포넌트의 책임은 사용자의 액션에서도 도메인 로직을 처리하기 위한 데이터를 뽑아다가 전달하는 역할인만큼 지금의 접근법은 상당히 균형잡혀 보여요.
질문하신 검색창 로직의 안티패턴에 대해 답변드리자면, AI에게 시키다 보니 그런 주석을 만들어 주었고 제가 삭제를 하지 못했었는데 유틸로 분리할 수 있는 훅 (debounce)와 검색의 기능이 한데 묶여 인라인으로 들어가있는 정도의 안티패턴이었어요. 우리는 이제 각자의 자리와 책임을 아니까 같이 있으면 안되는 책임과 자리를 가진 패턴이라서 안티패턴이라 명명했습니다.
Decorator 패턴에 관심을 가지신 것도 좋습니다. 이 패턴은 기존 객체를 수정하지 않고 새로운 기능을 추가할 때 유용한데, React의 HOC나 커스텀 훅 조합에서도 비슷한 개념을 활용할 수 있어요. 앞으로 더 큰 규모의 프로젝트에서 이런 패턴들을 적용해보면서 실무적 감각을 키워나가시길 바랍니다.
수고하셨습니다. 다음 주차도 화이팅입니다!