프로젝트 이모저모/HoduMarket 프로젝트

오픈마켓 버그) 상품디테일 불러올 때 이전 상태 값 남아있음

Ella Seon 2023. 4. 26. 09:28

0. 버그 현황

- 사실 데이터가 잘 불러와져서 버그(?) 라고 할 수 있을까 싶지만, 사용자 경험에서 이전 데이터가 남아있는 채 다음데이터가 덮여씌워지는거는 불편한 일이다. 

- 상품 디테일을 리덕스 툴킷을 사용해서 서버와 통신을 하고, 통신 결과를 리덕스 툴킷에 전역상태로 저장한다. 그리고 저장한 것을 useParams 를 이용해서, 상품 디테일을 불러오게 만들었다. 하지만, 새로운 상품을 클릭하면 이전에 클릭했던 상품 정보가 남아있고, 다음 상품이 덮어씌워져 렌더링된다. 

- 이전 상품 정보가 남아있지 않고, 바로 새로운 데이터를 렌더링 할 수 있게 만들어보자 

1. 원인 및 해결

- 이전 상품 정보가 남아있는 걸 해결하기 위해서는 상품 디테일 데이터를 가져오기 전에 상태를 초기화 해주는 과정이 필요하다. 상품 디테일을 가져오기 전에 초기화하는 로직을 만들자.

 

🔸기존 코드

// 기존 코드
export const fetchGetProductDetail = createAsyncThunk(
  "products/fetchGetProductDetail",
  async (productId: number) => {
    try {
      const detailResults = await axios.get(
        `${BASE_URL}/products/${productId}`
      );
      console.log(detailResults.data);
      return detailResults.data;
    } catch (error: any) {
      console.log(error);
    }
  }
);

const productSlice = createSlice({
  name: "products",
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchGetProducts.pending, (state) => {
        state.status = "loading";
      })
      .addCase(fetchGetProducts.fulfilled, (state, action) => {
        state.status = "succeeded";
        state.error = "";
        state.products = action.payload.results;
        state.totalPage = Math.floor(action.payload.count / 15 + 1);
      })
      .addCase(fetchGetProducts.rejected, (state, action) => {
        state.status = "failed";
        state.error = action.error.message || "Something is wrong";
        state.products = [];
      })
      // 상품 검색
      .addCase(fetchSearch.fulfilled, (state, action) => {
        state.products = action.payload.results;
      })
      // 상품 디테일
      .addCase(fetchGetProductDetail.fulfilled, (state, action) => {
        state.productDetail = action.payload;
      });
  },
});

export default productSlice.reducer;

초기화 하는 로직은 간단하다.

reducers 에 초기화 해주는 로직을 추가해주고, 디테일 페이지로 import 해와서 서버와 통신하기 전 코드에 삽입하면 되는 것이다. 

서버와 통신하기 전에 초기화 해준다.

이렇게 변경하면 새로운 상품을 클릭할 때 이전 상품 정보가 남아있지 않고 바로 새로운 데이터를 렌더링 할 수 있다.

 


➕) 도중에 발생한 타입스크립트 에러

- 아래처럼 빈 객체로 초기화 하니, interface ProductDetail 의 속성들을 다 필수값으로 지정해놨는데 오류가 떴다. 

그래서 ProductDetail 속성들을 모두 옵셔널로 변경해주었다.

하지만, 옵셔널 프로퍼티를 쓰면 undefined 값을 처리해야할 수도 있다. 

사용하는 컴포넌트에서 undefined일 경우 표시할 기본값을 설정해주어야 한다.

 

나는 ProductDetailPage 컴포넌트에서 가격과 배송료를 불러오기에 

undefined일 경우에 0원이라는 기본값을 설정해주었다.

🔸논리연산자 OR
- 주어진 두 값 중 하나가 참인지를 확인한다.
(productDetail.price || 0)에서 productDetail.price가 존재하고, 그 값이 undefined, null, false, NaN, 0, 또는 빈 문자열("")과 같은 falsy 값이 아니라면 productDetail.price를 사용합니다. 만약 productDetail.price가 falsy 값이면 0을 사용하게 됩니다.


const value1 = undefined;
const result1 = value1 || 0; // result1은 0입니다.

const value2 = 100;
const result2 = value2 || 0; // result2는 100입니다.
function ProductDetailPage() {
  const { productId } = useParams();
  const dispatch = useAppDispatch();
  const productDetail = useAppSelector((state) => state.products.productDetail);

  const [count, setCount] = useState(1);

  useEffect(() => {
    if (productId !== undefined) {
      const productIdNumber = parseInt(productId);
      dispatch(clearProductDetail());
      dispatch(fetchGetProductDetail(productIdNumber));
    }
  }, [dispatch, productId]);

  return (
    <>
      <S.ProductWrapper>
        <S.ProductImgBox>
          <img src={productDetail.image} alt="상품 사진" />
        </S.ProductImgBox>
        <S.ProductCartWrapper>
          <S.SellerText>{productDetail.store_name}</S.SellerText>
          <S.ProductText>{productDetail.product_name}</S.ProductText>
          <S.PriceText>
            {productDetail.price?.toLocaleString()}
            <span>원</span>
          </S.PriceText>
          <S.DeliveryText>
            {productDetail.shipping_method === "PARCEL"
              ? "직접배송"
              : "택배배송"}{" "}
            /{" "}
            {productDetail.shipping_fee === 0
              ? "무료배송"
              : `배송비 ${productDetail.shipping_fee?.toLocaleString()} 원`}
          </S.DeliveryText>
          <S.hr />
          <AmountBtn
            count={count}
            setCount={setCount}
            stock={productDetail.stock || 0}
          />
          <S.hr />
          <S.TotalPriceWrapper>
            <S.TotalText>총 상품 금액</S.TotalText>
            <div>
              <S.TotalAmountText>
                총 수량
                <span> {count}</span>개
              </S.TotalAmountText>
              <S.TotalPriceText>
                {(
                  (productDetail.price || 0) * count +
                  (productDetail.shipping_fee || 0)
                ).toLocaleString()}{" "}
                <span>원</span>
              </S.TotalPriceText>
            </div>
          </S.TotalPriceWrapper>
          <S.BtnWrapper>
            <S.PurchaseBtn type="button" size="md">
              바로 구매
            </S.PurchaseBtn>
            <S.CartBtn type="button" size="ms">
              장바구니
            </S.CartBtn>
          </S.BtnWrapper>
        </S.ProductCartWrapper>
      </S.ProductWrapper>
      <ProductDetail />
    </>
  );
}

export default ProductDetailPage;

위 코드에서 추가로 궁금증이 생겼다. 

number 타입은 undefined 일 경우 다 기본값을 0원으로 처리해주었는데, 왜 문자열타입은 기본값을 처리 안해줘도 될까??

 

- React에서 문자열 속성을 렌더링할 때, 만약 속성이 undefined 또는 null인 경우, 해당 속성은 자동으로 빈 문자열로 처리됩니다. 따라서, 기본값을 명시적으로 설정하지 않아도 렌더링에 문제가 없다고 한다.

 

 

 

타입스크립트를 사용하고 있지만, 내가 미리 예견해서 타입스크립트 버그를 막기보다는 타입스크립트 버그가 발생하니 수정하는 형식으로 타입스크립트를 이용하고 있어서 아쉽다... 언제쯤 익숙해질까 타입스크립트 ㅜㅜ