Notice
Recent Posts
Recent Comments
Link
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Archives
Today
Total
관리 메뉴

heyday2024 님의 블로그

[사전캠프 퀘스트] 한식 메뉴 렌더링하기 본문

프론트엔드 부트캠프

[사전캠프 퀘스트] 한식 메뉴 렌더링하기

heyday2024 2024. 9. 14. 16:42

https://koreanfood-menu.netlify.app/

 

한식 메뉴 렌더링하기

 

koreanfood-menu.netlify.app

 

한식 메뉴를 렌더링 해보기

주어진 데이터를 가지고, 데이터를 화면에 표시했다. 메뉴판 느낌으로 스타일링하고, 각 객체는 각각의 가격, 카테고리, 설명을 포함하고 있다. 카테고리별로 메뉴를 볼 수있고, 검색 기능을 사용해서, 입력한 키워드를 포함하는 요소를 보여줄 수 있다. 

한식 메뉴 퀘스트

특징

  • 각 메뉴를 카테고리 별대로 볼 수 있도록 하는 기능을 추가했다. (드롭다운 메뉴)
  • 음식을 bootstrap에서 가져온 cards 코드를 이용해 정갈하게 나열했다.
  • filter과 includes 메소드를 이용해서 특정 키워드를 사용자가 입력한 후 검색버튼을 누르면 검색어를 포함한 메뉴가 보이도록 하는 기능을 추가했다.

요소 추가 기능 방법

  1. createElement와 appendChild를 사용: 각 요소를 개별적으로 생성하고, 이를 부모 요소에 하나씩 추가할 수 있음.
    • 여러 요소를 한 번에 추가하는 경우 appendchild를 여러 번 호출하는 것이 innerHTML보다 성능이 좋을 수 있음.
    • 하지만, 코드가 다소 장황할 수 있고, 요소를 하나씩 생성하고, 추가해야함으로 코드가 길어질 수 있음.
  2. innerHTML 사용: 전체 HTML 구조를 문자열로 생성하여 한번에 삽입 (내가 사용한 방법)
    • HTML 구조를 한 번에 문자열로 작성하기 떄문에 간결함.
    • 하지만 사용자 입력을 포함할 때 XSS 공격에 취약할 수 있음(보안문제), innerHTML을 사용하면 기존의 모든 자식 요소가 제거되고, 새로운 내용이 삽입됨.(굳이 코드를 reset시킬 필요가 없는 것을 원하면 장점이 됨. 이 이유로 innerHTML 사용했음.)
      • XSS(Cross-Site Scripting) 공격: 익의적인 사용자가 웹 애플리케이션에 스크립트를 삽입하여 다른 사용자에게 피해를 주는 공격. 입력처리 시 악성 스크립트를 실행시키는 방법.
  3. 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 사용해서 요소 추가해줌.

추가적인 내용

  1. 요소의 ID를 사용해서 js에서 그 요소 지정할 때, getElementByID와 querySelector 차이
    •  querySelector("#menu-list"): CSS 선택자를 사용하기 때문에 ID 앞에 #을 붙여야함.
    • querySelector는 더 복잡한 CSS 선택자를 처리할 수 있지만, 선택자가 복잡할수록 속도가 느릴 수 있음. 단일 ID 선택을 한다면 getElementById보다 성능이 떨어짐.

2. flex 관련 내용 - 요소 한 줄에 특정 개수만큼만 보이게 하고 싶을 때:

  1. flex-basis 사용: flex 아이템의 기본 크기 설정.(flex-direction이 row일 떄는 너비, column일 때는 높이). 각종 단위 수(px, % 등) 들어갈 수 있음.
    • 예를 들어, 한 줄에 3개씩 배치하고 싶다면 각 요소의 너비를 33.33%로 설정하여 한 줄에 3개가 들어가도록 만들 수 있음.
  2. 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);
});

 

다음 퀘스트로~