오늘의집 긴장해.. 지금 내일의집 만든다. React 개발 리뷰 (3)

xedni·2020년 11월 29일
5

Project | HOT

목록 보기
3/4

이번화에서는 product 상세 페이지에 대한 코드리뷰를 해볼 것이다.

5. Code Review

2) Product Detail 주요 코드

: Product Detail은 option을 선택하는 컴포넌트와 장바구니 컴포넌트가 자식 컴포넌트로 존재한다.

  • 옵션은 상단부에서도 선택할 수 있지만....

  • 하단부의 Sticky된 우측바에서도 옵션을 선택할 수 있게 되어있다. 또한 같이 움직인다.
  • 동일한 컴포넌트를 사용하기로 했다. 재활용하기 위함도 있겠지만 더 큰 문제는 위쪽 옵션을 선택하면 아래쪽 옵션도 똑같이 움직여야하기 때문이다.

  • 이건 리액트 슬릭으로 만들어진 유저스타일뷰이다.
    이제 코드를 한번 보자.

OverView Component

: Product Detail의 부모 역할을 하는 컴포넌트이다. 지난번과 마찬가지로 함수부분을 제외하고는 생략하겠다ㅠㅠ

class Overview extends Component {
// 해당 제품에 대한 정보를 받아오는 함수만 컴디마에 넣었다.
  componentDidMount() {
    this.getProductList();
  }
// 이게 위에서 말한 함수!! 
  getProductList = () => {
    fetch(`${API}/store/${this.props.match.params.id}`, {
      method: 'GET',
      headers: {
        authorization: localStorage.getItem('token'),
      },
    })
      .then((res) => res.json())
      .then((result) => {
        this.setState({
          productList: result.result[0],
          lowestPrice: result.result[0].details[0].price,
          coverImageSrc: result.result[0].product_image_url[0],
        });
      });
  };
// 이건 북마크를 클릭했을때 마이페이지 서버에 포스트 하는 함수
  postProductId = () => {
    fetch(`${API}/user/bookmark`, {
      method: 'POST',
      body: JSON.stringify({
        product_id: this.state.productList.product_id,
      }),
      headers: {
        Authorization: localStorage.getItem('token'),
      },
    })
      .then((res) => res.json())
      .then((result) => {
        console.log(result);
      });
  };
// 이건 장바구니에 넣었을때 장바구니 서버에 포스트 하는 함수
  postCartInfo = (e) => {
    if (this.state.selectedProducts.length) {
      e.preventDefault();
      fetch(`${API}/order/cart`, {
        method: 'POST',
        body: JSON.stringify(this.state.selectedProducts),
        headers: {
          Authorization: localStorage.getItem('token'),
        },
      })
        .then((res) => res.json())
        .then((result) => {});
      this.props.takeModalEvent();
    }
  };
// color옵션을 선택시 작용하는 이벤트 함수
  getSelectedProductColor = (e) => {
    const { options, selectedIndex } = e.target;
    const selectedColor = options[selectedIndex].innerHTML;
    this.setState({ selectedColor });
  };
// 사이즈 옵션을 선택시 작용하는 이벤트 함수
  getSelectedProductOption = (e) => {
    const { options, selectedIndex, value } = e.target;
    const selectedProducts = [...this.state.selectedProducts];
    if (
      selectedProducts.some(
        (selectedProduct) =>
          selectedProduct.label === options[selectedIndex].innerHTML
      ) ||
      selectedProducts.some((selectedProduct) => selectedProduct.color) ===
        this.state.selectedColor
    ) {
      alert('이미 선택한 옵션입니다.');
    } else {
      selectedProducts.push({
        product_id: this.state.productList.product_id,
        value: value,
        count: 1,
        color: this.state.selectedColor,
        label: options[selectedIndex].innerHTML,
        sale: this.state.sale,
        seller: this.state.productList.product_seller,
      });
      this.setState({
        selectedProducts,
      });
    }
  };
// 사이즈 옵션 선택 후 수량을 선택시 작용하는 이벤트 함수
  getProductCount = (targetProduct, countString) => {
    const countNumber = parseInt(countString);
    const selectedProducts = [...this.state.selectedProducts];
    const index = selectedProducts.indexOf(targetProduct);
    selectedProducts[index].count = countNumber;
    this.setState({ selectedProducts });
  };
// 옵션을 삭제하는 함수
  handleDeleteProduct = (targetIndex) => {
    const selectedProductsIndex = this.state.selectedProducts.filter(
      (_, index) => index !== targetIndex
    );
    this.setState({ selectedProducts: selectedProductsIndex });
  };
// 이미지 리스트에서 hover시 이미지를 변경하는 함수
  changeCoverImage = (e) => {
    this.setState({ coverImageSrc: e.target.src });
  };
// 북마크에 따라 버튼이 바뀌는 이벤트 함수
  handleBookmarkEvent = (e) => {
    e.preventDefault();
    this.setState({ bookMarkSwitch: !this.state.bookMarkSwitch });
    this.postProductId();
  };
}
  • 이것도 마찬가지로 대부분의 함수는 이 부모 컴포넌트에서 이루어진다.
  • 자식에게는 이 프롭스 통로만 뚫어주었을 뿐이다.
const salePrice = Math.floor(lowestPrice - (lowestPrice * sale) / 100);
  • 할인가격이 존재하는데 미리 할인된 가격을 계산한 변수를 만들어두었다.
// 할인전 금액 부분
<div className='prevPrice'>
  {Math.floor(lowestPrice && lowestPrice)
    .toString()
    .replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
</div>
// 할인후 금액 부분
<div className='currentPriceBox'>
  <div className='currentPrice'>
    {salePrice
      .toString()
      .replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
  </div>
// 할부 금액 부분
  <div className='interestFreePoint'>
    {'월'}{' '}
    {Math.floor((lowestPrice - lowestPrice / sale) / 7)
      .toString()
      .replace(/\B(?=(\d{3})+(?!\d))/g, ',')}{' '}
    {'(7개월)'} {'무이자할부 >'}
  </div>
  • 그리고 렌더에서 해당 변수를 잘 활용하였다.
<SelectOption
  giveProductInfo={productList} //제품정보
  giveSelectedProducts={selectedProducts} // 선택한 옵션 정보
  giveBookmarkColor={bookMarkSwitch} // 북마크 스위치 정보
  takeSelectedColor={getSelectedProductColor} // 선택한 컬러 이벤트
  takeSelectedOption={getSelectedProductOption} // 선택한 옵션 이벤트
  takeSelectedProductsValue={getProductCount} // 옵션 갯수 변경 이벤트
  takeSelectedProductsDelIndex={handleDeleteProduct} // 옵션 삭제 이벤트
  takeSelectedProductsCart={postCartInfo} /> // 장바구니 이벤트
  • 옵션을 선택하는 자식 컴포넌트로는 많은 프롭스들이 전달된다. 왜냐하면 모든 이벤트가 이 부모 컴포넌트로 모이기 때문이다.
  • take로 시작하는 프롭스들은 전부 자식에서 보내온 이벤트들이다.
  • SelectOption 컴포넌트는 해당 렌더에서 2번 쓰인다. 위의 사진처럼 동일하게 움직이는 컴포넌트가 있기 때문이다. 유심히 보면 유일한 차이점을 찾을 수 있다. 북마크이다. 그래서 아래쪽에만 다른 프롭스네임이 한개 더 존재한다.
  takeBookmarkEvent={handleBookmarkEvent}
  • 요녀석이다. 북마크는 아래쪽에만 존재한다.

SelectOption Component

class SelectOption extends Component {
  // 그저 이벤트 전달의 중간 통로(옵션갯수 고르는)
  getProductCount = (targetProduct, countString) => {
    this.props.takeSelectedProductsValue(targetProduct, countString);
  };
  // 그저 이벤트 전달의 중간 통로(옵션갯수 삭제하는)
  handleDelete = (productIndex) => {
    this.props.takeSelectedProductsDelIndex(productIndex);
  };
}
  • 함수로 2개의 전달 통로가 존재한다.
  • 함수 getProductCount 에서는 이벤트만 보내는 것이 아니라 타겟 제품이 무엇인지, 갯수가 몇개인지까지 함께 전달하고 있다. 이것은 아래 SelectedProduct 컴포넌트에서 전달하는 것이 무엇인지 보면 알 수 있다.
// 색상을 고르는 select바
<select className='selectColorBox' onChange={takeSelectedColor}>
  <option hidden selected>옵션: 색상</option>
  {giveProductInfo.color && giveProductInfo.color.map((firstColorElement, firstOptionindex) => (
    <option key={firstOptionindex} label={firstColorElement}{firstColorElement}
    </option>))}
</select>
// 옵션을 고르는 select바
<select className='selectOptionBox' onChange={takeSelectedOption}>
  <option hidden selected>옵션: 사이즈</option>
  {giveProductInfo.details && giveProductInfo.details.map((firstOptionElement, firstOptionindex) => (
    <option 
      key={firstOptionindex}
      label={firstOptionElement.size}
      value={firstOptionElement.price}>{firstOptionElement.size}
    </option>))}
</select>
  • 렌더부분에는 옵션을 고를 수 있는 셀렉트바를 만들어두었다.
  • 색상은 컬러만 알면 되므로 label만 전달하고 있고 옵션의 경우 사이즈, 가격이 모두 전달되어야 하므로 label, value가 모두 전달되고 있다.

  • 위의 사진처럼 옵션이 선택된 이후에는 회색 박스가 한개 생겨난다. 이것을 SelectOption의 자식 컴포넌트로 만들었다. SelectedProduct 라는 컴포넌트로 아래서 설명하겠다.

SelectedProduct Component

class SelectedProduct extends Component {
  // 선택된 옵션이 여기서 최상위 부모로 전달되고 있었다.
  getSelectedProduct = (e, targetProductInfo) => {
    this.props.takeCountValue(targetProductInfo, e.target.value);
  };
  // 삭제 이벤트도 여기서 전달되고 있었다.
  handleDeleteButton = (productIndex) => {
    this.props.takeDeleteFuction(productIndex);
  };
  • 선택된 옵션은 여기서 관리되고 있다.
  • 삭제 이벤트도 이곳에서 관리되고 있다.
  • 최상위 부모로 올려보내서 다시 장바구니로 내려주는 방식으로 구성되어 있다.
const counts = [1, 2, 3, 4, 5, 6, 7, 8, 9];

<select
  value={targetProductInfo.count}
  className='selectProductCount'
  onChange={(e) => getSelectedProduct(e, targetProductInfo)}>
  {counts.map((countNumber, countIndex) => (
    <option key={countIndex} id={countIndex}>
      {countNumber}
    </option>))}
</select>
  • 이곳에서도 셀렉트 박스가 하나 생성되고 있는데, 옵션의 갯수를 조절하는 셀렉트 박스이다.

Cart와 Modal 등은 다음화에 계속!!

profile
"꾸준한 삽질과 우연한 성공"

3개의 댓글

comment-user-thumbnail
2020년 11월 29일

와....

답글 달기
comment-user-thumbnail
2020년 12월 5일

ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ상혁님 근데 왜 이거 gif아니죠? gif로 해주세요!

1개의 답글