heyday2024 님의 블로그
[사전캠프 퀘스트] 한식 메뉴 렌더링하기 본문
https://koreanfood-menu.netlify.app/
주어진 데이터를 가지고, 데이터를 화면에 표시했다. 메뉴판 느낌으로 스타일링하고, 각 객체는 각각의 가격, 카테고리, 설명을 포함하고 있다. 카테고리별로 메뉴를 볼 수있고, 검색 기능을 사용해서, 입력한 키워드를 포함하는 요소를 보여줄 수 있다.
- 각 메뉴를 카테고리 별대로 볼 수 있도록 하는 기능을 추가했다. (드롭다운 메뉴)
- 음식을 bootstrap에서 가져온 cards 코드를 이용해 정갈하게 나열했다.
- filter과 includes 메소드를 이용해서 특정 키워드를 사용자가 입력한 후 검색버튼을 누르면 검색어를 포함한 메뉴가 보이도록 하는 기능을 추가했다.
- createElement와 appendChild를 사용: 각 요소를 개별적으로 생성하고, 이를 부모 요소에 하나씩 추가할 수 있음.
- 여러 요소를 한 번에 추가하는 경우 appendchild를 여러 번 호출하는 것이 innerHTML보다 성능이 좋을 수 있음.
- 하지만, 코드가 다소 장황할 수 있고, 요소를 하나씩 생성하고, 추가해야함으로 코드가 길어질 수 있음.
- innerHTML 사용: 전체 HTML 구조를 문자열로 생성하여 한번에 삽입 (내가 사용한 방법)
- HTML 구조를 한 번에 문자열로 작성하기 떄문에 간결함.
- 하지만 사용자 입력을 포함할 때 XSS 공격에 취약할 수 있음(보안문제), innerHTML을 사용하면 기존의 모든 자식 요소가 제거되고, 새로운 내용이 삽입됨.(굳이 코드를 reset시킬 필요가 없는 것을 원하면 장점이 됨. 이 이유로 innerHTML 사용했음.)
- XSS(Cross-Site Scripting) 공격: 익의적인 사용자가 웹 애플리케이션에 스크립트를 삽입하여 다른 사용자에게 피해를 주는 공격. 입력처리 시 악성 스크립트를 실행시키는 방법.
- createElement와 insertAdjacentElement 사용: 요소를 특정 위치에 삽입할 수 있음. (예를 들어 beforeend를 사용하면 부모요소의 끝에 자식 추가 가능.)
- 요소를 부모 요소의 시작, 끝, 특정 요소의 앞 뒤 등 원하는 위치에 다양하게 삽입 가능.
- 'beforebegin': 현재 요소 바로 앞에 삽입
- 'afterbegin': 현재 요소의 첫 번째 자식으로 삽입
- 'beforeend': 현재 요소의 마지막 자식으로 삽입
- 'afterend': 현재 요소 바로 뒤에 삽입
- 위치 지정 문자열이 다소 복잡할 수 있음.
- 요소를 부모 요소의 시작, 끝, 특정 요소의 앞 뒤 등 원하는 위치에 다양하게 삽입 가능.
1. 검색 기능: 사용자에게 keyword를 받아 검색 기능 제공
//키워드 별
const keyword = document.getElementById("keyword");
const searchBtn = document.querySelector(".search-btn");
searchBtn.addEventListener("click", function () {
sortMenu(4, keyword.value);
});
//`value`라는 속성은 HTML요소의 현재 값을 반환하거나 설정할 떄 사용. 주로 `<input>`,` <textarea>`, `<select>`와 같은 폼 요소에서 사용됨.
filteredItems = menuItems.filter(
(item) =>
item.name.includes(word) ||
item.description.includes(word) ||
item.category.includes(word) ||
item.price.includes(word)
);
`filter()` : 주어진 배열의 일부에 대한 얕은 복사본(새로운 배열)을 생성함//원래 배열은 변경되지 않음!. 이 새롭게 만들어진 배열은 제공된 함수(predicate function)에 의해 구현된 테스트를 통과한 요소만을 갖음.
const numbers = [1, 2, 3, 4, 5];
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // [2, 4]
`includes()`: 배열이나 문자열이 특정 값을 포함하고 있는지 여부를 확인.(즉, 참과 거짓을 반환함. boolean 값)
const sentence = 'The quick brown fox';
console.log(sentence.includes('quick')); // true
console.log(sentence.includes('lazy')); // false
2. 카테고리 별 분류 기능
const menuList = document.getElementById("menu-list");
let option;
// 전체(0), 식사류(1), 면류(2), 음료(3), keyword(4)
function sortMenu(option, word = "") {
let filteredItems;
switch (option) {
case 0: // 전체
filteredItems = menuItems;
break;
case 1: // 식사류
filteredItems = menuItems.filter((item) => item.category === "식사류");
break;
case 2: // 면류
filteredItems = menuItems.filter((item) => item.category === "면류");
break;
case 3: // 음료
filteredItems = menuItems.filter((item) => item.category === "음료");
break;
case 4: // 키워드 검색
filteredItems = menuItems.filter(
(item) =>
item.name.includes(word) ||
item.description.includes(word) ||
item.category.includes(word) ||
item.price.includes(word)
);
break;
default:
filteredItems = menuItems;
break;
}
// 기존 메뉴 리스트 내용 제거
// menuList.innerHTML = "";
menuList.innerHTML = filteredItems
.map(
(item) => `<div class="card" style="width: 18rem">
<img src=${item.src} class="card-img-top" alt="${
item.name + " 이미지"
}" />
<div class="card-body">
<h5 class="card-title menu_item">${item.name}</h5>
<p class="card-text description">
${item.description}
</p>
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item category">${item.category}</li>
<li class="list-group-item price">${item.price}</li>
</ul>
</div>`
)
.join("");
}
sortMenu(0); //default로 전체 메뉴 보여줌
//카테고리 별
const categoryBtn = document.querySelector(".category_text");
const all = document.querySelector(".all");
const meal = document.querySelector(".meal");
const noodle = document.querySelector(".noodle");
const drinks = document.querySelector(".drinks");
all.addEventListener("click", function () {
categoryBtn.textContent = "전체";
sortMenu(0);
});
meal.addEventListener("click", function () {
categoryBtn.textContent = "식사류";
sortMenu(1);
});
noodle.addEventListener("click", function () {
categoryBtn.textContent = "면류";
sortMenu(2);
});
drinks.addEventListener("click", function () {
categoryBtn.textContent = "음료";
sortMenu(3);
});
- 이런식으로 아예 sortMenu 함수를 만들어서 사용(가독성 높이기 위함)
- 각 카테고리별로 option 값 지정해서 그 option 값에 해당되는 메뉴들만 filtering해서 새로운 배열을 만들어줌. 그리고 만든 배열을 기반으로 innerHTML 사용해서 요소 추가해줌.
- 요소의 ID를 사용해서 js에서 그 요소 지정할 때, getElementByID와 querySelector 차이
- querySelector("#menu-list"): CSS 선택자를 사용하기 때문에 ID 앞에 #을 붙여야함.
- querySelector는 더 복잡한 CSS 선택자를 처리할 수 있지만, 선택자가 복잡할수록 속도가 느릴 수 있음. 단일 ID 선택을 한다면 getElementById보다 성능이 떨어짐.
2. flex 관련 내용 - 요소 한 줄에 특정 개수만큼만 보이게 하고 싶을 때:
- flex-basis 사용: flex 아이템의 기본 크기 설정.(flex-direction이 row일 떄는 너비, column일 때는 높이). 각종 단위 수(px, % 등) 들어갈 수 있음.
- 예를 들어, 한 줄에 3개씩 배치하고 싶다면 각 요소의 너비를 33.33%로 설정하여 한 줄에 3개가 들어가도록 만들 수 있음.
- flex-grow 사용: 유연하게 늘리는 법. 아이템이 flex-basis 값보다 커질 수 있는지를 결정하는 속성. 숫자값이 들어감. 0보다 큰 값이 설정되면 해당 아이템이 유연한 박스로 변하고 원래 크기보다 커지며 빈 공간을 메우게 됨.(기본 값이 0이어서 따로 적용하기 전에는 아이템이 늘어나지 않음). flex-basis를 제외한 여백 부분을 flex-grow에 지정된 숫자의 비율로 나누어 가짐.
- 예를 들어, flex: 1 1 calc(33.33%); 이런 식으로 flex-grow 속성을 사용하여 한 줄에 3개씩 꽉 차게 만들 수 있음.
- 줄넘김 처리: `flex-wrap`
- `nowrap`(기본값, 넘치면 걍 삐져나옴), `wrap`(줄바꿈함),`wrap-reverse`(역순으로 줄바꿈함)
- 정렬: `justify`는 메인축 방향으로 정렬(수평),`align`은 수직 축으로 정렬
1. `justify-content`: 메인축 방향으로 아이템 정렬
- `flex-start`(기본값, 시작점으로 정렬),`flex-end`(끝점 정렬), `center`(가운데),`space-between`(아이템들의 사이에 균일한 간격 만들어줌),`space-around`(아이템들의 둘레에 균일한 간격),`space-evenly`(아이템들의 사이와 양 끝에 균일한 간격//IE와 EDGE 브라우저에서는 지원 안됨.)
2. `align-items`: 수직축 방향으로 아이템들 정렬.
- `stretch`(기본값, 수직축 방향),`flex-start`,`flex-end`,`center`,`baseline`
3. `align-content`: 여러행 정렬, `wrap`이 설정된 상태에서, 아이템들의 행이 2줄 이상 되었을 땨의 수직축 방향 정렬.
코드
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>한식 메뉴 렌더링하기</title>
<!-- 부트스트랩 -->
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
crossorigin="anonymous"
/>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"
></script>
<!-- 구글 아이콘 써보기 -->
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0"
/>
<link rel="stylesheet" href="./main.css" />
<script defer src="./main.js"></script>
</head>
<body>
<div class="top">
<div class="material-symbols-outlined icon">restaurant</div>
<h1>한식 메뉴</h1>
</div>
<div class="container">
<div class="button_area">
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle category_text" type="button" data-bs-toggle="dropdown" aria-expanded="false">
카테고리
</button>
<ul class="dropdown-menu">
<li><button class="dropdown-item all" type="button">전체</button></li>
<li><button class="dropdown-item meal" type="button">식사류</button></li>
<li><button class="dropdown-item noodle" type="button">면류</button></li>
<li><button class="dropdown-item drinks" type="button">음료</button></li>
</ul>
</div>
<div class="filtering">
<input type="text" id ="keyword" placeholder="키워드(한 단어)를 입력해주세요. (ex) 고기">
<button type="button" class="btn btn-outline-secondary search-btn">검색</button>
</div>
</div>
<div class="menu" id="menu-list">
</div>
</div>
</body>
</html>
@import url("https://fonts.googleapis.com/css2?family=Gowun+Dodum&family=Nanum+Pen+Script&display=swap");
* {
font-family: "Gowun Dodum", sans-serif;
font-weight: 400;
font-style: normal;
}
body {
display: flex;
justify-content: center;
/* 부모요소의 주 축(가로축)에서 자식 요소들을 가운데로 정렬 */
align-items: center;
/* 교차 축에서의 정렬(세로축 정렬) */
flex-direction: column;
padding: 50px 0;
margin: 0 40px;
background-color: #ede5d4;
}
.top {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
padding-bottom: 30px;
border-bottom: solid 2px #7e3a19;
width: 100%;
color: #7e3a19;
}
.icon {
padding-bottom: 20px;
font-size: 50px;
}
.container {
display: flex;
justify-content: center;
align-items: center;
padding: 30px 0;
flex-direction: column;
}
.button_area {
display: flex;
width: 100%;
justify-content: space-between;
/* justify-content: space-between는 두 자식 요소를 양 끝에 배치하고 싶을 때 사용함 */
padding-bottom: 40px;
}
.filtering {
display: flex;
right: 0;
}
#keyword {
border-radius: 5px;
border: solid 1px lightslategray;
margin-right: 5px;
padding-left: 10px;
width: 300px;
}
.menu {
display: flex;
width: 100%;
gap: 10px;
/* gap으로 요소 간격 지정 가능 */
flex-wrap: wrap;
/* 요소들이 넘치면 다음 줄로 넘어가게 하기 위해 flex-wrap: wrap 사용 */
}
.card {
flex-basis: calc(33.33% - 10px);
/* 요소의 너비를 3등분함(33.33%)
각 요소의 너비를 3등분하되, 간격(gap)을 고려해 calc()를 사용하여 정확히 3개씩 배치되도록 함. */
margin-bottom: 10px; /* 줄 간격을 위해 아래 여백 추가 */
}
.card-img-top {
height: 250px;
/* 사진 사이즈를 일정하게 하고 싶었음 */
}
const menuItems = [
{
name: "비빔밥",
description: "밥 위에 나물, 고기, 고추장 등을 얹고 비벼 먹는 한국 요리",
price: "₩12,000",
category: "식사류",
src: "https://img.etoday.co.kr/pto_db/2019/07/20190726153503_1350707_1200_876.jpg",
},
{
name: "김치찌개",
description: "김치와 돼지고기 등을 넣고 끓인 한국의 찌개 요리",
price: "₩12,000",
category: "식사류",
src: "https://static.wtable.co.kr/image-resize/production/service/recipe/655/16x9/74eb99a1-cb37-4ef0-a3a9-f7ab12e3b8fe.jpg",
},
{
name: "불고기",
description: "양념한 고기를 구워서 먹는 한국 요리",
price: "₩15,000",
category: "식사류",
src: "https://gomean.co.kr/wp-content/uploads/2023/06/gm-bulgogiTINY.jpg",
},
{
name: "떡볶이",
description: "떡과 어묵을 고추장 양념에 볶아 만든 한국의 간식",
price: "₩8,000",
category: "식사류",
src: "https://i.namu.wiki/i/A5AIHovo1xwuEjs7V8-aKpZCSWY2gN3mZEPR9fymaez_J7ufmI9B7YyDBu6kZy9TC9VWJatXVJZbDjcYLO2S8Q.webp",
},
{
name: "잡채",
description: "당면과 여러 채소, 고기를 볶아 만든 한국 요리",
price: "₩12,000",
category: "면류",
src: "https://recipe1.ezmember.co.kr/cache/recipe/2023/09/16/7dbb36575d7f1d26346f794f3af0d2de1.jpg",
},
{
name: "잔치국수",
description: "깊이 우려낸 멸치 육수를 베이스로한 한국의 국수 요리 ",
price: "₩11,000",
category: "면류",
src: "https://static.wtable.co.kr/image-resize/production/service/recipe/438/16x9/ed2bf141-5342-4804-ac34-6a30fb525b01.jpg",
},
{
name: "비빔국수",
description:
"풍미 가득한 고추장 양념이 더해진 새콤하고 매콤한 한국의 국수 요리",
price: "₩12,000",
category: "면류",
src: "https://recipe1.ezmember.co.kr/cache/recipe/2018/01/19/9deb7510516f154f465f04aa46379d6e1.jpg",
},
{
name: "콩국수",
description: "콩을 직접 갈아 만든 고소한 국물 베이스의 국수 요리",
price: "₩13,000",
category: "면류",
src: "https://i.namu.wiki/i/4ChBILJgL-UnL7OZnW3kbx84D0R9wfKnlLj159tKil6RrGtqYcF3M_-LLXqDnaNZVHGkGbXNn53t2SZVvpNULg.webp",
},
{
name: "탄산음료",
description: "콜라, 환타, 사이다 중 택 1",
price: "₩2,000",
category: "음료",
src: "https://d15q6xcjx71x0s.cloudfront.net/_30124f1c05.jpg",
},
{
name: "오렌지 주스",
description: "직접 오렌지를 갈아 만든 홈메이드 주스",
price: "₩5,000",
category: "음료",
src: "https://cafe24.poxo.com/ec01/bringcoffee/Xeym8gXyw/uNs04t9Tz1Dh1akVDrAs/NfZ9w9APGK/1238tOpjRkjIaCzg3Ip5GOD8RolSB5ES06i4T0Q9GQCQ==/_/web/product/medium/202211/82019e1922390df7000151fa6aff5f3b.jpg",
},
{
name: "식혜",
description: "찹쌀이 들어간 달콤한 한국의 전통적인 음료",
price: "₩3,000",
category: "음료",
src: "https://cdn.sisajournal.com/news/photo/first/200609/img_118910_1.jpg",
},
];
// map은 배열을 순회하면서 각 요소를 변환하고 새로운 배열을 반환.
// forEach는 배열을 순회하면서 특정 작업을 수행하되, 새로운 배열을 반환하지 않음.
// const menuList = document.querySelector("#menu-list");
const menuList = document.getElementById("menu-list");
let option;
// 전체(0), 식사류(1), 면류(2), 음료(3), keyword(4)
function sortMenu(option, word = "") {
let filteredItems;
switch (option) {
case 0: // 전체
filteredItems = menuItems;
break;
case 1: // 식사류
filteredItems = menuItems.filter((item) => item.category === "식사류");
break;
case 2: // 면류
filteredItems = menuItems.filter((item) => item.category === "면류");
break;
case 3: // 음료
filteredItems = menuItems.filter((item) => item.category === "음료");
break;
case 4: // 키워드 검색
filteredItems = menuItems.filter(
(item) =>
item.name.includes(word) ||
item.description.includes(word) ||
item.category.includes(word) ||
item.price.includes(word)
);
break;
default:
filteredItems = menuItems;
break;
}
// 기존 메뉴 리스트 내용 제거
// menuList.innerHTML = "";
menuList.innerHTML = filteredItems
.map(
(item) => `<div class="card" style="width: 18rem">
<img src=${item.src} class="card-img-top" alt="${
item.name + " 이미지"
}" />
<div class="card-body">
<h5 class="card-title menu_item">${item.name}</h5>
<p class="card-text description">
${item.description}
</p>
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item category">${item.category}</li>
<li class="list-group-item price">${item.price}</li>
</ul>
</div>`
)
.join("");
}
sortMenu(0); //default로 전체 메뉴 보여줌
//카테고리 별
const categoryBtn = document.querySelector(".category_text");
const all = document.querySelector(".all");
const meal = document.querySelector(".meal");
const noodle = document.querySelector(".noodle");
const drinks = document.querySelector(".drinks");
all.addEventListener("click", function () {
categoryBtn.textContent = "전체";
sortMenu(0);
});
meal.addEventListener("click", function () {
categoryBtn.textContent = "식사류";
sortMenu(1);
});
noodle.addEventListener("click", function () {
categoryBtn.textContent = "면류";
sortMenu(2);
});
drinks.addEventListener("click", function () {
categoryBtn.textContent = "음료";
sortMenu(3);
});
//키워드 별
const keyword = document.getElementById("keyword");
const searchBtn = document.querySelector(".search-btn");
searchBtn.addEventListener("click", function () {
sortMenu(4, keyword.value);
});
다음 퀘스트로~
'프론트엔드 부트캠프' 카테고리의 다른 글
[사전캠프 4주차] Firebase (0) | 2024.09.19 |
---|---|
[사전캠프 퀘스트] TO DO LIST 만들기 (1) | 2024.09.19 |
[사전캠프 퀘스트] 숫자 기억 게임 만들기 (8) | 2024.09.12 |
[사전캠프 3주차] Fetch 여기저기 사용해보기 (0) | 2024.09.11 |
[사전캠프 3주차] 클라이언트-서버 개념 이해와 Fetch 시작하기 (3) | 2024.09.10 |