저번 포스팅 [주문앱 1탄] UI 만들기에서는 컴포넌트를 분리하고 기본적인 UI를 만들었다.
하지만 아직 만들지 않은 것이 있는데, 바로 카테고리이다.
아래의 와이어프레임을 보면 생과일
, 견과류/건과일
, 수제청/시럽
, 시리얼
, 케익
으로 이루어진 카테고리가 있다. 생과일
카테고리에 무화과, 사과, 청포도, 딸기 등의 생과일 목록이 있듯이 견과류/건과일
을 누르면 아몬드, 호두, 마카다미아, 건무화과 등의 목록이 나와야 한다.
이번 포스팅에서는 이 카테고리를 만들어볼 것이다.
최종적으로 완성된 결과는 아래와 같다!
현재 단계에서는 서버와의 통신 없이 각 토핑 종류에 따라 하드코딩된 데이터를 이용하기로 했다.
기존에는 이렇게 DUMMY_TOPPINGS
만을 만들었었다. 여기에는 그릭요거트
라는 하나의 카테고리에 들어가는 재료들만 있었다. 하지만 여러개의 카테고리를 만들고 그 안에 각기 다른 종류의 토핑들이 들어가야 하므로, DUMMY_CATEGORIES
라는 데이터를 만들기로 했다.
DUMMY_TOPPINGS: [
{
id: 't1',
name: '무가당 그릭요거트(100g)',
description: '최상급 우유와 유산균 외의 첨가물이 전혀 없는 그릭요거트',
price: 3500,
},
{
id: 't2',
name: '망고 그릭요거트(100g)',
description: '망고의 달콤함과 풍미를 담은 망고 그릭요거트',
price: 4500,
},
{
id: 't3',
name: '딸기 그릭요거트(100g)',
description: '설향딸기의 달콤함을 담은 딸기 그릭요거트',
price: 4500,
},
{
id: '4',
name: '황치즈 그릭요거트(100g)',
description: '세가지 치즈의 깊은 맛을 담은 황치즈 그릭요거트',
price: 5000,
},
]
DUMMY_CATEGORIES
데이터는 아래와 같다. 위에 만든 DUMMY_TOPPINGS
를 DUMMY_CATEGORIES
안에 넣었다.
그릭요거트
, 그래놀라
, 수제청&시럽
, 시리얼
라는 카테고리를 만들고 그 안에 각기 다른 DUMMY_TOPPINGS
를 넣었다.
const DUMMY_CATEGORIES = [
{
name: '그릭요거트',
id: 'c1',
DUMMY_TOPPINGS: [
{
id: 't1',
name: '무가당 그릭요거트(100g)',
description: '최상급 우유와 유산균 외의 첨가물이 전혀 없는 그릭요거트',
price: 3500,
},
{
id: 't2',
name: '망고 그릭요거트(100g)',
description: '망고의 달콤함과 풍미를 담은 망고 그릭요거트',
price: 4500,
},
{
id: 't3',
name: '딸기 그릭요거트(100g)',
description: '설향딸기의 달콤함을 담은 딸기 그릭요거트',
price: 4500,
},
{
id: '4',
name: '황치즈 그릭요거트(100g)',
description: '세가지 치즈의 깊은 맛을 담은 황치즈 그릭요거트',
price: 5000,
},
]
},
{
name: '그래놀라',
id: 'c2',
DUMMY_TOPPINGS: [
{
id: 't1',
name: '카카오 그래놀라',
description: '수제로 만든 달콤 쌉싸름한 카카오 그래놀라 한스쿱',
price: 1500,
},
{
id: 't2',
name: '얼그레이 그래놀라',
description: '직접 냉침한 밀크티로 만든 얼그레이 그래놀라 한스쿱',
price: 1500,
},
{
id: 't3',
name: '바닐라 그래놀라',
description: '바닐라빈을 듬뿍 넣은 수제 바닐라 그래놀라 한스쿱',
price: 1500,
},
{
id: 't4',
name: '코코넛 그래놀라',
description: '코코넛청크를 듬뿍 넣은 수제 그래놀라 한스쿱',
price: 1500,
},
]
},
{
name: '수제청&시럽',
id: 'c3',
DUMMY_TOPPINGS: [
{
id: 't1',
name: '딸기청',
description: '새콤달콤한 설향딸기로 만든 수제 딸기청',
price: 2000,
},
{
id: 't2',
name: '블루베리청',
description: '싱싱한 블루베리로 만든 수제 블루베리청',
price: 2000,
},
{
id: 't3',
name: '벌집꿀',
description: '산지직송한 달콤한 벌집꿀 30g',
price: 3500,
},
{
id: '4',
name: '연유',
description: '달달한 그릭요거트를 원하신다면 최고의 선택',
price: 1000,
},
]
},
{
name: '시리얼',
id: 'c4',
DUMMY_TOPPINGS: [
{
id: 't1',
name: '초코칩',
description: '인기만점 작은 물방울 모양의 초코칩',
price: 800,
},
{
id: 't2',
name: '드라이 마시멜로',
description: '큰 마시멜로가 부담스러울 때 딱 좋은 미니미 마시멜로',
price: 1000,
},
{
id: 't3',
name: '후르츠링',
description: '새콤달콤한 무지개색의 후르츠링 ',
price: 800,
},
{
id: '4',
name: '초코 프레첼',
description: '묵직하고 달콤한 허쉬의 초코 프레첼',
price: 1500,
},
]
},
]
ToppingsCategory라는 카테고리 컴포넌트를 만들었다. Toppings 컴포넌트에 더미 데이터가 있기 때문에, 거기서 prop으로 데이터를 받아 출력할 것이다. 카테고리의 이름(그릭요거트
, 그래놀라
, 수제청&시럽
, 시리얼
)을 props.DUMMY_CATEGORIES
에서 map()
으로 하나씩 꺼낸다.
// 📃 ToppingsCategory.jsx
import React from 'react';
import classes from './ToppingsCategory';
const ToppingsCategory = (props) => {
const selectHandler = (id) => {
props.onSelect(id);
}
return (
props.DUMMY_CATEGORIES.map(category =>
<li
key={category.id}
onClick={() => selectHandler(category.id)}
>
{category.name}
</li>
)
);
};
export default ToppingsCategory;
카테고리에는 선택할 수 있는 네가지 종류의 토핑들이 있다.(그릭요거트
, 그래놀라
, 수제청&시럽
, 시리얼
) 이중에서 그릭요거트
를 선택하면 여러 그릭요거트가 보여야 하고, 시리얼
을 선택하면 여러 시리얼이 보여야 한다.
그렇다면 이건 어떻게 구현할 수 있을까 생각하다가, 선택된 카테고리를 의미하는 State인 selectedCategory
를 만들기로 했다. 이 State는 각 카테고리의 id
를 의미한다.
만약 selectedCategory
가 c1
이면 그릭요거트
에 해당하는 재료들을 보여준다. 만약 c2
이면 그래놀라
에 해당하는 재료들을 보여준다.
이 selectedCategory
는 ToppingsCategory 컴포넌트에서도 쓰이지만, AvailableToppings 컴포넌트에서도 쓰인다. (AvailableToppings 컴포넌트: 선택된 카테고리에 있는 재료들을 보여주는 곳) 그리고 ToppingsCategory 컴포넌트와 AvailableToppings 컴포넌트는 모두 Toppings 컴포넌트에서 임포트된다. 그래서 selectedCategory
라는 State 또한 Toppings 컴포넌트에 선언했다.
ToppingsCategory 컴포넌트에서 카테고리를 선택할 수 있다. 그런데, 선택된 카테고리에 관한 정보는 상위 컴포넌트인 Toppings에서 필요하다. 왜냐하면, 그곳에 있는 DUMMY_CATEGORIES에서 데이터를 필터링하여 AvailableToppings라는 컴포넌트에 전달해야 하기 때문이다.
그렇다면 ToppingsCategory 컴포넌트에서 선택된 카테고리에 대한 데이터를 Toppings로 끌어올려야 한다.
먼저 ToppingsCategory 컴포넌트에서 카테고리가 선택되었을 때의 상황을 살펴보자.
// 📃 ToppingsCategory.jsx
import React from 'react';
import classes from './ToppingsCategory';
const ToppingsCategory = (props) => {
const selectHandler = (id) => {
props.onSelect(id);
}
return (
props.DUMMY_CATEGORIES.map(category =>
<li
key={category.id}
onClick={() => selectHandler(category.id)}
>
{category.name}
</li>
)
);
};
export default ToppingsCategory;
DUMMY_CATEGORIES
를 map()
을 이용하여 <li>
안에 펼친다. 그리고 이 중 하나의 카테고리가 클릭되면 selectHandler 함수에 해당 카테고리의 id를 넘긴다. Toppings 컴포넌트의 onSelect로 넘어온 id로 selectedCategory
를 업데이트한다. 이렇게 해서 ToppingsCategory 컴포넌트에서 선택된 카테고리를 Toppings에서 쓸 수 있게 되었다.
// 📃 Toppings.jsx
const Toppings = () => {
// 생략
const onSelect = (id) => {
setSelectedCategory(id);
}
return (
// 생략
<ToppingsCategory
DUMMY_CATEGORIES={DUMMY_CATEGORIES}
onSelect={onSelect}
/>
)
}
선택된 카테고리의 id를 끌어올려 가져온 이유는 데이터를 필터링하기 위해서이다.
DUMMY_CATEGORIES
에서 선택된 id를 갖고 있는 데이터만 선택하고, 그것을 toppingsInSelectedCategory
라고 선언하려고 한다.
만약 그래놀라
카테고리의 id가 선택되었다면 toppingsInSelectedCategory
는 그래놀라 재료들을 담은 객체일 것이다.
그리고 이들은 AvailableToppings 컴포넌트에 prop으로 넘겨져서 선택된 카테고리의 재료들을 화면에 보여주게 된다.
// 📃 Toppings.jsx
const Toppings = () => {
// 생략
const onSelect = (id) => {
setSelectedCategory(id);
}
const toppingsInSelectedCategory = DUMMY_CATEGORIES.filter(category => {
return category.id === selectedCategory;
});
return (
// 생략
<AvailableToppings
toppingsInSelectedCategory={toppingsInSelectedCategory}
/>
)
}
export default Toppings;
setSelectedCategory
에 인자로 넣어 selectedCategory
를 업데이트한다.filter()
함수를 이용하여 선택된 카테고리에 해당하는 재료들을 toppingsInSelectedCategory
라고 선언한다toppingsInSelectedCategory
를 prop으로 넘긴다이제 필터링된 데이터를 받은 AvailableToppings 컴포넌트에 가보자.
prop으로 받은 toppingsInSelectedCategory
에서 DUMMY_TOPPINGS
를 map()
으로 펼친 후, 거기서 각각 id와 name, description, price를 ToppingItem으로 넘겨주면 된다. 그럼 ToppingItem에서 이 정보를 활용하여 화면에 보여줄 것이다.
// 📃 AvailableToppings.jsx
import React from 'react';
import Card from '../UI/Card';
import ToppingItem from './ToppingItem/ToppingItem';
import classes from './AvailableToppings.module.css';
const AvailableToppings = (props) => {
const toppingsList =
props.toppingsInSelectedCategory[0].DUMMY_TOPPINGS.map(
topping =>
<ToppingItem
id={topping.id}
key={topping.id}
name={topping.name}
description={topping.description}
price={topping.price}
/>
)
return (
<section className={classes.toppings}>
<Card>
{toppingsList}
</Card>
</section>
);
};
export default AvailableToppings;
참고로 prop으로 받은 toppingsInSelectedCategory
는 아래와 같다.
{
name: '그릭요거트',
id: 'c1',
DUMMY_TOPPINGS: [
{
id: 't1',
name: '무가당 그릭요거트(100g)',
description: '최상급 우유와 유산균 외의 첨가물이 전혀 없는 그릭요거트',
price: 3500,
},
{
id: 't2',
name: '망고 그릭요거트(100g)',
description: '망고의 달콤함과 풍미를 담은 망고 그릭요거트',
price: 4500,
},
{
id: 't3',
name: '딸기 그릭요거트(100g)',
description: '설향딸기의 달콤함을 담은 딸기 그릭요거트',
price: 4500,
},
{
id: '4',
name: '황치즈 그릭요거트(100g)',
description: '세가지 치즈의 깊은 맛을 담은 황치즈 그릭요거트',
price: 5000,
},
]
},
}
<오류>
props.toppingsInSelectedCategory.DUMMY_TOPPINGS.map(() => {})
<정답>
props.toppingsInSelectedCategory[0].DUMMY_TOPPINGS.map(() => {})
처음에 첫번째처럼 작성해서 오류가 났었다. 그런데 도무지 이유를 모르겠어서 많이 헤맸는데, toppingsInSelectedCategory
가 아니라 toppingsInSelectedCategory[0]
라고 수정하여 해결했다.
왜 이렇게 해야 하는 걸까? 콘솔을 찍어서 확인해봤다.
props.toppingsInSelectedCategory
을 콘솔로 찍어봤을 때, 배열 안에 객체가 들어있다고 나온다.
그리고 배열의 0
번째 인덱스에 DUMMY_TOPPINGS가 있다고 나온다. 즉, toppingsInSelectedCategory가 객체가 아니라 배열 안에 있는 객체여서 0번째 인덱스를 선택해야 정확히 DUMMY_TOPPINGS
를 선택할 수 있는 것이다.
console.log(props.toppingsInSelectedCategory) // [{...}]
현재 기능 구현은 완료됐다. 카테고리를 선택하면 해당하는 카테고리에 있는 재료들이 화면에 표시된다.
이제 카테고리의 UI를 보기 좋게 꾸며보기로 했다. 가장 먼저 든 생각은, 어떻게 아래처럼 카테고리와 재료들이 자연스럽게 이어지게 만드냐는 것이었다.
아래처럼 AvailableToppings 컴포넌트 위에 ToppingsCategory 컴포넌트를 쓰고 css로 디자인을 변경하면 되나? 일단 이렇게 해보기로 했다.
<ToppingsCategory />
<AvailableToppings />
원래는 AvailableToppings 컴포넌트 안에서 AvailableToppings의 내용을 Card로 감쌌었는데, 이 Card 컴포넌트를 꺼내서 ToppingsCategory 컴포넌트와 AvailableToppings 컴포넌트를 감싸도록 했다.
// 📃 Toppings.jsx
<Card>
<ul>
<ToppingsCategory
DUMMY_CATEGORIES={DUMMY_CATEGORIES}
onSelect={onSelect}
/>
</ul>
<AvailableToppings
toppingsInSelectedCategory={toppingsInSelectedCategory}
/>
</Card>
이렇게 해서 Card 컴포넌트 안에 ToppingsCategory와 AvailableToppings 컴포넌트가 들어있는 것을 확인했다.
카테고리의 종류가 잘 기능하도록 만들었으므로, 여기에 추가적인 CSS를 적용하여 UI를 완성했다.
카테고리가 완성되었다!! 아직 부족한 점이 많지만, 일단 다음 기능을 구현하기 위해 CSS는 여기서 멈추기로 했다.
와이어프레임으로 계획했던 것처럼 어떤 카테고리를 선택하면 그 카테고리가 있는 칸의 색이 바뀌었으면 좋겠다.
화면의 너비를 줄이면 글자 등이 레이아웃을 무시하고 벗어나는데, 이를 해결하고 싶다.
카테고리를 처음 만들어보기도 하고, 강의에서 배운 적도 없어서 시작할 때 많이 막막했다.
하지만 계획과 비슷한 카테고리를 구현하게 되어 뿌듯했다!