[2nd 팀 프로젝트] STARBUCKS 기록하고 싶은 코드

Kate Jung·2021년 2월 7일
0

Projects

목록 보기
7/10
post-thumbnail

📌 ListPage 전체코드

https://github.com/katej927/STARBUGS_ListPage_210205

✅ 카테고리 checkbox 기능 구현

👉 개요

전체 데이터를 한 번에 받고 그 후 모든 걸 front에서 처리한다.

구조도

  • 최상위 컴포넌츠 (ProductList.js)

    • 카테고리 checkbox :

      ProductList.js ⊃ ClassificationBox.js ⊃ ClassificationBoxCheckbox.js

    • checkbox에 따라 보여줄 컴포넌츠:

흐름

  1. 카테고리 checkbox를 선택하면 전체 데이터 (배열)의 각 카테고리 (객체)를 돌면서 해당 카테고리 이름이 있는 객체에서 이름을 가져와서 해당 input의 value로 업데이트를 시켜준다.
  2. 체크된 카테고리 이름을 배열에 넣어준다 (checkedNames state)
  3. 배열(checkedNames state) 안에 있는 값에 따라 해당 카테고리에 대한 컴포넌트를 보여준다
    • 컴포넌트를 보여주는 2가지 방식
      1. 전체 데이터의 순서대로 보여주는 방법
      2. 카테고리 선택한 순서대로 보여주는 방법
        • fetch에서 전체 데이터를 바로 담을 state(categories)와 선택된 카테고리에 따른 데이터를 보여줄 state(copyCategories)를 구분 한다.

👉 1

카테고리 checkbox를 선택 시 input의 value로 업데이트

    // 1. 인풋 업데이트 (카테고리 이름 업뎃)_최상위 컴포넌츠(ProductList.js)
      const isCheckedCategoryName = (e) => {
        const { name, value } = e.target;
        setState({ [name]: value });
        makeCheckedNames(value);   // 검사 mtd 연결
      };

    // 2. 자식 컴포넌츠 (ClassificationBoxCheckbox.js)
    {categories.map((category) => {
            return (
                <input
                  type="checkbox"
                  value={category.name}
                  name="subcategoryCheckboxId1"
                  onClick={isCheckedCategoryName}  // 부모 컴포넌츠로 연결
                />
            );
          })}

설명

  • 코드 간소화 ver
  • categories : 전체 데이터를 담은 배열.
  • categories를 돌면서 한 input에 onClick이 발생하면 부모 컴포넌츠에 value(category.name)가 setState된다.
    - 그 후 value 를 검사하는 메소드를 연결한다.

👉 2

체크된 카테고리 이름을 배열(checkedNames state)에 넣어주기

    // const [checkedNames, setCheckedNames] = useState(["전체 보기"]);

    // 배열(checkedNames)에 넣어도 될지 검사하는 메소드
    const makeCheckedNames = (value) => {
        const isInclude = checkedNames.some((name) => name === value); // 이미 체크된 카테고리
        const isIncludeAllBtn = checkedNames.some((name) => name === "전체 보기"); // "전체 보기"가 checkedNames에 있을 경우

        // 1. 2번 체크시 해당 카테고리 이름 배열에서 제거
        if (isInclude) {  
          const deleteOverlappedName = checkedNames.filter(
            (name) => name !== value
          );
          setCheckedNames(deleteOverlappedName);
        } else {  
          // 1. 배열에 중복된 값이 없을 때
          if (value === "전체 보기") {     
            // 2. "전체 보기" 선택 시 배열에 "전체 보기"만 업데이트
            setCheckedNames(["전체 보기"]);
          } else {
            // 2. "전체 보기"가 배열에 있는 경우
            if (isIncludeAllBtn) {        
              setCheckedNames([value]);                     // 2-1. "전체 보기" 없애고 value 업데이트
              setCopyCategories([]);                        // 2-2. copyCategories를 빈 배열로 만들어준다.
            } else {                                        // 2. "전체 보기"가 배열에 없는 경우
              setCheckedNames((prev) => [...prev, value]);  // 2-1. 이전 값 뒤에 새로운 value 업데이트
            }
          }
        }
      };

👉 3

배열(checkedNames state) 내부 값에 따라 해당 카테고리에 대한 컴포넌트를 보여준다

3-1. 전체 데이터의 순서대로 보여주는 방법

        {copyCategories.map((category) => {
                    const isAll = checkedNames.includes("전체 보기"); // "전체 보기"가 체크된 카테고리 이름만 모아둔 배열(checkedNames)에 있다면
                    const isInclude = checkedNames.includes(category.name);  // category.name의 값이 checkedNames에 있다면
                    if (!isInclude && !isAll) return null;  // 해설 참고

                    return (
                      <div>
                        보여줄 카테고리 컴포넌츠 내용
                      </div>
                    );
                  })}

해설

  • "전체 보기"와 다른 카테고리 이름은 checkedNames에 공존하지 않는다

    = checkedNames 에 값이 있다면, !isInclude && isAll 이거나 isInclude && !isAll

    checkedNames 에 없는 값이 map을 돌게 되면 !isInclude && !isAll 가 나옴

    return null 로 화면에 나오지 않게 된다.

    즉, 체크되지 않은 값은 화면에 나오지 않게 된다.

3-2. 카테고리 선택한 순서대로 보여주는 방법

        // subcategoryCheckboxId1 : 모든 체크박스 input의 name (전체보기 input 제외)

        // 1. checkedNames가 변화하게 되면 useEffect 실행하며 검사 메소드로 연결
          useEffect(() => {
            alignData();
          }, [checkedNames]);

          // 2. 선택한 카테고리 순서로 보여주는 메소드
          const alignData = () => {
            // checkedNames에 방금 클릭한 value가 있다면
            const isInclude = checkedNames.some(
              (name) => name === state.subcategoryCheckboxId1
            ); 

            // 2-1. checkedNames에 값이 있다면
            if (checkedNames.length > 0) {  
              checkedNames.forEach((name) => {
                // 2-2. "전체 보기"가 있다면
                if (name === "전체 보기") {   
                  // 2-3. 전체 데이터를 업데이트
                  setCopyCategories(categories);  
                } else {
                  // 2-2. "전체 보기" 아닌 카테고리가 선택되었다면
                  
                  // 전체 데이터(categories)에서 선택한 카테고리 객체 추출
                  const matchedDatas = categories.filter(
                    (category) => category.name === name
                  );  

                  // 2-3. 방금 누른 e.target.value가 checkdNames에 있다면
                  if (isInclude) { 
                  // 2-4. 복제데이터에 추출한 객체를 넣음 
                    setCopyCategories(copyCategories.concat(matchedDatas)); 
                    
                  } else {  
                    // 2-3. 방금 누른 e.target.value가 checkdNames에 없다면
                    
                    // 2-4. 방금 업데이트된 카테고리 이름이 없도록 copyCategories 업데이트
                    const filteredGoneObj = copyCategories.filter(
                      (category) => category.name !== state.subcategoryCheckboxId1
                    );      
                    setCopyCategories(filteredGoneObj);
                  }
                }
              });
            } else {   
              // 2-1. checkedNames에 값이 없다면
              
              // 2-2. 빈 화면 출력
              setCopyCategories([]);  
            }
          };

✅ MenuTap을 활용한 컴포넌트 재사용 (사진/영양정보 버튼)

  • 기존 메뉴 탭 방식: 사진/영양정보로 보기 버튼을 누르면 아래 컴포넌트 모두 교체
  • 내 메뉴 탭 방식: 버튼을 눌렀을 때 카테고리 이름은 그대로 (디카페인 설명 만 교체) & 아래 사진 vs 테이블 교체

👉 부모 comp

사진/영양정보 보기 버튼 컴포넌트

    export default class ProductCategory extends Component {
      constructor() {
        super();
        this.state = {
          currentId: 1,
        };
      }

      clickHandler = (id) => {
        this.setState({ currentId: id });
      };

      render() {
        const { categories } = this.props;
        const { currentId } = this.state;

        return (
          <ProductCategoryWrapper>
            <AllBtnsWrapper>
              <BtnWrap>
                {BTN_WATCH_THROUGH.map((btn, idx) => {
                  return (
                    <BTN
                      key={idx}
                      onClick={() => this.clickHandler(idx + 1)}
                      className={btn}
                    >
                      {btn}
                    </BTN>
                  );
                })}
              </BtnWrap>
            </AllBtnsWrapper>

            <SubCategory categories={categories} currentId={currentId} />  // 자식 comp
          </ProductCategoryWrapper>
        );
      }
    }

    // - 배열
    // 사진/영양정보 버튼 text
    const BTN_WATCH_THROUGH = ["사진으로 보기", "영양정보로 보기"]
  • 자식 comp
    버튼에서 어떤 걸 선택했는지에 대한 값을 담은 state(currentId)를 내려줌

👉 자식 comp

부모의 state에 따라 사진 vs 테이블 컴포넌트 보여줌

    export default class SubCategory extends Component {
      render() {
        const { currentId, categories } = this.props;
        return (
          <div className="subCategory">
            <div className="subCategoryDiv">
              {categories.map((category) => {
                const MAPPING_OBJ = {   // currentId에 대한 객체
                  1: <ProductMap products={category.products} />,  // 사진으로 보기
                  2: <NutritionMap products={category.products} />,  // 영양정보로 보기 
                };
                return (
                  <div>
                    <SUBCATEGORY_TITLE>
                      <SubcategoryName>{category.name}</SubcategoryName> // 서브카테고리 이름
                      <SubcategoryDesc>  // 서브카테고리 설명 (조건부 렌더링)
                        {currentId === 1 && category.description}
                      </SubcategoryDesc>
                    </SUBCATEGORY_TITLE>
                    {MAPPING_OBJ[currentId]}  // currentId에 따라 사진 vs 테이블 보여줌
                  </div>
                );
              })}
            </div>
          </div>
        );
      }
    }

✅ 리팩토링 하고 싶은 코드

리팩토링 전 코드

  • 필요한 부분만 간소화 시킨 코드

👉 SubCategory.js

  • 구현한 로직 요약

    • 선택된 카테고리에서 상세 분류 버튼의 뉴/시즌 체크박스 클릭 시 해당 카테고리에 해당 상품이 모두 false일 경우 <NoResult />('검색 결과 없습니다'가 나오는 컴포넌츠) 출현

    • 주의

      선택된 카테고리들 중 상품이 true인 카테고리가 있다면 <NoResult /> 가 나오면 안된다.

        return (
                    // [조건] 상세 분류 버튼 중 무엇을 체크 했는가?
                    const isNewChecked = checkedMarkNames.includes("신규 출시된 메뉴");
                    const isSeasonChecked = checkedMarkNames.includes(
                      "한정기간 출시되는 시즌성 메뉴"
                    );

                    // [조건] products에 1개라도 뉴/시즌이 true인가?
                    const atLeastOneIsNewhasTrue = category.products.some(
                      (product) => product.isnew === true
                    );
                    const atLeastOneIsSeasonhasTrue = category.products.some(
                      (product) => product.isseason === true
                    );

                    // [조건] SUBCATEGORY_TITLE 나오면 안되는 경우 (SUBCATEGORY_TITLE = 카테고리 이름)
                    const CheckedIsNewButAllFalse =
                      isNewChecked && atLeastOneIsNewhasTrue === false;
                    const CheckedIsSeasonButAllFalse =
                      isSeasonChecked && atLeastOneIsSeasonhasTrue === false;

                    // [조건] 다른 뉴/시즌 체크박스 선택시 값이 있는 경우
                    const CheckedIsNewHasTrue = isNewChecked && atLeastOneIsNewhasTrue;
                    const CheckedIsSeasonHasTrue =
                      isSeasonChecked && atLeastOneIsSeasonhasTrue;

                    // [조건] NoResult.js 출현 조건
                    const canShowNoResult =
                      currentId === 1 &&
                      isInclude &&
                      !isAll &&
                      ((CheckedIsNewButAllFalse && !CheckedIsSeasonHasTrue) ||
                        (CheckedIsSeasonButAllFalse && !CheckedIsNewHasTrue));
        							// 다른 뉴/시즌 체크박스 선택시 값이 있는 경우 에는 <NoResult />가 나오지 않도록 구성

                    return (
                      <div>
                        보여줄 컴포넌츠 코드
                        {canShowNoResult && <NoResult />}
                      </div>
                    );

👉 비슷한 패턴의 코드

ProductMap.js

        return (
            <ProductMapWrap>
              {products.map((product) => {
                // [조건] 상세 분류 버튼 중 무엇을 체크 했는가?
                const isNewChecked = checkedMarkNames.includes("신규 출시된 메뉴");
                const isSeasonChecked = checkedMarkNames.includes(
                  "한정기간 출시되는 시즌성 메뉴"
                );

                // [조건] 각 상품의 뉴/시즌이 true 인가?
                const isNewProduct = product.isnew === true;
                const isSeasonProduct = product.isseason === true;

                // [상품 보여줄 수 있는 경우의 수]
                const canShowProduct =
                  (!isNewChecked && !isSeasonChecked) ||
                  (isNewChecked && isNewProduct) ||
                  (isSeasonChecked && isSeasonProduct);

                if (!canShowProduct) return null;

                return (
                  <Div>
                    보여줄 컴포넌츠
                  </Div>
                );
              })}
            </ProductMapWrap>
          );

SubCategory.js 의 코드를 줄인다면 줄일 수 있는 코드들

👉 아쉬운 점

  • 코드가 너무 길다

  • <NoResult /> 가 나오지 말아야 할 상황에도 나온다.

    전체 보기를 눌렀을 때, 어떤 카테고리에는 뉴/시즌 상품이 있고 어떤 카테고리에는 뉴/시즌 상품이 없어서 <NoResult /> 가 나오지 말아야 하는데, 다른 카테고리/상품들과 함께 나온다.

profile
복습 목적 블로그 입니다.

0개의 댓글