-
Notifications
You must be signed in to change notification settings - Fork 204
[1단계 - 장바구니 미션] 에리얼(정예지) 미션 제출합니다. #327
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
Conversation
Co-Authored-By: YEJI <[email protected]>
Co-Authored-By: YEJI <[email protected]>
Co-Authored-By: YEJI <[email protected]>
Co-Authored-By: YEJI <[email protected]>
Co-Authored-By: YEJI <[email protected]>
Co-Authored-By: YEJI <[email protected]>
Co-Authored-By: YEJI <[email protected]>
Co-Authored-By: YEJI <[email protected]>
Co-Authored-By: YEJI <[email protected]>
Co-Authored-By: YEJI <[email protected]>
Co-Authored-By: YEJI <[email protected]>
Co-Authored-By: YEJI <[email protected]>
Co-Authored-By: YEJI <[email protected]>
Co-Authored-By: YEJI <[email protected]>
Co-Authored-By: YEJI <[email protected]>
Co-Authored-By: YEJI <[email protected]>
Co-Authored-By: YEJI <[email protected]>
Co-Authored-By: YEJI <[email protected]>
Co-Authored-By: YEJI <[email protected]>
Co-Authored-By: YEJI <[email protected]>
Co-Authored-By: YEJI <[email protected]>
Co-Authored-By: YEJI <[email protected]>
Co-Authored-By: YEJI <[email protected]>
Co-Authored-By: YEJI <[email protected]>
Co-Authored-By: YEJI <[email protected]>
Co-Authored-By: YEJI <[email protected]>
Co-Authored-By: YEJI <[email protected]>
Co-Authored-By: YEJI <[email protected]>
Co-Authored-By: YEJI <[email protected]>
Co-Authored-By: YEJI <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
안녕하세요 에리얼!
장바구니 미션을 함께하게 된 리뷰어 해먼드입니다 ㅎㅎ
앞으로 잘 부탁드려요!
컴포넌트를 무척 잘 분리해주셨어요!
props도 어색함 없이 정의를 너무 잘 해주셨네요 ㅎㅎ
주로 제가 네이밍에 대한 리뷰를 많이 남기는 편인데, 에리얼의 리뷰에는 네이밍에 대한 리뷰를 거의 남기지 않았네요 ㅋㅋ
그리고 재활용할 수 있는 부분을 최대한 잘 분리해주셔서 무척 보기 좋았습니다.
고생하셨어요!
1️⃣ isChecked 개선 방향
상세 피드백에 남겨놓긴 했는데요, 상태가 아니라 값으로 관리를 하시면 좋답니다 ㅎㅎ
const checkedItems = useMemo(() => new Set(cart.data.map((item) => item.id)), [cart.data])
이러면 cart.data 의 값이 변경될 때 자동으로 값을 만들어주는거죠.
2️⃣ Props를 통한 상태 전달 구조
이것도 상세 피드백에 남겨놓았는데요, 재사용을 기준으로 판단하시면 좋을 것 같아요!
재사용 단위가 CartList 라고 치면 그냥 컨텍스트를 사용하는게 제일 편할 것 같긴 합니다 ㅋㅋ
3️⃣ 관심사 분리
지금 모습도 나쁘진 않다고 생각해요.
여기서 더 나아가자면, 각각의 함수를 훅으로 분리할 수 있지 않을까 싶네요!
const useCart = () => {
const toggle = useCartToggle();
const update = useCartUpdate();
...
}
요로코롬..
다만 지금은 각각의 함수와 상태가 서로 얽혀있다보니 분리하기가 쉽진 않을 것 같습니다.. ㅠㅠ
마지막으로 lint, tsc 등이 현재 실패하고 있어서 한 번 확인 부탁드려요!
테스트도 통합 테스트만 있고, 단위 테스트가 없어서 아쉽네요 ㅠㅠ
그리고 실패하는 케이스에 대한 테스트가 없어서 이에 대한 부분도 채워지면 좋겠어요!
테스트 커버리지를 항상 잘 측정해주세요!
src/api/cart.ts
Outdated
import { CartResponse } from '@/features/Cart/types/Cart.types'; | ||
import { ProductQuery, UpdateCartItem } from '@/features/Cart/types/Product.types'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
api 폴더에서 feature를 의존하고 있네요..!
그렇다면 차라리 cart라는 이 api가 아예 feature 폴더 내부에 있어야 하는게 아닐까요!?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cart 관련 api를 features/Cart 내부로 이동하여 응집도를 향상시켰습니다.
src/api/fetcher.ts
Outdated
const url = new URL(ENV.BASE_URL + path); | ||
Object.entries(query || {}).forEach(([key, value]) => { | ||
if (value !== undefined && value !== null && String(value)) { | ||
url.searchParams.append(key, String(value)); | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
쿼리를 관리하는 부분을 유틸 함수로 분리한 다음 테스트로 검증해도 좋을 것 같아요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
쿼리 문자열 생성 로직을 buildQueryParams 유틸로 분리했습니다.
위치는 src/shared/utils/url.ts
입니다.
src/api/fetcher.ts
Outdated
const headers = { | ||
'Content-Type': 'application/json', | ||
Authorization: `Basic ${ENV.TOKEN}`, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이런건 DEFAT_HEADER 라는 상수로 만들어서 관리해도 좋아보입니다 ㅎㅎ
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
반영했습니다!
src/api/fetcher.ts
Outdated
if (!response.ok) { | ||
// const errorData = await response.json().catch(() => null); | ||
// throw new HttpError(response.status, errorData); | ||
throw new Error(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
어땐 에러인지 명시해주면 좋을 것 같아요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fetcher 내부에서의 에러 상황을 throw new Error() 대신, 커스텀 HttpError 클래스를 활용해 명확하게 처리하도록 개선했습니다.
if (response.status === 204 || response.headers.get('content-length') === '0') { | ||
if (returnOriginalOnNoContent && body) { | ||
return body as unknown as T; | ||
} | ||
return returnOriginalOnNoContent as unknown as T; | ||
} | ||
|
||
return await response.json(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (response.status === 204 || response.headers.get('content-length') === '0') { | |
if (returnOriginalOnNoContent && body) { | |
return body as unknown as T; | |
} | |
return returnOriginalOnNoContent as unknown as T; | |
} | |
return await response.json(); | |
return await response.json(); |
어차피 응답이 빈 값일꺼라서, await response.json() 으로 바로 넘어가도 차이가 없지 않을까요!?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
의견 주신 부분과 관련해서 궁금한 점이 있어 질문 드립니다!
바로 await response.json()
을 호출하게 되면,
응답 본문이 {}
같은 빈 객체가 아니라 아예 없을 경우(204 No Content
나 content-length: 0
),
SyntaxError: Unexpected end of JSON input
같은 에러가 발생하진 않을까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
앗.. 그렇네요 ㅎㅎ
제가 잘못알고 있었네요 ㅠㅠ 죄송합니다.
const hasCheckCartLength = cartItems?.filter((item) => item.isChecked).length; | ||
const totalQuantity = cartItems?.reduce( | ||
(acc, item) => acc + (item.isChecked ? item.quantity : 0), | ||
0 | ||
); | ||
const totalPrice = cartItems?.reduce( | ||
(acc, item) => acc + (item.isChecked ? item.product.price * item.quantity : 0), | ||
0 | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이런 것들도 훅으로 묶어줄 수 있겠네요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OrderConfirm 컴포넌트 내 선택된 상품 수량, 총 수량, 총 결제 금액 계산 로직을 useOrderInfo 훅으로 분리했습니다.
const orderPrice = cartItems | ||
.filter((item) => item.quantity > 0 && item.isChecked) | ||
.reduce((acc, cart) => { | ||
return acc + Number(cart.product.price) * Number(cart.quantity); | ||
}, 0); | ||
|
||
const deliveryFee = orderPrice >= 100000 ? 0 : 3000; | ||
const totalPrice = orderPrice + deliveryFee; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
앞선 리뷰와 동일합니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PriceSummary 컴포넌트 내부의 가격 관련 계산 로직을 usePriceInfo 훅으로 분리했습니다.
src/features/Cart/pages/CartPage.tsx
Outdated
export const CartPage = () => { | ||
const { Funnel, setStep } = useFunnel<STEPS>('장바구니'); | ||
const { cartItems, toggleCheck, toggleAllCheck, removeCartItem, updateQuantity } = useCart(); | ||
|
||
return ( | ||
<> | ||
<Funnel> | ||
<Funnel.Step name="장바구니"> | ||
<CartInfo | ||
cartItems={cartItems ?? []} | ||
onToggle={toggleCheck} | ||
onToggleAll={toggleAllCheck} | ||
onRemove={removeCartItem} | ||
onUpdateQuantity={updateQuantity} | ||
onNext={() => setStep('주문정보')} | ||
/> | ||
</Funnel.Step> | ||
<Funnel.Step name="주문정보"> | ||
<OrderConfirm cartItems={cartItems ?? []} onPrev={() => setStep('장바구니')} /> | ||
</Funnel.Step> | ||
</Funnel> | ||
</> | ||
); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
결국 cart에 대한 값이 전부 하위 컴포넌트로 props를 통해 전파되고 있어요.
이 때 고민할 수 있는 포인트는 이렇게 두 가지라고 생각합니다.
- cart ui 를 재활용 할 것인가? -> 그렇다면 현재 모습 유지
- cart ui 에 대한 재활용 의사가 없는가? -> 그렇다면 핵심 로직을 전부 context로 묶어서 전파하기. 전체적인 흐름을 이해하기가 더 수월함.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cart ui는 재사용 가능성이 낮을 것 같아 핵심 로직을 context로 묶었습니다!
const deliveryFee = orderPrice >= 100000 ? 0 : 3000; | ||
const totalPrice = orderPrice + deliveryFee; | ||
|
||
return ( | ||
<Flex | ||
direction="column" | ||
justifyContent="flex-start" | ||
alignItems="flex-start" | ||
gap="10px" | ||
width="100%" | ||
padding="20px" | ||
> | ||
<Text type="Caption" weight="regular"> | ||
🛍️ 총 주문 금액이 100,000원 이상일 경우 무료 배송됩니다. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
100000 은 상수로 만들어서 관리해도 좋을 것 같네요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
반영했습니다!
src/features/Cart/hooks/useCart.ts
Outdated
useEffect(() => { | ||
if (cart.data && cart.data.length > 0 && !hasInitialized.current) { | ||
setCheckedItems(new Set(cart.data.map((item) => item.id))); | ||
hasInitialized.current = true; | ||
} | ||
}, [cart.data, checkedItems.size]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
checkdItems를 상태가 아니라 값으로 관리하면 어떨까요?
useEffect(() => { | |
if (cart.data && cart.data.length > 0 && !hasInitialized.current) { | |
setCheckedItems(new Set(cart.data.map((item) => item.id))); | |
hasInitialized.current = true; | |
} | |
}, [cart.data, checkedItems.size]); | |
const checkedItems = useMemo(() => new Set(cart.data.map((item) => item.id)), [cart.data]) |
이러면 useEffect를 사용할 필요가 없답니다 ㅎㅎ
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
현재 체크 상태는 사용자 상호작용(개별/전체 토글 등)에 따라 동적으로 변경되기 때문에,
상태로 관리해야 유연하게 처리할 수 있다고 판단했습니다!
말씀해주신 useMemo
방식은 cart.data
변화에만 반응하기 때문에 사용자 조작이 반영되기는 힘들 것 같은데 맞을까요 ??
안녕하세요 해먼드! 코멘트로도 커밋 주소 달아두겠습니다 ! 📦 리팩토링 내역 정리🗂 폴더 구조 개선
🧩 로직 분리 및 커스텀 훅화
📝 네이밍 및 구조 개선
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
안녕하세요 에리얼!
리뷰가 늦었네요 ㅠㅠ 죄송합니다.
전체적으로 잘 개선해주셔서 이번 단계는 여기서 마무리하고, 이어서 다음 단계 진행해주시면 될 것 같아요!
몇 가지 자잘한 리뷰 남겼으니 한 번 확인 부탁드립니다!
if (response.status === 204 || response.headers.get('content-length') === '0') { | ||
if (returnOriginalOnNoContent && body) { | ||
return body as unknown as T; | ||
} | ||
return returnOriginalOnNoContent as unknown as T; | ||
} | ||
|
||
return await response.json(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
앗.. 그렇네요 ㅎㅎ
제가 잘못알고 있었네요 ㅠㅠ 죄송합니다.
constructor(status: number, data?: unknown) { | ||
super(`HTTP Error ${status}`); | ||
this.name = 'HttpError'; | ||
this.status = status; | ||
this.data = data; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
constructor(status: number, data?: unknown) { | |
super(`HTTP Error ${status}`); | |
this.name = 'HttpError'; | |
this.status = status; | |
this.data = data; | |
constructor(public status: number, public data?: unknown) { | |
super(`HTTP Error ${status}`); | |
this.name = 'HttpError'; |
아마 이렇게 정의하면 자동으로 할당될 것 같아요!
javascript에서는 안 되고, typescript 에서만 가능한 문법이랍니다 ㅎㅎ
}, duration); | ||
}; | ||
|
||
const contextValue = useMemo(() => ({ showToast }), [showToast]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
지금 여기서 contextValue에 메모를 하는게 의미가 없어요.
어차피 showToast는 항상 새로운 값일테니까요..!
어째서 그런걸까요~?
direction={direction} | ||
justifyContent={justifyContent} | ||
alignItems={alignItems} | ||
gap={gap} | ||
margin={margin} | ||
padding={padding} | ||
width={width} | ||
height={height} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
gap 빼고 다 props로 그대로 넘겨줘도 될 것 같네요!
useEffect(() => { | ||
if (cart.error && isError(cart.error)) { | ||
showToast('장바구니 정보를 불러올 수 없습니다.'); | ||
} | ||
}, [cart.error, showToast]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
showToast를 의존하고 있는데요,
앞선 리뷰에 남긴 것 처럼 toast의 값이 변경 될 때 마다 showToast가 매번 변경되고,
showToast가 변경되면 ToastContext를 사용하는 구간이 다 렌더링되고,
이 useEffect 에서도 showToast를 의존하고 있어서 불필요하게 계속 실행될 가능성이 있어요..!
const imgUrl = | ||
!product.imageUrl || product.imageUrl === '' || product.imageUrl.includes('kream') | ||
? NoImage | ||
: product.imageUrl; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이런 부분도 유틸 함수로 만들어주면 어떨까 싶어요!
css={css` | ||
margin-top: 8px; | ||
`} | ||
onClick={() => onRemove(id)} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
삭제를 클릭하는거라
onRemove가 아니라
onRemoveClick 처럼 표현해야 적절할 것 같네요!
onIncrease={() => onUpdateQuantity(id, quantity + 1)} | ||
onDecrease={() => onUpdateQuantity(id, quantity - 1)} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
onUpdateQuantity 라는 이름이 내포하는건 이렇습니다.
- 현재 컴포넌트 내부에 quantity에 대한 상태가 정의되어 있고
- 해당 quantity를 변경하는 함수가 있고,
- 그 함수를 실행하여 quantity를 변경할 때 onUpdateQuantity 가 실행됨
const [quantity, setQuantity] = useState(0);
const updateQuantity = (newQuantity) => {
setState(newQuantity);
onUpdateQuantity(id, newQuantity);
}
이런 모습이랄까..
input의 이벤트에 대해 잘 고민해보시면
input은 자체적으로 value를 가지고 있어요
그리고 onChange 같은 이벤트가 발생하면 자신의 value를 event에 담아서 전달한답니다 ㅎㅎ
그래서 이벤트란 무엇일지 한 번 고민해보시면 좋아요!
이 상황에서는 어떻게 네이밍을 해야 좋을까요?
padding="20px" | ||
> | ||
<Text type="Caption" weight="regular"> | ||
🛍️ 총 주문 금액이 100,000원 이상일 경우 무료 배송됩니다. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FREE_DELIVERY_THRESHOLD 을 여기서도 사용할 수 있지 않을까요!?
<CartItemDetail | ||
key={item.id} | ||
onToggle={onToggle} | ||
onRemove={onRemove} | ||
onUpdateQuantity={onUpdateQuantity} | ||
{...item} | ||
/> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
각각의 CartItemDetail
이 지금은 매번 렌더링 될 수 밖에 없는 상황인데요,
필요할 때만 렌더링 되도록 만들어주려면 어떤식으로 개선이 되어야 좋을까요!?
📦 장바구니 미션
이번 미션을 통해 다음과 같은 학습 경험들을 쌓는 것을 목표로 합니다.
1단계
🕵️ 셀프 리뷰(Self-Review)
제출 전 체크 리스트
기능 요구 사항을 모두 구현했고, 정상적으로 동작하는지 확인했나요?
RTL 테스트 케이스를 모두 작성했나요?
배포한 데모 페이지에 정상적으로 접근할 수 있나요?
리뷰어가 장바구니 추가를 쉽게 할 수 있도록 Curl 명령어를 전달해주세요. (DM으로 보내겠습니다!)
🎥 시연 영상
2025-05-29.1.52.03.mov
리뷰 요청 & 논의하고 싶은 내용
안녕하세요 해먼드 ! 처음 뵙네요 🙇🏻♀️🙇🏻♀️
이번 미션은 세라랑 진행했습니다 ! 벌써 레벨2가 끝나간다니 시간이 너무 빠른 것 같아요 ,,
리뷰 잘 부탁드립니다 😀
1) 상태 설계 의도
서버에서 내려오는 상태를 제외한 대부분의 상태는 파생 상태로 보고, 가능하면 별도의 상태를 만들지 않는 방향으로 구현했습니다. 다만 체크 여부는 원본 상태의 유지가 필요하다고 판단해 별도로 상태로 관리하고 있습니다. 이때 각 항목의 고유성을 보장하고 효율적인 연산을 위해 Set 자료구조를 사용해 체크된 항목의 ID를 저장하고 있습니다. 또 이 데이터를 불러오는 useFetchData 를 구현했습니다. 이를 통해 API 호출에 대한 로딩, 에러, 데이터 상태를 통합적으로 관리하고, autoFetch 옵션을 활용해 초기 데이터를 자동으로 불러올 수 있도록 구성했습니다!
2) 이번 단계에서 가장 많이 고민했던 문제와 해결 과정에서 배운 점
1️⃣ MSW 미사용 이유와 테스트 선택
MSW를 사용하면 테스트 작성이 더 수월해질 수는 있지만, 테스트 문법이나 다양한 상황을 깊이 있게 학습하기에는 적합하지 않다고 판단했습니다. 또한 실제 API를 연동해야 하는 상황에서는 MSW 없이 테스트가 모두 깨지는 문제가 발생할 수 있기 때문에, 오히려 사용하지 않는 방향을 선택하게 되었습니다.
물론 MSW를 사용하지 않으면서 불편한 점도 있었습니다. 예를 들어, 테스트 코드에서
mockImplementation
를 지속적으로 조작하거나, render를 매번 호출해야 하는 점이 번거로웠습니다. 하지만 이러한 조작은 MSW를 사용하는 경우에도 결국 필요하다는 점에서, 테스트 흐름을 명확히 파악할 수 있는 직접적인 조작 방식을 선택했습니다.2️⃣ 사용자 경험 개선
요구사항에는 명시되지 않았지만, 현재 장바구니 기능을 구현하면서 “사용자가 어떻게 하면 실제 구매까지 이어질 수 있을까?“에 대해 고민해보았습니다. 그 과정에서 가장 눈에 띄는 요소는 무료 배송 혜택이었고, 이를 강조하기 위해 progress bar를 활용한 시각적 요소를 추가해 사용자에게 동기를 부여하고자 했습니다.
3️⃣ 퍼널 기반의 페이지 흐름 관리
현재는 페이지가 2개뿐이지만, 추후 결제 페이지 등 추가될 가능성을 고려해 퍼널 형태로 페이지 흐름을 관리하도록 구현했습니다. 라우터를 사용하지 않고 상태 기반으로 흐름을 전환함으로써, 페이지 전환 간의 복잡도를 낮추고 내부 상태 관리에 집중할 수 있도록 했습니다.
또한, 주문 확인 페이지에서 뒤로 가기 버튼을 눌러 장바구니로 돌아왔을 때에도 기존 데이터(상품 체크 상태 등)가 초기화되지 않고 유지되도록 처리하여 사용자 경험을 고려했습니다.
3) 이번 리뷰를 통해 논의하고 싶은 부분
1️⃣ isChecked 개선 방향
현재는 데이터를 불러온 이후,
useEffect
를 활용해 체크된 항목들을 1차적으로 동기화하고 있습니다. 이때 최초 렌더링 여부를 파악하기 위해useRef
를 사용하여 렌더링에 영향을 주지 않도록 처리했습니다. 이후 toggle이나 remove 등의 사용자 인터랙션에 대해서는useState
로 관리되는 checkedItems를 지속적으로 갱신하고 있는데, 이 구조가 최적의 방식인지 고민이 들었습니다.특히 처음 데이터를 불러온 이후의 동기화 타이밍, 그리고 이후 상태 업데이트 방식이 적절한지 확신이 서지 않아 개선 방향에 대한 조언을 구하고 싶습니다! 혹시 더 효율적인 동기화 또는 상태 관리 방식이 있을까요??
2️⃣ Props를 통한 상태 전달 구조
현재 장바구니 관련 상태와 함수는
useCart
에서 관리하고, 이를CartPage
에서 하위 컴포넌트로 prop을 통해 전달하고 있습니다.아직은 상태가 복잡하지 않다고 판단하여 props 기반으로 구현했지만, 향후 컴포넌트가 많아지고 상태가 늘어날 경우 Context API를 도입해 props drilling을 줄이는 것도 고려해볼 수 있다고 생각했습니다.
지금 단계에서 Context API를 도입하는 것이 좋을지, 리뷰어의 의견이 궁금합니다!
3️⃣ 관심사 분리
현재 useCart 훅은 데이터 불러오기부터 항목 토글, 삭제, 수량 변경 등 거의 모든 장바구니 관련 로직을 포함하고 있습니다. 그런데 이렇게 다양한 기능이 하나의 훅에 모두 포함되다 보니, 데이터 로직과 사용자 인터랙션 로직이 분리되지 않아 유지보수 측면에서 아쉬움이 있습니다.
관심사를 명확히 분리하기 위해 훅을 역할별로 나누는 방식을 고려하고 있는데, 그렇게 했을 때 서로 다른 훅 간에 필요한 데이터를 props나 매개변수로 계속 전달해야 할 가능성이 생겨 오히려 구조가 복잡해지지는 않을까 하는 고민도 있습니다. 이러한 상황에서 더 나은 구조를 만들 수 있는 방법이 있을지 조언을 듣고 싶습니다!
✅ 리뷰어 체크 포인트
1. 클라이언트 상태관리
2. MSW/Test