👩‍💻
Megaptera Frontend
  • 주차별 학습
    • megaptera-front
    • 1. 개발 환경
      • 1. 개발 환경
      • 2. TypeScript
      • 3. React
      • 4. Testing Library
      • 5. Parcel & ESLint
      • 한 주를 마치며
    • 2. JSX
      • 1. JSX
      • 한 주를 마치며
    • 3. React로 사고하기
      • 1. React Component
      • 2. React State
      • 한 주를 마치며
    • 4. React Hooks
      • 1. Express
      • 2. Fetch API & CORS
      • 3. React의 Hook
      • 4. useRef & Custom Hook
      • 5. usehooks-ts
      • 한 주를 마치며
    • 5. 테스트
      • 1. TDD
      • 2. React Testing Library
      • 3. MSW
      • 4. Playwright
      • 한 주를 마치며
    • 6. External Store
      • 1. External Store
      • 2. TSyringe
      • 3. Redux 따라하기
      • 4. usestore-ts
      • 한 주를 마치며
    • 7. React Router
      • 1. Routing
      • 2. Routes
      • 3. Router
      • 4. Navigation
      • 한 주를 마치며
    • 8. CSS in JS
      • 1. Design System
      • 2. Style Basics
      • 3. CSS in JS
      • 4. styled-components
      • 5. props와 attrs
      • 6. Global Style & Theme
      • 한 주를 마치며
    • 9. 쇼핑몰 목록, 상품 페이지
      • 1. 개발하기 전 준비
      • 2. 목록 보기
      • 3. 상품 상세 보기
      • 4. 장바구니 보기
      • 5. 장바구니에 상품 담기
      • 한 주를 마치며
    • 10. 사용자 인증, 인가
      • 1. 로그인
      • 2. 로그아웃
      • 3. 회원가입
      • 4. 주문 목록 & 주문 상세
      • 한 주를 마치며
    • 11. 주문, 결제
      • 1. 배송 정보 입력
      • 2. 포트원 결제 요청
      • 3. 배송 및 결제 정보 전달
      • 한 주를 마치며
    • 12. 어드민
      • 1. 관리자 웹 사이트 개발 시작
      • 2. 로그인, 사용자 목록
      • 3. 카테고리 관리
      • 4. 주문 관리
      • 5. 상품 관리
      • 한 주를 마치며
Powered by GitBook
On this page
  • 장바구니에 상품 담기
  • AddToCartForm.tsx 컴포넌트 작성
  • Quantity 컴포넌트 작성
  • 공통으로 사용할 Button 컴포넌트 생성
  • useProductFormStore hook 생성
  • ProductFormStore 생성
  • 테스트 작성하기
  • Price 컴포넌트 작성
  • Getter 이용하기
  • SubmitButton 컴포넌트 작성
  • Store에 addToCart 및 상태 추가
  • API Service에 addProductToCart 추가
  • Options 컴포넌트 작성
  • Option 컴포넌트 작성
  • 범용 ComboBox 컴포넌트 작성
  • Store에 changeOptionItem 추가
  1. 주차별 학습
  2. 9. 쇼핑몰 목록, 상품 페이지

5. 장바구니에 상품 담기

Previous4. 장바구니 보기Next한 주를 마치며

Last updated 2 years ago

장바구니에 상품 담기

🎯 장바구니에 상품 담기 기능 구현하기

🛒 장바구니에 상품 담기의 의미

정확히는 Product가 Cart로 들어가는 것이 아니라, Product와 관련된 Option 정보, 수량 등 다양한 값이 조합돼 Cart의 Line Item을 구성하는 것

실제로는 조금 복잡한 도메인 로직이 들어갈 수 있는데, 이런 처리는 백엔드에서 담당하기로 하자. 현업에서는 백엔드와 협업할 때, 어떤 방식으로 데이터를 넘겨주면 좋은지 먼저 소통을 하면 좋음 여기서는 상품과 관련된 옵션, 수량 등을 컨트롤하는데 집중할 것

AddToCartForm.tsx 컴포넌트 작성

  • src/components/product-detail/form

구현 내용

  1. 옵션을 보여주고, 선택하기

  2. 수량 정하기 (기본값: 1)

  3. 수량에 맞는 비용 보여주기

    • 단가 보여주기

    • 추가 가능 작업 : 할인가, 정상가, 쿠폰 적용가 등

  4. 장바구니에 담기 버튼이 있고, 클릭 시 장바구니에 담았다는 메시지로 교체

    • UX 관련 파트 : 다양한 방법 존재

    • 타임아웃으로 기존 페이지로 돌아가게 처리

    • 팝업 모달 띄우기

export default function AddToCartForm() {
  return (
    <div>
      <Options />
      <Quantity />
      <Price />
      <SubmitButton />
    </div>
  );
}
  • Prop Drilling을 피하기 위해 전부 개별 컴포넌트에서 Store를 가져다 쓰도록 구현

  • 부모 컴포넌트에서 props를 내려줘도 되지만, 복잡해질 가능성이 커서 내부에서 처리

Quantity 컴포넌트 작성

export default function Quantity() {
  const [{ quantity }, store] = useProductFormStore();

// +, - 함수 생성 
   
  return (
    <Container>
      <Button onClick={handleClickDecrease}>-</Button>
      <input type="text" value={quantity} readOnly />
      <Button onClick={handleClickIncrease}>+</Button>
    </Container>
  );
}
  • 숫자를 직접 입력하는 것은 불가

  • 버튼만 이용해서 수량을 조절하도록 구현

공통으로 사용할 Button 컴포넌트 생성

  • 공통 UI는 ui 폴더를 만들어서 관리하면 좋음

  • src/components/ui

import styled from 'styled-components';

const Button = styled.button.attrs({
  type: 'button',
})`
  border: .1rem solid #888;
  background: transparent;
  color: ${(props) => props.theme.colors.primary};
  cursor: pointer;
`;

export default Button;

useProductFormStore hook 생성

  • src/hooks

export default function useProductFormStore() {
  const store = container.resolve(ProductFormStore);
  return useStore(store);
}

ProductFormStore 생성

  • src/stores

@singleton()
@Store()
export default class ProductFormStore {
  quantity = 1;

  @Action()
  changeQuantity(quantity: number) {
    if (quantity <= 0) {
      return;
    }
    if (quantity > 10) {
      return;
    }
    this.quantity = quantity;
  }
}

테스트 작성하기

사소한 비즈니스 로직이지만, 테스트 코드를 통해 검증할 것

💡 테스트를 하기 어려운 상황일 때 최소한의 방법

설계에서 비즈니스 로직과 UI를 분리하고, 비즈니스 로직을 중점으로 테스트 리액트와 완전히 분리하면 jest와 typescript만 가지고 테스트를 작성할 수 있음 테스트가 깔끔, 단순해지고 오래 감

describe('ProductFormStore', () => {
  let store: ProductFormStore;

  beforeEach(() => {
    store = new ProductFormStore();
  });

  describe('changeQuantity', () => {
    context('with correct value', () => {
      it('changes quantity', () => {
        store.changeQuantity(3);

        expect(store.quantity).toBe(3);
      });
    });

    context('with incorrect value', () => {
      it("doesn't changes quantity", () => {
        store.changeQuantity(-1);
        store.changeQuantity(11);

        expect(store.quantity).toBe(1);
      });
    });
  });
});

Price 컴포넌트 작성

export default function Price() {
  const [{ product }] = useProductDetailStore();
  const [{ quantity }] = useProductFormStore();

  return (
    <Container>
      {numberFormat(product.price * quantity)}원
    </Container>
  );
}

Getter 이용하기

ProductFormStore에 수량에 따른 금액을 계산하는 메서드 또는 Getter가 있다면 다른 형태로 접근할 수도 있음

export default function Price() {
  const [{ product }] = useProductDetailStore();
  const [{ price }, productFormStore] = useProductFormStore();

  // TODO: product 변경에 따른 setProduct 호출은 여기가 아니라 page 등에서 처리할 것!
  useEffect(() => {
    productFormStore.setProduct(product);
  }, [productFormStore, product]);

  return (
    <Container>
      {numberFormat(price)}원
    </Container>
  );
}
  • src/stores/ProductFormStore.ts

복잡한 로직의 경우 이런 방식으로 구현하면 깔끔해짐 할인, 쿠폰 적용가 등을 처리할 수 있음

get price() {
    return this.product.price * this.quantity;
}

SubmitButton 컴포넌트 작성

export default function SubmitButton() {
  const [{ done }, store] = useProductFormStore();

  const handleClick = () => {
    store.addToCart();
  };

  if (done) {
    return (
      <p>장바구니에 담았습니다</p>
    );
  }

  return (
    <Button onClick={handleClick}>
      장바구니에 담기
    </Button>
  );
}

Store에 addToCart 및 상태 추가

장바구니에 상품을 담기 위해 관리해야 할 여러 상태가 필요

@singleton()
@Store()
export default class ProductFormStore {
  productId = '';

  options: ProductOption[] = [];

  selectedOptionItems: ProductOptionItem[] = [];

  quantity = 1;

  done = false;

  async addToCart() {
    // 
  }
   @Action()
   //
}
async addToCart() {
  this.resetDone();

  await apiService.addProductToCart({
    productId: this.productId,
    options: this.options.map((option, index) => ({
      id: option.id,
      itemId: this.selectedOptionItems[index].id,
    })),
    quantity: this.quantity,
  });

  this.complete();
}

@Action()

  • setProduct(product: ProductDetail) {}

  • changeQuantity(quantity: number) {}

  • resetDone() {}

  • complete() {}

API Service에 addProductToCart 추가

async addProductToCart({ productId, options, quantity }: {
  productId: string;
  options: {
    id: string;
    itemId: string;
  }[];
  quantity: number;
}): Promise<void> {
  await this.instance.post('/cart/line-items', {
    productId, options, quantity,
  });
}

Options 컴포넌트 작성

export default function Options() {
  const [{ options, selectedOptionItems }, store] = useProductFormStore();

  const handleChange: ChangeFunction = ({ optionId, optionItemId }) => {
    store.changeOptionItem({ optionId, optionItemId });
  };

  return (
    <div>
      {options.map((option, index) => (
        <Option
          key={option.id}
          option={option}
          selectedItem={selectedOptionItems[index]}
          onChange={handleChange}
        />
      ))}
    </div>
  );
}

Option 컴포넌트 작성

복잡한 로직을 분리해서 처리

범용 ComboBox 컴포넌트 작성

  • src/components/ui

Store에 changeOptionItem 추가

  • src/stores/ProductFormStore.ts

@Action()
changeOptionItem({ optionId, optionItemId }: {
  optionId: string;
  optionItemId: string;
}) {
   this.selectedOptionItems = this.product.options.map((option, index) => {
      const item = this.selectedOptionItems[index];
      return option.id !== optionId
              ? this.selectedOptionItems[index]
              : option.items.find(i => i.id === optionItemId) ?? option.items[0];
   });
}
  • 찾는 값을 못 찾았을 경우 초기값으로 설정

Array를 Immutable하게 변경하기
🔗 실습 링크 : 장바구니에 상품 담기 수량 조절, 가격 표시 구현
🔗 실습 링크 : 장바구니에 상품 담기 버튼 구현, Price 컴포넌트 리팩토링
🔗 실습 링크 : 장바구니에 상품 담기 옵션 선택 구현