전체 데이터를 한 번에 받고 그 후 모든 걸 front에서 처리한다.
구조도
최상위 컴포넌츠 (ProductList.js)
카테고리 checkbox :
ProductList.js ⊃ ClassificationBox.js ⊃ ClassificationBoxCheckbox.js
checkbox에 따라 보여줄 컴포넌츠:
흐름
- 카테고리
checkbox
를 선택하면 전체 데이터 (배열)의 각 카테고리 (객체)를 돌면서 해당 카테고리 이름이 있는 객체에서 이름을 가져와서 해당 input의 value로 업데이트를 시켜준다.- 체크된 카테고리 이름을 배열에 넣어준다 (
checkedNames
state)- 배열(
checkedNames
state) 안에 있는 값에 따라 해당 카테고리에 대한 컴포넌트를 보여준다
- 컴포넌트를 보여주는 2가지 방식
- 전체 데이터의 순서대로 보여주는 방법
- 카테고리 선택한 순서대로 보여주는 방법
- fetch에서 전체 데이터를 바로 담을 state(
categories
)와 선택된 카테고리에 따른 데이터를 보여줄 state(copyCategories
)를 구분 한다.
카테고리 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} // 부모 컴포넌츠로 연결
/>
);
})}
설명
categories
: 전체 데이터를 담은 배열.categories
를 돌면서 한 input에 onClick이 발생하면 부모 컴포넌츠에 value
(category.name)가 setState된다.value
를 검사하는 메소드를 연결한다.체크된 카테고리 이름을 배열(
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 업데이트
}
}
}
};
배열(
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([]);
}
};
- 기존 메뉴 탭 방식: 사진/영양정보로 보기 버튼을 누르면 아래 컴포넌트 모두 교체
- 내 메뉴 탭 방식: 버튼을 눌렀을 때
카테고리 이름
은 그대로 (디카페인 설명
만 교체) &아래 사진 vs 테이블
교체
사진/영양정보 보기 버튼 컴포넌트
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 = ["사진으로 보기", "영양정보로 보기"]
부모의 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>
);
}
}
리팩토링 전 코드
구현한 로직 요약
선택된 카테고리에서 상세 분류 버튼의 뉴/시즌 체크박스 클릭 시 해당 카테고리에 해당 상품이 모두 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 />
가 나오지 말아야 하는데, 다른 카테고리/상품들과 함께 나온다.