[React] 맛집 리스트 PJ - (1)

박하늘·2025년 4월 15일

Project

나만의 맛집 페이지를 구현하는 프로젝트

📍 1단계

React와 Node.js 서버를 사용하여 제공된 와이어프레임 기반으로 페이지 구조를 설계하고, 서버를 세팅하여 API를 통해 전체 맛집 데이터를 화면에 출력합니다. 또한, .gitignore를 사용하여 불필요한 파일이 Git에 업로드되지 않도록 합니다.

프로젝트 구조

EATINGMARK-FE
├── src/
│   ├── components/
│   ├── pages/
│   ├── context/
│   ├── api/
│   ├── styles/
│   └── App.jsx
└── public/




1️⃣ 리액트 프로젝트 생성 및 기본 라이브러리 세팅

💡 [ 리액트 프로젝트 생성 ]

npm create vite@latest


💡 [ 기본 라이브러리 세팅 ]

npm i
npm i react-router
npm i axios

2️⃣ 기본 페이지 링크 연결

  • 기본 페이지 링크 연결 및 엔드포인트 경로도 확인 해주기
  • BrowserRouter 로 App 감싸기

📁 App.jsx

import { Link, Route, Routes } from "react-router"
import { Main } from "./pages/Main"
import { Detail } from "./pages/Detail"
import { Favorite } from "./pages/Favorite"

function App() {


  return (
    <>
      <nav>
        <Link to={'/'}> Main </Link>
        <Link to={'/detail'}> Detail </Link>
        <Link to={'/favorite'}> Favorite </Link>
      </nav>
      <main>
        <Routes>
          <Route path={"/"} element={<Main />}/>
          <Route path={"/detail/:placeId"} element={<Detail />}/>
          <Route path={"/favorite"} element={<Favorite />}/>
        </Routes>
      </main>
    </>
  )
}

export default App

📁 main.jsx

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
import { BrowserRouter } from 'react-router'

createRoot(document.getElementById('root')).render(
  <BrowserRouter>
  <StrictMode>
    <App />
  </StrictMode>
  </BrowserRouter>,
)

3️⃣ 로컬 서버 clone / 실행

npm install
node app.js

4️⃣ axios로 백엔드 서버 불러오기

  • .env 파일 생성하여 BASE_URL 환경 변수 설정 → gitignore 파일에 추가
  • api 폴더 생성 → 📁 axios.js / placeApi.js 파일 2개 생성
  • 📁 axios.js : axios 인스턴스 생성
  • 📁 placeApi.js : axios 인스턴스를 이용한 데이터 가져오기

📁 .env

  • 기본 서버 주소를 BASE_URL 환경 변수 설정
  • vite 사용 시 변수명 맨 앞에 VITE_ 넣어주기
VITE_BASE_URL = 'http://localhost:3000'

📁 .gitignore

  • .env 파일이 Github 에 올라가지 않게 파일 정의 (보안)
# Logs
~~~

# Editor directories and files
~~~
.env

📁 axios.js

  • axios 인스턴스 생성
import axios from "axios";

const api = axios.create({
  
    // 모든 요청의 기본 URL을 설정
    baseURL: 'http://localhost:3000',
  
    headers: {
		// 이 요청의 본문(body)에 담긴 데이터 형식이 JSON이라는 걸 서버에 알려주는 것
        'Content-Type' : 'application/json'
    }
})

export default api;

📁 placeApi.js

  • axios 인스턴스를 이용한 데이터 가져오기

import api from './axios';


// 전체 맛집 목록
export const getPlaces = async () => {
  try {
    const response = await api.get('/places');
    return response.data.places;
  } catch (error) {
    console.error(" Loading Fail ... ", error)
    return []
  }
};

// 찜한 맛집 목록
export const getUserPlaces = async () => {
  try {
    const response = await api.get('/users/places');
  return response.data.places;
  } catch (error) {
    console.error(" Loading Fail ... ", error)
    return []
  }
}
  • 📁 axios.js 에서 변수로 설정한 api 를 이용하여 데이터를 가져옴 (get)
  • async 를 사용한 비동기 함수로 받아야 하므로 await 사용하여 데이터 받아오기

5️⃣ API 데이터 사용하기

📁 App.jsx

import { Link, Route, Routes, useNavigate } from "react-router"
import { Main } from "./pages/Main"
import { Detail } from "./pages/Detail"
import { Favorite } from "./pages/Favorite"
import styles from "./styles/app.module.scss"
import { useEffect, useState } from "react"
import { getPlaces, getUserPlaces } from "./api/placeApi"
import { Error404, Loading } from "./components/LoadingError"



function App() {

  const navigate = useNavigate()

  const [places, setPlaces] = useState([])
  const [fav, setFav] = useState([])

  
      useEffect(()=> {
          const fetchData = async () => {
            
              // await 를 사용하여 api 데이터 가져온 함수 호출하기
              const allPlace = await getPlaces();
              const favPlace = await getUserPlaces()
              
              // 함수 호출하여 받은 데이터를 상태 변경 함수로 넣어주기
              setPlaces(allPlace)
              setFav(favPlace)
            }
          }
          fetchData()
      },[])

  return (
    <>

    <>
      <h1 className={styles.h1}
        onClick={() => navigate('/') }
      > Eating Mark ! 🍓 </h1>

        <nav className={styles.nav}>
          <Link to={'/'}> Main </Link>
          <Link to={'/detail'}> Detail </Link>
          <Link to={'/favorite'}> Favorite </Link>
        </nav>

        <main className={styles.main}>
          <Routes>
            {/* 데이터 props로 넘겨주기 */}
            <Route path={"/"} element={<Main places={places}/>}/>
            <Route path={"/detail/:placeId"} element={<Detail />}/>
            <Route path={"/favorite"} element={<Favorite fav={fav}/>}/>
            <Route path={"*"} element={<Error404 />}/>
          </Routes>
        </main>
    </>
  )
}

export default App
  • 전체 장소 (places) , 찜 누른 장소 (fav) 를 관리하는 상태 변경 함수 만들기 - useState
  • 받아온 데이터 필요한 컴포넌트로 props 해주기

6️⃣ 전체 목록 보여주기

📁 Main.jsx


import { useEffect, useState } from "react"
import { Card } from "../components/Card"
import { sortPlacesByDistance } from "../components/loc"
import styles from "../styles/main.module.scss"
import { Favorite } from "./Favorite"

export function Main ({places}) {
    

    return(
        <div className={styles.firdiv}>
            <h1 className={styles.h1}> All Place ... </h1>
            <div className={styles.secdiv}>
                {places
                .map((place) => <Card key={place.id} place={place}/>)}
            </div>
        </div>
    )
}
  • Card 컴포넌트에 places 데이터 넘겨주기

📁 Card.jsx

  • 카드 각각을 컴포넌트로 구성
import { useNavigate } from "react-router"

export const Card = ({place}) => {
    const navigate = useNavigate()
    const BASE_URL = import.meta.env.VITE_BASE_URL;
    
    return(
        <>
        <section onClick={() => navigate(`/detail/${place.id}`)}>
            <img src={`${BASE_URL}/${place.image.src}`} alt={place.title}/>
            <div> {place.title} </div>
        </section>
        </>
    )
}
  • img src.env 파일로 설정했던 BASE_URL 가져오기

7️⃣ 스타일 수정

📁 index.css

@import "tailwindcss";

#root {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
  }
  
  html, body {
    min-height: 100vh;
    margin: 0;
    background-color: #000000 ;
    color: white;
  }

📁 app.module.scss

.h1{
    padding: 20px;
    font-size: 40px;
    text-align: center;
    font-weight: bold;
}
.nav{
    display: flex;
    justify-content: center;
    gap: 10px;
}


.main{
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    margin-top: 30px;
    padding: 30px;
    padding-top: 20px;
    gap: 10px;
}

📁 card.module.scss

.section{
  width: 160px;
  border: 1px solid #ccc;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 12px;
  border-radius: 12px;
  margin: 10px;
  cursor: pointer;
  transition: all 0.2s ease-in-out;

  &:hover {
    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
    transform: scale(1.03);
  }

  img {
    width: 120px;
    height: 80px;
    object-fit: cover;
    border-radius: 8px;
  }

  .title {
    font-size: 1rem;
    font-weight: 600;
    text-align: center;
  }
}

📁 main.module.scss

.firdiv{
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
}
.h1{
    padding: 15px;
    margin-top: 10px;
    font-size: 30px;
    font-weight: bold;
    background-image: linear-gradient(to right, rgb(255, 255, 255), rgb(79, 79, 79));
    color: transparent;
    background-clip: text;
  -webkit-background-clip: text;
}
.secdiv{
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    width: 80%;
    padding: 20px;
    gap: 10px;
}

0개의 댓글