나만의 맛집 페이지를 구현하는 프로젝트
React와 Node.js 서버를 사용하여 제공된 와이어프레임 기반으로 페이지 구조를 설계하고, 서버를 세팅하여 API를 통해 전체 맛집 데이터를 화면에 출력합니다. 또한, .gitignore를 사용하여 불필요한 파일이 Git에 업로드되지 않도록 합니다.
EATINGMARK-FE
├── src/
│ ├── components/
│ ├── pages/
│ ├── context/
│ ├── api/
│ ├── styles/
│ └── App.jsx
└── public/

💡 [ 리액트 프로젝트 생성 ]
npm create vite@latest
💡 [ 기본 라이브러리 세팅 ]
npm i
npm i react-router
npm i axios
📁 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>,
)
npm install
node app.js
api 폴더 생성 → 📁 axios.js / placeApi.js 파일 2개 생성axios.js : axios 인스턴스 생성placeApi.js : axios 인스턴스를 이용한 데이터 가져오기📁 .env
VITE_BASE_URL = 'http://localhost:3000'
📁 .gitignore
# Logs
~~~
# Editor directories and files
~~~
.env
📁 axios.js
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
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)📁 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
📁 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>
</>
)
}
.env 파일로 설정했던 BASE_URL 가져오기📁 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;
}