State와 Props를 이용해서 메뉴 검색기능 구현하기
May 24, 2023
메뉴판을 만들고, 식당 이름을 검색했을 때 검색 결과에 해당하는 식당 정보만 보여주는 기능을 구현하려고 했습니다.
요구사항 요구사항은 다음과 같습니다.
사용자는 식당 이름, 종류, 메뉴가 보이는 식당 목록을 볼 수 있다.
사용자는 식당 이름을 입력하여 이름이 (부분)일치하는 식당 목록을 골라 볼 수 있다.
사용자는 식당 종류 버튼을 눌러서 종류가 일치하는 식당 목록만 골라 볼 수 있다.
사용자는 입력한 식당 이름과 선택한 종류가 모두 일치하는 식당 목록만 골라 볼 수 있다.
구현해야할 것들을 생각해보면,
식당목록, 식당 카테고리
사용자가 검색창에 식당이름을 검색했을 때 → 이름이 일치하는 식당 목록만 보여주기
식당 카테고리를 클릭하면 일치하는 식당 목록만 보여주기
입력한 식당 이름과 선택한 카테고리가 모두 일치하는 식당 목록만 보여주기
이 정도가 될 것 같습니다. 👉🏻 결과물 미리보기
Restaurant.json 확인하기 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 { "restaurants" : [ { "id" : "1" , "category" : "중식" , "name" : "메가반점" , "menu" : [ {"id" : "1" , "name" : "짜장면" , "price" : 8000 }, {"id" : "2" , "name" : "짬뽕" , "price" : 8000 }, {"id" : "3" , "name" : "차돌짬뽕" , "price" : 9000 }, {"id" : "4" , "name" : "탕수육" , "price" : 14000 } ] }, { "id" : "2" , "category" : "한식" , "name" : "메리김밥" , "menu" : [ {"id" : "5" , "name" : "김밥" , "price" : 3500 }, {"id" : "6" , "name" : "참치김밥" , "price" : 4500 }, {"id" : "7" , "name" : "제육김밥" , "price" : 5000 }, {"id" : "8" , "name" : "훈제오리김밥" , "price" : 5500 }, {"id" : "9" , "name" : "컵라면" , "price" : 2000 } ] }, ... ] }
restaurants는 각 식당 객체를 요소로 가지고 있는 배열입니다.
즉, 하나의 식당 객체를 Restaurant
라고 하면 restaurants는 Restaurant[]
이 되는거죠!
Restaurant 타입 지정하기
1 2 3 4 5 6 7 8 9 10 11 { "id" : "1" , "category" : "중식" , "name" : "메가반점" , "menu" : [ {"id" : "1" , "name" : "짜장면" , "price" : 8000 }, {"id" : "2" , "name" : "짬뽕" , "price" : 8000 }, {"id" : "3" , "name" : "차돌짬뽕" , "price" : 9000 }, {"id" : "4" , "name" : "탕수육" , "price" : 14000 } ] },
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import Food from './Food' ;interface Restaurant { id : string , category : string , name : string , menu : Food [] } export default Restaurant ;===================================================== interface Food { id : string ; name : string ; price : number ; } export default Food ;
main.tsx main.tsx에서는 index.html에 있는 id=”root”
에 만든 컴포넌트를 렌더링해야 합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 import ReactDOM from 'react-dom/client'; import App from './App'; function main() { const element = document.getElementById('root'); if (element) { const root = ReactDOM.createRoot(element); root.render(<App />); } } main();
컴포넌트 구조 생각해보기
App.tsx 우선 데이터가 필요하니 restauants 데이터를 받아와서 전체적인 구조를 App.tsx에 만들어보도록 하겠습니다.
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 import { restaurants } from '../restaurants.json' ;const categories = [ '전체' , ...new Set (restaurants.map ((restaurant ) => restaurant.category )), ]; export default function App ( ) { return ( <div > <h1 > 오늘의 메뉴</h1 > <label htmlFor ="search" > 검색</label > <input id ="search" placeholder ="식당이름" /> <div > {categories.map((category) => ( <button key ={category} type ="button" > {category} </button > ))} </div > <table > <thead > <tr > <th > 식당이름</th > <th > 종류</th > <th > 메뉴</th > </tr > </thead > <tbody > {restaurants.map((restaurant) => { const { id, name, category, menu } = restaurant; return ( <tr key ={id} > <td > {name}</td > <td > {category}</td > <td > <ul > {menu.map((item) => ( <li key ={item.id} > {item.name}({item.price} 원) </li > ))} </ul > </td > </tr > ); })} </tbody > </table > </div > ); }
Divide Component 컴포넌트 분리하면서 기능 구현하기! 🚀
✅ SearchBar Component & RestaurantTable Component 크게 두 개의 컴포넌트로 나눌 수 있을 것 같은데요, 검색하는 기능이 있는 SearchBar, 식당 정보를 보여주는 RestaurantTable 컴포넌트 두 개로 나눠보겠습니다.
1 2 3 4 5 6 7 8 9 export default function App ( ) { return ( <div > <h1 > 오늘의 메뉴</h1 > <SearchBar /> <RestaurantTable /> </div > ); }
input창에 텍스트를 입력했을 때 그리고 카테고리 버튼을 눌렀을 때 정보에 맞게 식당이 바뀌어야하므로 텍스트에 대한 상태와 카테고리에 대한 상태는 App 컴포넌트가 가지고 있어야 할 것 같습니다.
1 2 3 4 import { useState } from 'react' ;const [filterText, setFilterText] = useState<string >('' );const [filterCategory, setFilterCategory] = useState<string >('전체' );
카테고리만 배열로 만드는 로직은 utils 폴더로 이동시키도록 하겠습니다.
1 2 3 4 5 6 7 import Restaurant from '../types/Restaurant' ;export default function selectCategories (restaurants: Restaurant[] ): string [] { return [...new Set (restaurants.map ((restaurant ) => restaurant.category ))]; }
SeachBar 컴포넌트에 넘겨줘야할 것들이 뭐가 있을까요? 🤔
카테고리 배열 categories
input에 입력한 텍스트와 텍스트 상태를 변경하는 함수 filterText
, setFilterText
카테고리 버튼을 누를 때 카테고리를 변경해야하므로 setFilterCategory
1 2 3 4 5 6 <SearchBar categories={categories} filterText={filterText} setFilterText={setFilterText} setFilterCategory={setFilterCategory} />
SearchBar Component
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 import React from 'react' ;type SearchBarProps = { categories : string []; filterText : string ; setFilterText : (text: string ) => void ; setFilterCategory : (text: string ) => void ; }; export default function SearchBar ({ categories, filterText, setFilterText, setFilterCategory, }: SearchBarProps ) { const handleChange = (event: React.ChangeEvent<HTMLInputElement> ) => { setFilterText (event.target .value ); }; const id = 'input-search' ; return ( <> <label htmlFor ={id} > 검색</label > <input id ={id} placeholder ="식당이름" type ="text" value ={filterText} onChange ={handleChange} /> <div > <ul > {['전체', ...categories].map((category: string) => ( <li key ={category} > <button type ="button" onClick ={() => setFilterCategory(category)}> {category} </button > </li > ))} </ul > </div > </> ); }
RestaurantTable Component
RestaurantTable 컴포넌트는 reataurants를 props로 받아서 렌더링합니다.
1 <RestaurantTable restaurants={restaurants} />
그런데 여기서 restaurants를 그대로 받는게 아니라 input값과 일치하는 필터링된 배열을 props로 받아야하는데요. 어떻게 기능을 구현해야할까요?
filterText를 받아서 filterText과 식당이름이 일치하는 배열을 반환한다.
배열은 filterCategory에 해당하는 배열로 비교해야한다.
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 32 33 34 35 36 37 import Restaurant from '../types/Restaurant' ;type filterRestaurantsProps = { filterText : string ; filterCategory : string ; }; export default function filterRestaurants ( restaurants: Restaurant[], { filterText, filterCategory }: filterRestaurantsProps ): Restaurant [] { const filteredRestaurants = filterCategory === '전체' ? restaurants : restaurants.filter ( (restaurant ) => restaurant.category === filterCategory ); const normalize = (text: string ) => text.trim ().toLowerCase (); const query = normalize (filterText); if (!query) { return filteredRestaurants; } const contains = (restaurant: Restaurant ) => normalize (restaurant.name ).includes (query); return filteredRestaurants.filter (contains); }
이제 E2E 테스트를 실행시켜보면?
모두 통과한것을 볼 수 있습니다.😎
3주차를 복습하면서 헷갈렸던 state와 props에 대해서 다시 공부할 수 있었고 utils 함수를 만드는 시기와 방법에 대해서도 이해할 수 있었습니다. 가장 헷갈렸던 카테고리가 달라진 배열에 input 값을 어떻게 필터링할까? 에 대한 문제를 이해할 수 있는 시간이었습니다. (역시 복습 매우 중요하다!)