굿즈 스토어 프로젝트 06 - 메인 페이지, 상품 목록 조회 기능.

이유승·2023년 7월 20일
0

온라인 굿즈 스토어의 메인 페이지. 일반적인 온라인 쇼핑몰의 구성을 참고하되 내가 할 수 있을 만큼의 간단한 기능만을 구현하였다. (정확하게는 내가 원래 하려던 것들에서 이건 못하겠다, 안되겠다 싶어서 내용이 계속 빠졌지만..)



1. 슬라이드쇼

슬라이드쇼에 대한 내용은 내용이 너무 길어져서 기능 구현 시리즈로 따로 빼두었다.

기능 구현 리액트 - 슬라이스 쇼 기본.
기능 구현 리액트 - 슬라이스 쇼 숙련 1, 슬라이드쇼 자동 이동.
기능 구현 리액트 - 슬라이스 쇼 숙련 2, 드래그 앤 드랍 이동.

기능을 구현하고, 실제 제품 데이터를 넘겨서 동작하게 하는 부분에서 여러가지 문제가 많이 발생했는데.. Redux Store에서 State를 관리하는 구조나 방법이 잘못되어 있는 게 원인이었다. 전역 상태관리 라이브러리를 처음 사용해본 탓에 State를 생성하고 수정하고 가져오는 기능의 순서라던지, Store의 구조가 어떻게 되어야 하는지 당최 감을 못잡고 있는게 문제. 이 문제는 굿즈 스토어의 기능을 하나씩 추가해가면서 점점 심각해졌다. 자세한 것은 이 시리즈 마지막에서 설명할 예정..



2. 제품 데이터를 리스트 형태로 가져오기.

제일 구현이 쉬운 방법은 그냥 기능마다 내용이 비슷하든 중복되는 내용이 있든 뭐든, 기능마다 함수를 분리하는게 만들기도 쉽고 관리하기도 쉽다. 그러나 일단 나도 더 뛰어난 개발자를 목표로 하고 있으니, 되도록이면 하나의 함수에서 여러가지 기능을 수행할 수 있도록 기능 구현의 방향성을 잡았다.

DB에서 데이터를 리스트 형태로 가져오는 것은 이미 이전 프로젝트에서 구현한 기능이다. 다만 그 당시에는 DB 컬렉션에 저장된 모든 데이터를 가져오도록 했지만, 이번에는 조건에 맞는 데이터만을 가져오게 해야하므로 파이어베이스에서 제공하는 query 함수를 사용하였다.

const GetProductList = (listCallType, itemPerPage, searchKeyword) => {
    return (dispatch, getState) => {
    
    (...)

    // 제품 목록을 조회하는 방식에 따라 쿼리를 다르게 적용한다.
    let queryRef = '';
    // 최초 랜더링일 때, itemPerPage만큼 데이터를 조회해온다.

    // 일반 유저화면에서의 제품 로딩의 경우, 비공개된 제품이 출력되서는 안된다.
    if (listCallType === 'commonusergetproduct') {
        queryRef = query(storeCollectionRef, orderBy('number'), where('productDisclosure', '==', true), limit(itemPerPage));
    }
    // 만약 검색어를 입력하지 않았는데 검색 버튼을 클릭할 경우에도 마찬가지로 처리한다.
    else if (listCallType === 'firstRender' || searchKeyword === '') {
        queryRef = query(storeCollectionRef, orderBy('number'), limit(itemPerPage));
    }
    // 검색일 때, where 함수를 사용하여 조건검색으로 데이터를 조회해온다.
    else if (listCallType === 'keywordSearch') {
        queryRef = query(storeCollectionRef, orderBy('number'), where('name', '==', searchKeyword), limit(itemPerPage));
    };
    
    (...)
};

제품 데이터를 리스트 형태로 가져오는 기능은 메인 페이지와 상품 목록 페이지, 관리자 페이지에서 모두 사용할 수 있도록 구현하였다. 이를 위해서 query 함수에 사용될 인자를 조건문을 이용해 분기를 나누어 적절한 상황에서 맞는 인자가 query 함수에 사용될 수 있도록 하였다.

// 그리고 쿼리를 기준으로 Doc을 가져온다.
const allDocumentSnapshots = await getDocs(queryRef);

// 제품 데이터를 배열에 담아 저장해준다.
const result = [];
allDocumentSnapshots.forEach((doc) => {
	result.push(doc.data());
});
returnData.processData2 = result;

그렇게 query 함수로 조건을 설정하고, DB에서 데이터를 반환하면 이를 Redux Store에 저장하고 프론트에서 State를 꺼내와서 화면에 출력하도록 하였다.

컴포넌트가 렌더링 되면, 우선 제품 목록 데이터를 조회하는 Action Creator 함수를 호출한다.

    useEffect(() => {
        dispatch(GetProductList('commonusergetproduct', 9, ''));
    }, []);

    (...)

    useEffect(() => {
        if (getStoreState.processInfo.processData2 !== '') {
     		setListData(getStoreState.processInfo.processData2);
        };
    }, [getStoreState.processInfo]);

useEffect Hook과 디펜던시를 이용하여 제품 데이터가 프론트에 렌더링되는 과정은 다음과 같다. 컴포넌트 렌더링 이후 제품 목록 데이터를 조회하는 Action Creator 함수를 호출되면 Redux Store에 State가 갱신된다. 이를 디펜던시를 통해 프론트에서 값의 변화를 감지, setState에 저장된 데이터를 꺼내와서 저장한 다음에 map 함수를 사용해 화면에 렌더링한다.

  • map 함수 사용시 주의점.
    map 함수는 호출 대상의 데이터 형식이 배열이 아닐 경우 바로 에러를 발생시킨다. 리액트 특성상 비동기 통신의 결과값이 반환되기 전에 화면이 먼저 렌더링 되는데, 이 시점에서 데이터 형식이 배열이 아닌 경우에는 제대로 된 결과값이 돌아오는 것과 관계없이 에러가 발생한다. 이를 방지하기 위해서 setState가 동작하기 전에 조건문으로 Store의 State가 배열이 아닐 때 기능이 동작하지 않도록 해주었다. (Redux Store의 초기값이 빈 문자열이기 때문에 위와 같은 조건을 사용하였다.)(그냥 Store에서 초기값을 배열로 두면 안되나 싶지만 나름의 사정이 있었다. 자세한건 시리즈 마지막에..)

배열 데이터까지 잘 준비되었으므로 이제 화면에 렌더링하기만 하면 된다.

{listData?.length === 0 && '제품 정보가 존재하지 않습니다.'}

{listData?.map(item => (
	<Product key={item.number} onClick={() => navigate(`/store/productdetail/${item.name}`)}>
		<ProductImg isSale={item.inventory <= 0}>
			<img src={`https://firebasestorage.googleapis.com/v0/b/prj07pyroblossom.appspot.com/o/productsImage%2F${item.name}%2F${item.productInformationFile?.titleimage}?alt=media&token=bf2eff71-3c5e-4dc2-9706-445f95fd91e8`} alt='' />
		</ProductImg>

		<hr />

		<ProductTitle>

            <ProductName isSale={item.inventory <= 0}>
                {item.name}
            </ProductName>

            <div>
                 {item.inventory <= 0 ?
                      <SoldOutMsg>품절</SoldOutMsg>
                      :
                      <SaleMsg>판매중</SaleMsg>
                 }
            </div>

            <p>{item.price}원</p>

        </ProductTitle>
    </Product>
))}

만약 제품 데이터가 로딩되지 않았거나, DB에 제품 데이터가 없다면 제품 정보가 존재하지 않는다는 문구가 출력되도록 하였다.

제품 데이터가 존재한다면, map 함수를 이용한 배열 렌더링으로 적절하게 화면에 출력되도록 구현하였다. DB의 제품 데이터에는 제품의 재고량 데이터가 존재하는데, 조건부 렌더링을 이용하여 재고량이 0 이하인 경우에는 품절 표시가 출력되고 0 초과인 경우에는 판매중 표시가 출력되도록 하였다. 또한 제품 이미지를 클릭하였을 때 상세 페이지로 이동할 수 있도록 onClick 속성을 사용하였다. 렌더링의 결과물은 다음과 같다.

코드 평가.

평가 방법, 개인적인 코드 리뷰 및 Chat GPT 사용.

-> 조건부 렌더링에서 데이터 유효성 방법이 미흡. null 대신 더 명시적으로 0을 체크하는 방법을 사용하는 것이 더 안전함. 지금 코드에서는 옵셔널 체이닝(Optional Chaining)만을 null값 체크로 사용하면서 조건부 렌더링을 2개 따로 사용하고 있는데, 이를 하나로 합치는게 코드 가독성 면에서 더 나아보임.

-> 데이터 보안성 낮음. 제품 이미지 URL에 토큰이 노출되고 있으며, 액세스 권한이 없는 사용자도 URL을 통해 이미지에 접근할 수 있습니다. (DB에서 이미지를 꺼내오는 방법을 내 파이어스토어 계정을 통해 가져온 이미지의 웹 링크를 사용했는데, 이것이 문제.)

profile
프론트엔드 개발자를 준비하고 있습니다.

1개의 댓글

comment-user-thumbnail
2023년 7월 20일

유익한 글 잘 봤습니다, 감사합니다.

답글 달기