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

[NEXTLEVEL 클린코드 리액트 조기문] 장바구니 미션 Step2 #8

Open
wants to merge 58 commits into
base: guymoon
Choose a base branch
from

Conversation

guymoon
Copy link
Member

@guymoon guymoon commented Mar 9, 2022

웹 VSCODE 환경

바로가기

참고사항

  • test를 열심히 해보려고 했는데 주문 목록 기능은 아직 작성하지 못했습니다.
  • 주문 목록에서 서버에서 받아온 데이터를 리듀서 부분에서 수정해서 원본 데이터와 다른 형태로 스토어에 저장해 뷰 컴포넌트에서 조금 더 깔끔하게 보여주기 위해 노력했습니다. (ProductItem 컴포넌트를 주문 목록에서 재사용했는데 앞으로 어떻게 변경시킬지 고민해봐야될 것 같습니다)

궁금한 점

1

수량 감소 버튼을 눌렀을 때 물품의 수량이 1개라면 감소시키지 않는다. (현재 수량 1개 -> 감소 버튼 클릭 -> 현재 수량 1개)
여기서 수량이 redux store에서 관리되는 상태라면 이 수량에 대한 로직은 어디에 담기는게 이상적일까요?

  1. redux에서 현재 수량이 1개인데 수량 감소에 대한 액션이 디스패치되면 감소시키지 않는다.
  2. view를 담당하는 리액트 컴포넌트에서 현재 수량이 1개인 경우 -> 감소에 대한 액션이 디스패치되지 않도록 막는다.

상태의 min,max 값에 대한 부분이므로 리듀서 부분에 관련 로직을 담는 것도 맞는 것 같지만.. 현재 상태에 따라 액션이 디스패치되지 않도록 막는게 맞는 것 같기도하고 혼란스럽네요. 어떻게 생각하시나요?!

필수 요구사항

  • React Testing Library & Jest를 활용해 자유로운 단위의 테스트를 진행한다.
  • Redux 상태와 Hooks를 조합할 수 있는 패턴과 구조를 시도한다.

장바구니

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

주문/결제

  • 주문할 상품들의 정보가 보여진다.
  • 총 결제금액을 보여준다.
  • 결제하기 버튼을 클릭하면, confirm 메시지가 보여진다.
    • 확인 버튼을 누르면, 주문 목록페이지로 이동한다.

주문목록

  • 주문 정보들이 보여진다.
  • 장바구니 버튼을 클릭하면, 해당 상품이 장바구니에 담기고 장바구니 이동 선택 모달이 보여진다.
    • 장바구니 이동 버튼을 누르면 장바구니 페이지로 이동한다.

오늘 중으로 조금 더 내용 정리해서 보기 편하게 해드리겠습니다!

  - feat: cartsAsyncActions 생성
    - getCarts
    - deleteCartItemId
    - addCartItem

  - feat: deleteCardItemById(id값으로 장바구니 아이템 삭제) 기능 추가

  - feat: increaseCartItemQuantityById(id값으로 장바구니 아이템 수량 증가) 기능 추가

  - feat: decreaseCartItemQuantityById(id값으로 장바구니 아이템 수량 감소)

  - feat: getCarts(장바구니 정보 불러오기) 기능 추가

  - feat: deleteCartItemById(장바구니 아이템 삭제하기)
   기능 추가

  - feat: addCartItem(장바구니 아이템 담기) 기능 추가
  -feat: getCartsSaga(장바구니 정보 가져오기) 기능 추가

  -feat: deleteCartItemByIdSaga(장바구니 아이템 삭제) 추가

  -feat: addNewCartItemSaga(장바구니 아이템 추가) 기능 추가
  - CARTS: 'carts' 상수

  - GetCartsSuccessPayload: getCarts 성공 시 payload
  DeleteCartItemRequestPayload: deleteCartItem 요청 시 paylaod

  - DeleteCartItemSuccessPayload: deleteCartItem 성공 시 payload

  - CartsReducerState: cartsReducer 의 상태 타입

  - AddCartItemRequestPayload: addCartItem 요청 시 payload

  - AddCartItemSuccessPayload: addCartItem 성공 시 paylaod
  - 아무 key나 들어올 수 있는 형태에서 타입 고정하는 방식으로 수정

  - refactor: GetProductsResponseType key값 products로 고정

  - refactor: GetProductsErrorType 타입 삭제하고, BaseRequestFailure 타입으로 재사용 가능하도록 수정
  - feat: GET_ORDERS method와 url 추가
  - feat: POST_ORDER method와 url 추가
  - feat: GET_ORDER_BY_ID method와 url 추가
  - cartsFromServer: 리듀서에서 정재된 데이터 만들 기 전 원본 서버 데이터 형태를 가진 고정물

  - cartsReducerInitialState: cartsReducer의 초기 상태 값 고정물

  - cartsWithQuantity: getCarts.success에서 기본 값 quantity를 추가한 carts 데이터 고정물
  -feat: createCartItem(장바구니 아이템의 id값과 quantity의 값을 받아 장바구니 아이템을 만들어주는 유틸 함수) 추가. (기본값은 id:1, quantity: 1)

  -feat: createCartsReducerStateHasValue(carts.value의 value값을 입력으로 받아 입력 받은 값을 갖는 cartsReducerState를 반환해주는 유틸 함수) 추가.
  - feat: rootReducer에 cartsReducer 추가
  - feat: rootReducer에 ordersReducer 추가

  - feat: rootSaga에 cartsSaga 추가
  - feat: rootSaga에 ordersSaga 추가
  - feat: rootReducer에 cartsReducer 추가
  - feat: rootReducer에 ordersReducer 추가

  - feat: rootSaga에 cartsSaga 추가
  - feat: rootSaga에 ordersSaga 추가
  - feat: carts 상태 셀렉터 추가(carts,isLoadingCarts,hasErrorCarts,errorCarts)

  - feat: getCarts(dispatch - getCarts.request)

  - feat: addCarts(dispatch - addCarts.request)

  - feat: increaseCartItemQuantityById(장바구니 아이템 최대 값이 아닌 경우 dispatch increaseCartItemQuantityById)

  - feat: decreaseCartItemQuantityById(장바구니 아이템 최소값이 아닌 경우 dispatch decreaseCartItemQuantityById)

  - feat: deleteCartItemById(아이디 값을 이용해 장바구니 아이템 삭제하는 기능, dispatch deleteCartItemById)
  - feat: addSelectedItemToCarts(선택된 아이템을 장바구니로 추가)

  - feat: removeSelectedItemFromCarts(선택된 장바구니 아이템을 삭제)

  - feat: mutateSelectedCartItems(선택된 장바구니 아이템을 수정, 수량 조절)

  - feat: deleteSelectedCartItem(선택된 장바구니 아이템을 삭제)

  - feat: clearSelectedCartItems(장바구니 아이템을 모두 삭제)

  - feat: selectAllCartItems(모든 장바구니 아이템을 선택)

  - feat: orderSelectedCartItem(선택된 장바구니 아이템을 주문)
  - feat: 초기에 getCarts 액션을 디스패치해 redux 스토어에carts 값을 저장
  - feat: 선택 여부에 따라 checkbox 달리 보이게 하는 기능 추가

  - feat: 선택되었을 때 선택된 장바구니 정보에 추가해주는 기능 추가

  - feat: + 버튼을 누르면 장바구니 아이템 수량을 증가시켜주는 기능 추가

  - feat: - 버튼을 누르면 장바구니 아이템 수량을 감소시켜주는 기능 추가
  - it('로딩중인 상태에는 로딩중 상태를 보인다)
  - it('로딩중이 아닌 경우 장바구니 정보가 나타난다.')
  - it('장바구니에 담긴 아이템 정보를 보여준다.)

  - it('선택 checkbox input을 누르면 props로 전달 받은 핸들러가 호출된다.')

  - it('장바구니 수량 버튼(+)을 누르면 액션이 dispatch 된다.)

  - it('장바구니 수량 버튼(-)을 누르면 액션이 dispatch 된다.')
- it('cartsReducer는 초깃값을 갖는다')

- describe('deleteCartItemById')
  - it('일치하는 id가 있는 경우 cartItem을 삭제')

- describe('increaseCartItemQuantityById')
  - it('일치하는 id가 있는 경우 quantity를 증가')

- describe('decreaseCartItemQuantityById')
  - it('일치하는 id가 있는 경우 quantity를 감소')
    - it('store의 state는 초깃값을 가지고 있다.')
    - it('deleteCartItemByIdSaga success')
    - it('deleteCartItemByIdSaga failure')
    - it('store의 state는 초깃값을 가지고 있다.')

    - it('getCarts success 시 carts 상태를 저장 할 수 있다.'

    - it('getCarts failure')
- feat: ordersAsyncActions 액션 생성
  - getOrders(주문 목록 받아오기)

- feat: getOrders.success 시 서버에서 온 데이터 orderDetails 값 변경해서 스토어에 저장(OrderItem[])
  - feat: getOrdersSaga(주문 목록 받아오는 작업) 추가
  - ORDERS: 'orders' 상수 값

  - GetOrdersSuccessPayload: getOrders 성공 시 payalod 타입 선언

  - OrdersReducerState: ordersReducer 상태 타입 선언

  - OrderDetailFromServer: 서버에서 받아오는 원본orderDetail 타입 선언

  - OrderItemFromServer: 서버에서 받아오는 원본 타입 orderDetail 타입 선언
- feat: orders 상태 셀렉터 추가(orders,isLoadingOrders, hasErrorOrders, errorOrders)

- feat: getOrders(dispatch getOrders.request) orders 데이터 받아오는 액션 디스패치
  - feat: getOrders: 주문 목록 request
  - feat: props로 받아온 isLoading 값이 true면 로딩중임을 알려주는 기능 추가

  - feat: props로 받아온 orders(주문 목록)을 OrderListItem 컴포넌트를 이용해 주문 정보를 보여주는 기능 추가
  - refactor: /apis/carts.ts -> /service/apis/carts.ts

  - refactor: /apis/orders.ts -> /service/apis/orders.ts

  - refactor: /apis/products.ts -> /service/apis/products.ts

 refactor: service 관련 유틸 service 디렉토리 하위에 위치하도록 수정

   - refactor: service/cartsUtils 에 carts 관련 service 함수 이동

   - refactor: service/ordersUtils 에 orders 관련 service 함수 이동

   - refactor: service/ordersUtils 에 orders 관련 service 함수 이동
  - feat: catch 부분에 error가 instanceof Error 인지 확인하고productsActions.getProductsAsyncAction.failure에 error 전달
  - orders: 주문 내역(배열)
  - orderItem: 주문 내역(orders 배열)의 아이템
  - product: 상품 정보
  - useAppDispatch: 프로젝트에서 useDispatch의 타입을 넣어준 것 mocking

  - useAppDispatch: 프로젝트에서 useSelector의 타입을 넣어준 것 mocking
  - it('장바구니 수량이 20개인 경우 버튼(+)을 누르면 액션이 dispatch 되지 않는다.)

  - it('장바구니 수량이 1개인 경우 버튼(-)을 누르면 액션이 dispatch 되지 않는다.')
  - feat: renderOrderList 테스트 코드 간소화를 위한 렌더 함수 추가

  - describe('<OrderList />'
      - it('로딩중인 경우 로딩임을 보여준다.')

      - it('로딩중이 아닌 경우 총 주문 상품 개수와 액수를 보여준다.')

      - it('주문내역(orders)에 아무것도 들어있지 않다면 주문 내역이 존재하지 않다는 메시지를 보여준다.')
  - feat: renderOrderListItem 테스트 코드 간소화를 위한 렌더 함수 추가

  describe('<OrderListItem />')
      - it('주문 번호와 제품 수량 정보를 확인 할 수 있다.'
  describe('<OrderListContainer />')
      - it('처음 렌더링 되었을 때 로딩중임을 보인다.')
  - 발견하지 못한 이슈로 아직 해결하지 못하고 있음
  - feat: getTotalOrderItemNumber 서비스 함수를 통해 총 주문 상품 개수를 알려주는 기능 추가

  - feat: getTotalOrderAmount 서비스 함수를 통해 총 주문액을 알려주는 기능 추가
  - feat: 주문 번호 확인 할 수 있는 기능 추가
  - feat: 주문된 상품들을 보여주는 기능 추가
  - feat: 주문된 상품들의 개수를 보여주는 기능 추가
  - feat: ordersActions.postOrders.request을 dispatch 하는 기능 추가(newOrderDetails(변경된 주문 내역들)을 보내줘 postOrdersSaga에서 payload로 받을 수 있음)
@guymoon
Copy link
Member Author

guymoon commented Mar 15, 2022

PR이 너무 커져버렸네요... 죄송합니다..ㅜㅜ test하고 스토리북이 같이 들어가니까 더 커지네요... 요구사항 별로 step을 나누니 조금 커지는 것 같습니다ㅜㅜ

@woobottle
Copy link

  1. 수량 감소 버튼을 눌렀을 때 물품의 수량이 1개라면 감소시키지 않는다. (현재 수량 1개 -> 감소 버튼 클릭 -> 현재 수량 1개)
    여기서 수량이 redux store에서 관리되는 상태라면 이 수량에 대한 로직은 어디에 담기는게 이상적일까요?

redux에서 현재 수량이 1개인데 수량 감소에 대한 액션이 디스패치되면 감소시키지 않는다.
view를 담당하는 리액트 컴포넌트에서 현재 수량이 1개인 경우 -> 감소에 대한 액션이 디스패치되지 않도록 막는다.
상태의 min,max 값에 대한 부분이므로 리듀서 부분에 관련 로직을 담는 것도 맞는 것 같지만.. 현재 상태에 따라 액션이 디스패치되지 않도록 막는게 맞는 것 같기도하고 혼란스럽네요. 어떻게 생각하시나요?!

제 생각엔 redux에 해당 로직이 담기는게 좋지 않을까 싶습니다.
view는 view의 역할에만 충실하고 state에 해당하는 로직들은 관련 store에서 조지는게 좋지 않을까 싶어서요
각자의 역할에만 충실하도록 하는것이 추후의 유지보수에 있어서 용이하지 않을까 싶어요


export const transformOrderDetailFromCartItem = (cartItems: CartItem[]): OrderDetail[] => {
return cartItems.map(({ product, quantity }) => ({ product, quantity }));
};

Choose a reason for hiding this comment

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

cartsService파일, ordersService파일은 util의 성격을 띄는것 같은데 맞을까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

음.. 완전한 유틸로 추상화하지 못한 것들이라고 해야할까요? 약간 서비스 로직을 담고있는 유틸 정도의 개념입니다!

const dispatch = useAppDispatch();

const selfSelector = (state: RootState) => state[ORDERS];
const ordersModelSelector = createSelector(

Choose a reason for hiding this comment

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

createSelector를 사용하신 이유가 궁금해요~~

Copy link
Member Author

@guymoon guymoon Mar 25, 2022

Choose a reason for hiding this comment

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

reselect를 쓰기 위함이었어요!

};
};

export default useCarts;

Choose a reason for hiding this comment

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

이렇게 custom hook 으로 state, actions 들 빼는것 좋은것 같습니다!!
배우고 싶네요!!

Copy link
Member Author

Choose a reason for hiding this comment

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

container, presentational 컴포넌트 구분하는 목표를 redux 관련 로직을 커스텀 훅으로 빼면 더 쉽게 이룰 수 있는 것 같아요! 서비스 관련 로직들을 훅으로 1차적으로 밀어버려서 보여주는 목적에 충실한 컴포넌트를 만들 수 있었던 것 같고, 비지니스 로직이 훅 쪽에 모여있으니 작업하기도 수월했던 것 같아요!

이게 맞는 방법인지는 모르겠지만 실제로 작업하면서 매우 편했던 것 같습니다!! 그래도 서비스 로직들이 이곳저곳에 위치한다는 것이 아직 해결해야하는 숙제 같아요 ㅜ

}

const CartList = ({ carts, isLoading }: Props) => {
const [selectedCartItems, setSelectedCartItems] = useState<CartItem[]>([]);

Choose a reason for hiding this comment

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

혹시 selectedCartItem 관련 로직들을 custom hook으로 뺄수 있을까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

훔 이 부분은 감이 잘 잡히지 않네요! 근데 이 상태또한 redux에서 관리해주는게 어떨까 생각이 들긴 하네요..!


return (
<Container>
<label htmlFor="SelectCartItemButton">선택</label>

Choose a reason for hiding this comment

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

label 태그 적절히 이용하신 것같아 좋은것 같습니다!


return (
<div>
<OrderList orders={orders} isLoading={isLoadingOrders} />

Choose a reason for hiding this comment

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

주요한 사항은 아니지만 혹시 isLoading을 props로 넘겨주지 않는건 어떠할까요??

orderList의 역할인 보여주는 것에만 충실할 수 있게 OrderListContainer에서 isLoading에 따라 보여줄지 말지를 결정하는 건 어떻게 생각하시는지 궁금합니다

Copy link
Member Author

Choose a reason for hiding this comment

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

저도 이 부분은 많이 고민했던 부분이에요! 실제로 우병님이 말씀해주신 것 처럼 진행했었습니다!

그런데 컨테이너 컴포넌트를 테스트함에 있어 문제가 있더라고요. 제가 컨테이터 컴포넌트에서 로딩 상태에 따라 데이터들이 보이는지 마는지를 테스트하고 있었는데 왜 컨테이너 컴포넌트에서 이걸(뷰) 테스트하고 있지 싶은 생각이 들더라구요.. (지금 생각해보면 셀렉터를 모킹해서 해결 할 수 있긴 했던 것 같습니다)

로딩 값에 따라 어떤 화면을 보여주는지는 컨테이너 컴포넌트의 역할이 아니라고 생각이 들었고, 그래서 presentational 컴포넌트 역할을 할 자식에게 props로 값을 넘겨줘, 그냥 그 받아온 값을 바보처럼 보여주기만 하자 결정했습니다.

"컨테이너는 그냥 값을 넘겨주는 역할만" 이라는 컨셉으로 진행했던 것 같습니다!

@woobottle
Copy link

혹시 반응형은 어떻게 되고 있는지 구현 영상 공유 가능하실까요???

display grid를 잘 사용하시는건 정말 배우고 싶네요!
저는 반응형을 구현할때 꽤나 애를 많이 먹었어서 궁금합니다!!


const createCartItemId = (carts: CartItem[]) => {
return carts.map((cartItem) => cartItem.id).reduce((id1, id2) => Math.max(id1, id2), 0) + 1;
};

Choose a reason for hiding this comment

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

slice에 대한 리뷰는 저도 현재 해당 부분 구현중이여서 완료되는대로 리뷰 드릴수 있도록 하겠습니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

이 로직은 다른 곳으로 이사보낼 예정입니닼ㅋㅋㅋ

@woobottle
Copy link

오늘 리뷰 드리지 못한 내용은 제가 현재 구현하고 있는 부분 완료후에 추가로 리뷰 드릴게요!!!

@guymoon
Copy link
Member Author

guymoon commented Mar 25, 2022

우병님 리뷰 너무 감사드립니다 :)
이제 몸좀 괜찮으신가요?
저도 갑자기 막 열이 나고 고생좀 했네요ㅜㅜ 코로나는 아니었던 것 같습니다...

감사드려요! 질문주신거에 몇개 답 해보았고, 다른 답은 코멘트에 달았습니다!!
감사합니다 :)

1

수량 감소 버튼을 눌렀을 때 물품의 수량이 1개라면 감소시키지 않는다. (현재 수량 1개 -> 감소 버튼 클릭 -> 현재 수량 1개)
여기서 수량이 redux store에서 관리되는 상태라면 이 수량에 대한 로직은 어디에 담기는게 이상적일까요?
redux에서 현재 수량이 1개인데 수량 감소에 대한 액션이 디스패치되면 감소시키지 않는다.
view를 담당하는 리액트 컴포넌트에서 현재 수량이 1개인 경우 -> 감소에 대한 액션이 디스패치되지 않도록 막는다.
상태의 min,max 값에 대한 부분이므로 리듀서 부분에 관련 로직을 담는 것도 맞는 것 같지만.. 현재 상태에 따라 액션이 디스패치되지 않도록 막는게 맞는 것 같기도하고 혼란스럽네요. 어떻게 생각하시나요?!

제 생각엔 redux에 해당 로직이 담기는게 좋지 않을까 싶습니다.
view는 view의 역할에만 충실하고 state에 해당하는 로직들은 관련 store에서 조지는게 좋지 않을까 싶어서요
각자의 역할에만 충실하도록 하는것이 추후의 유지보수에 있어서 용이하지 않을까 싶어요

여기에 대한 제 답은 우선 뷰의 역할은 아니라고 생각했습니다. 그런데 이걸 리듀서까지 가져가냐도 아닌 것 같더라구요.

그래서 dispatch 로직을 담고있는 커스텀 훅에서 이 작업을 해줬습니다! 최대한 서비스 관련 로직을 훅에 담기로 결정해서 이런 결정을 했는데 저도 뭐가 더 나은지 는 모르곘네요 ㅎㅎ

근데 만약 갑자기 요구 사항 변경으로 최대 수량이 20개에서 10개로 바뀐다 생각하면 이걸 가장 멀리 있는 리듀서까지 가서 고치는게 맞나? 생각이 들긴 하더라구요! 그래서 그나마 사용하는 쪽에서 가장 가까운 훅에 위 로직을 담았습니다! 그리고 alert 하는 등의 동작도 이게 더 쉬울 것 같더라구요!

2

혹시 반응형은 어떻게 되고 있는지 구현 영상 공유 가능하실까요???

display grid를 잘 사용하시는건 정말 배우고 싶네요!
저는 반응형을 구현할때 꽤나 애를 많이 먹었어서 궁금합니다!!

반응형은 구현하지 않았습니다!

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