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

[클린코드 리액트 3기 박재영] 장바구니 미션 Step2 #60

Open
wants to merge 39 commits into
base: saewookkang
Choose a base branch
from

Conversation

SaeWooKKang
Copy link

안녕하세요 정혁님 😄 step2도 잘 부탁드립니다!

Pull Request Template

1. 가이드

1-1. 배포

docs

1-2. 로컬 환경

# install
yarn

# run
yarn dev

2. 리뷰 요청 ✍️

2-1. 어렵거나 아쉬웠던 점

  • 비 로그인 유저의 상품 데이터를 로컬 저장소와 쿠키중 어디에 저장할까 고민이었습니다. 쿠키에 저장시 api 요청을 하면 자동으로 헤더에 담겨 관리하기 편리하고 쿠키 만료 기간도 서버에서 설정 할 수 있어 편리할것 같아서 쿠키에 저장하고, 쿠키의 값을 상태와 동기화 시켰습니다.
  • 메인 페이지부터 장바구니 페이지까지의 흐름을 msw로 모킹했는데, 공수가 생각보다 많이 드는것 같습니다.
  • 시간관계상 장바구니 페이지의 컴포넌트를 분리하지 못한것이 아쉽습니다. 😭

2-2. 나누고 싶은 고민

  • 낙관적 업데이트를 어떻게 처리할 지 고민입니다. 현재는 useMutaion없이 useQuery로 장바구니 페이지에서 상품 카운트 증가, 감소, 삭제 등 카트 정보 변경을 모두 하나의 api로 처리하고 있어서 api 호출시 UI가 꿀렁이는 현상이 있습니다. 😅 혹은 상품의 선택여부를 프론트의 상태로 관리하는 것처럼 상품의 카운트도 프론트의 상태로 관리한다면 해결 될 수 있을것도 같네요 ㅎㅎ

2-3. 중점적으로 리뷰받고 싶은 부분

  • 지난번과 마찬가지로 이해하기 어려운 부분 혹은 추가하면 좋을것이 있다면 리뷰 달아주시면 감사하겠습니다.

2-4. 새로운 시도 혹은 미션을 통해 도전한 점

  • 메인 페이지에서 상품을 담을 때 사용할 전역 스토어가 필요했는데 최근 마이크로 상태 관리 관련 책을 읽었던 내용을 기반으로 모듈 스토어를 구현해 보았습니다.
  • 뒤로가기시 스크롤 복구 기능을 @tanstack/react-routerScrollRestoration를 활용해 구현했습니다.
  • 비지니스 로직의 응집도를 높이기 위해 페이지의 viewmodel을 작성했습니다.
  • zod의 맛을 아주 살짝 보았습니다. 🫨

2-5. 도식화된 자료 혹은 작성된 이미지 형태의 설계 구조

-

3. 질문있어요 🙋

  • 정혁님께서는 장바구니 데이터를 어디에 저장하는게 좋다고 생각하시는지 궁금합니다 🤔

4. 요구 사항 ✅

4-1. 필수 요구사항

필수 요구사항

  • [�x] TanStack Query를 기반으로 상태 분리
    • 낙관적 업데이트를 활용하여 UX/UI 증진
    • refetch() 사용 금지 있다면 제거
  • MSW를 활용한 API mocking
    • Endpoint만 변경하면 언제든 Real API를 바라볼 수 있다고 가정하고 상상한다.
      • Real API 없이 로컬에서만 동작하는 상태로 리뷰 받는 것이 기본 원칙
  • 상태 관리 라이브러리가 필요하다면 추가적으로 선택하고 적용한다.
    • 전략을 세우고 PR 본문에 내용을 작성한다.
  • 선택한 상태 관리 라이브러리에 대응되는 테스트 전략을 세우고 PR 본문에 내용을 작성한다.
    • 없다면 React Testing Library & Jest를 활용해 자유로운 단위의 테스트라도 진행한다.

상품목록

  • 페이징 혹은 인피니티 스크롤 적용
  • 뒤로가기 및 페이지 전환시 기존 페이지 및 스크롤 위치 기억

장바구니

  • 해당 상품의 수량을 변경할 수 있다.
  • 상품의 수량은 항상 1이상, 20이하여야 한다
    • 상품의 수량이 1이면 상품 수량 감소할 수 없다.
    • 상품의 수량이 20이면 상품 수량 증가할 수 없다.
  • 해당 상품의 총 금액이 변경된다.
  • 해당 상품이 체크되어있으면, 결제예상금액도 변경된다.
  • 모든선택 버튼이 체크되면, 상품들이 모두 선택된다.
    • 모든선택 버튼이 체크가 풀리면, 상품들의 선택이 모두 해제된다.
    • 상품 삭제 버튼을 누르면, confirm 메시지가 보여진다.
      • 확인을 누르면, 선택된 상품이 모두 삭제된다.
      • 결제예상금액이 0원이 된다.
  • 🗑 버튼을 누르면 confirm 메시지가 보여진다.
    • 확인을 누르면, 해당 상품이 삭제된다.
  • 체크된 상품 개수에 따라 주문하기 버튼 내부에 수량이 변경된다.
  • 주문하기 버튼을 누르면, confirm 메시지가 보여진다.
    • 확인을 누르면, 주문/결제 페이지로 이동한다.
    • 확인을 누르면, 장바구니에서 선택된 상품들이 삭제된다.
    • 확인을 누르면, 체크된 상품들을 데이터베이스에서 제거한다.
  • 주문할 상품이 0개이면 버튼이 비활성화된다.

4-2. 선택 요구사항 (심화)

  • 반응형 레이아웃을 구현한다.

Copy link

@zereight zereight left a comment

Choose a reason for hiding this comment

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

안녕하세요! 재영님! 리뷰 늦게 해서 죄송합니다!

전반적으로 hook으로 분리가 잘 되어있어서 읽기에 편했어요!
리뷰할것도 자잘한거 밖에 없었습니다.

혹시 추가적인 기능구현도 하실 계획이실까요?
이 정도 퀄리티면 지금 머지해도 충분하다고 생각이 들어서요 ㅋㅋ
일단 다시 request드리고 재요청주시면 merge해도될거같습니당

의견

  • 개인적으로는 api 에러처리가 되어있었으면 하는 마음이 들었네요!
    (api에서 에러가나면 메인페이지에서 무한 렌더링이 발생하는것 같아서)
  • 장바구니에서 수량을 변경하면 새롭게 fetch하는거같아요 ㅋㅋ UI가 새로 불러와지네요.

답변

데이터를 어디에 저장할까

  • 상품 데이터의 경우 많다면 엄청 많을수도 있겠다는 생각
  • 비로그인 사용자의 상품 데이터는 구매하기 전까지 클라이언트만 알고 있으면 된다는 생각
  • 상품 데이터가 그렇게 취약한 정보는 아닐거라는 생각
    위 3가지 이유때문에 저라면 localStorage에 해버렸을거같아요! ㅋㅋ

낙관적 업데이트를 어떻게 처리할 지 고민입니다.

네네 말씀주신것처럼 프론트상태로 받아와서 저장해도 구현할 수 있고
mutate써서 후속동작으로 query data를 직접변경하는 방법도 있을거같아요!

@@ -0,0 +1,38 @@
import { CookieOptions } from './type'

Choose a reason for hiding this comment

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

요거 유틸함수 같은데, useCookieRepository 에서만 사용할 유틸이라 여기서 정의한것인가 생각했었는데

getCookie 가 다른 곳에서도 쓰이고 있네요!

저라면 util폴더에 놔뒀을것 같긴합니다!
아니면 재영님이 어떤 기준을 가지고 넣었다면 괜찮은거 같습니다 ㅎㅎ

Copy link
Author

Choose a reason for hiding this comment

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

원래는 useCookieRepository 훅에서 쿠키 상태를 관리하려고 해당 폴더 하위에 만들어 두었었는데요 ㅎㅎ useCartListStore 훅에서 전역 상태로 관리하는것으로 변경하면서 해당 유틸 함수의 위치른 변경하지 않았었습니다. 말씀해주신대로 utils 하위로 수정해두었습니다!

refactor: cookie get, set 함수 utils 하위로 이동

const cookie = document.cookie
.split('; ')
.find((row) => row.startsWith(`${name}=`))
?.split('=')[1]

Choose a reason for hiding this comment

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

쿠기에 '=' 값이 들어가도 정상적으로 동작할까 싶었느데, MDN 공식문서에 그렇게 되어있네요 흠..

Comment on lines 28 to 35
const cookieOptions = Object.entries(options).reduce((prev, [key, value]) => {
switch (key) {
case 'expires':
return (prev += `; expires=${(value as Date).toUTCString()}`)
default:
return (prev += `; ${key}=${value}`)
}
}, '')

Choose a reason for hiding this comment

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

prop을 직접 수정하는건 지양해보는게 어떨까요?
const newValue = ~~ 형식으로 새로운 값을 반환하는게
불변성을 지키는 측면 등등에서도 좋아보입니다 :)

Copy link
Author

Choose a reason for hiding this comment

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

언급하신 prop이 options를 말씀하시는걸까요? 👀 options를 const newValue = ~~와 같은 형식으로 deep copy하는게 좋다는 말씀이신지 궁금합니다!

Comment on lines +1 to +35
export interface Store<T> {
getState: () => T
setState: (next: T | ((prev: T) => T)) => void
subscribe: (callback: () => void) => () => void
}

/**
* @summary 스토어를 생성한다.
*/
export const createStore = <T>(initialValue: T): Store<T> => {
let state = initialValue
const callbacks = new Set<() => void>()

const getState = () => {
return state
}

const setState = (next: T | ((prev: T) => T)) => {
state = typeof next === 'function' ? (next as (prev: T) => T)(state) : next

callbacks.forEach((callback) => callback())
}

const subscribe = (callback: () => void) => {
callbacks.add(callback)

return () => callbacks.delete(callback)
}

return {
getState,
setState,
subscribe,
}
}

Choose a reason for hiding this comment

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

오 직접 구현하셨군요 대단하십니다..!

return unsubscribe
}, [store])

return [state, store.setState] as const

Choose a reason for hiding this comment

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

useState와 사용성을 맞춰주셨군요 👍

Comment on lines 32 to 40
foundProduct
? setValue((prev) => ({
...prev,
cartList: [
...prev.cartList.filter((product) => product.id !== id),
{ id, count: foundProduct.count + 1 },
],
}))
: setValue((prev) => ({ ...prev, cartList: [...prev.cartList, { id, count: 1 }] }))

Choose a reason for hiding this comment

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

개인적으로 삼항연산자는 식으로 쓰이기 때문에, 값으로 평가되는 곳에 사용하는게 목적에 맞다고 생각합니다.

간단하게 쓰이는 용도로 쓰셔도 되는데, 지금과 같은 곳에서는
if/else 처럼 '문'으로 쓰이도록 하는 편입니당 ㅎㅎ

개인차이라서 넘어가셔도 될거같아요!

Copy link
Author

Choose a reason for hiding this comment

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

말씀하신대로 표현식과 문을 혼용해서 사용했었네요 ㅎㅎ 🤣 반영 했습니다!

refactor(cartListStore.ts): 맥락에 맞게 삼항연산자(표현식) -> if 문으로 변경

Comment on lines +1 to +4
export const CART_MIN_COUNT = 1
export const CART_MAX_COUNT = 20

export const CART_COOKIE_KEY = 'CARTS'

Choose a reason for hiding this comment

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

상수화 👍

setSelectedProductList((prev) => {
const newMap = new Map(prev)

if (newMap.get(id)) {

Choose a reason for hiding this comment

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

has 로 해도 되었지 않았을까 하는 생각이 들었습니당 ㅎ_ㅎ!

Copy link
Author

Choose a reason for hiding this comment

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

말씀해주신게 더 의미가 적절할 것 같습니다. 감사합니다!

refactor(useCartVM.ts): 값의 존재 여부 확인을 get -> has 로 변경


<div className="number-input-container">
<input
key={product.count}

Choose a reason for hiding this comment

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

오잉 요거 key가 왜 count 일까요..!

Copy link
Author

@SaeWooKKang SaeWooKKang May 1, 2024

Choose a reason for hiding this comment

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

ㅎㅎ; 해당 input은 상품의 개수를 변경할 수 있는 역할을 합니다. input이 controlled, uncontrolled로 다룰 수 있을텐데 먼저 controlled일 경우 value, onChange props를 내려줘야 하는데 onChange를 사용하게 될 경우 입력된 value를 변경하고 사용자가 입력을 멈췄다는 플래그가 추가로 필요할 것 같습니다. 그래서 defaultValue로 기본값을 할당하고 onBlur시 상품의 카운트를 바꾸는 uncontrolled 방식으로 input을 다루게 되었습니다. 그런데 defalutValue때문인지 상품의 갯수 증감시 리렌더링이 되지 않더라구요 🤔 그래서 key값으로 강제 리렌더링 되도록 했습니다.

import { ProductCard } from '../ProductCard/ProductCard'

/**
* @summary 상품목록 컴포넌트
*/
export const ProductList = () => {
const { data, isLoading, isError } = useQuery<ProductsListType>({
const { ref, inView } = useInView()
const { data, status, isFetchingNextPage, hasNextPage, fetchNextPage } = useInfiniteQuery({
queryKey: ['/posts'],

Choose a reason for hiding this comment

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

갑자기 든 생각인데 왜 'posts' ㅋㅋㅋ

Copy link
Author

Choose a reason for hiding this comment

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

@SaeWooKKang
Copy link
Author

안녕하세요 정혁님! 리뷰 남겨주셔서 감사합니다.😊

코멘트 달아주신 부분들 수정 혹은 댓글로 제 생각과 질문을 남겼습니다.

장바구니에서 수량을 변경하면 새롭게 fetch하는거같아요 ㅋㅋ UI가 새로 불러와지네요.

queryClient.invalidateQueries를 활용하여 백그라운드에서 데이터를 가져오도록 수정해보았습니다. 추후 낙관적 업데이트를 적용해 UX를 향상시켜볼 생각입니다.

혹시 추가적인 기능구현도 하실 계획이실까요?
이 정도 퀄리티면 지금 머지해도 충분하다고 생각이 들어서요 ㅋㅋ
일단 다시 request드리고 재요청주시면 merge해도될거같습니당

낙관적 업데이트를 추가적으로 적용해볼 생각입니다. 이 부분은 개인 레파지토리에서 수행 하겠습니다ㅎㅎ
이번 클린코드 3기는 여기서 하산 하겠습니다. 그동안 꼼꼼한 리뷰 감사드립니다 🙏

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