npm installnpm run devimport fs from "node:fs/promises";
import bodyParser from "body-parser";
import express from "express";
const app = express();
app.use(express.static("images"));
app.use(bodyParser.json());
// CORS
app.use((req, res, next) => {
res.setHeader("Access-Control-Allow-Origin", "*"); // allow all domains
res.setHeader("Access-Control-Allow-Methods", "GET, PUT");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
next();
});
app.get("/places", async (req, res) => {
const fileContent = await fs.readFile("./data/places.json");
const placesData = JSON.parse(fileContent);
res.status(200).json({ places: placesData });
});
app.get("/user-places", async (req, res) => {
const fileContent = await fs.readFile("./data/user-places.json");
const places = JSON.parse(fileContent);
res.status(200).json({ places });
});
app.put("/user-places", async (req, res) => {
const places = req.body.places;
await fs.writeFile("./data/user-places.json", JSON.stringify(places));
res.status(200).json({ message: "User places updated!" });
});
// 404
app.use((req, res, next) => {
if (req.method === "OPTIONS") {
return next();
}
res.status(404).json({ message: "404 - Not Found" });
});
app.listen(3000);
🔗 HTTP 개요
🔗 GET 메서드
🔗 POST 메서드
npm run dev를 한 터미널 이외에 다른 터미널은 연 뒤 다음의 명령어 실행
cd ./backendnode app.js : 백엔드 서버 시작이렇게 실행 중이 두 프로세스 모두 백엔드에 연결해야 한다.
리액트 코드 내에서 HTTP 요청을 백엔드 코드로 보내서 데이터 목록을 가져올 것이다.
localStorage는 동기적으로 데이터를 바로 받아올 수 있다. 하지만 백엔드를 이렇게 데이터를 받을 수 없다.
인터넷을 통해서(HTTP 요청) 오기 때문에 약간의 시간차가 발생한다. → 동기적으로는 안되고 비동기적으로 처리해야한다.
// src/component/AvailablePlaces.jsx
import { useState } from "react";
import Places from "./Places.jsx";
const places = localStorage.getItem("places");
export default function AvailablePlaces({ onSelectPlace }) {
//const [availablePlaces, setAvailablePlaces] = useState(places); // 로컬 사용하는 경우
// 비동기적으로 사용한다 = 데이터를 가져오고 나서야 상태를 업데이트 하겠다. 라는 의미
const [availablePlaces, setAvailablePlaces] = useState([]);
return (
<Places
title="Available Places"
places={availablePlaces}
fallbackText="No places available."
onSelectPlace={onSelectPlace}
/>
);
}
// backend/app.js
app.get("/places", async (req, res) => {
const fileContent = await fs.readFile("./data/places.json");
const placesData = JSON.parse(fileContent);
res.status(200).json({ places: placesData });
});
// src/component/AvailablePlaces.jsx
import { useState } from "react";
import Places from "./Places.jsx";
export default function AvailablePlaces({ onSelectPlace }) {
const [availablePlaces, setAvailablePlaces] = useState([]);
// 1. then을 이용하는 방법 - await/async를 사용하는 방법도 있다.
fetch("http://localhost:3000/places")
.then((response) => {
return response.json(); // json 형식의 데이터를 뽑아온다. => 이 메서드는 또다른 프로미스를 반환
})
.then((resData) => {
setAvailablePlaces(resData.places);
}); // 브라우저 제공 : HTTP 요청을 다른 서버들로 보낸다.
// fetch를 프로미스를 반환. Promise가 response 객체를 감싼다.
return (
<Places
title="Available Places"
places={availablePlaces}
fallbackText="No places available."
onSelectPlace={onSelectPlace}
/>
);
}
// src/components/AvailablePlaces.jsx
import { useState, useEffect } from "react";
import Places from "./Places.jsx";
export default function AvailablePlaces({ onSelectPlace }) {
const [availablePlaces, setAvailablePlaces] = useState([]);
useEffect(() => {
fetch("http://localhost:3000/places")
.then((response) => {
return response.json();
})
.then((resData) => {
setAvailablePlaces(resData.places);
});
}, []); // 컴포넌트가 실행된 이후에 실행한다. 의존성은 비어있어야 한다. 딱 한번 처음에 실행된다.
return (
<Places
title="Available Places"
places={availablePlaces}
fallbackText="No places available."
onSelectPlace={onSelectPlace}
/>
);
}
// backend/data/places.json
[
{
"id": "p1",
"title": "Forest Waterfall",
"image": {
"src": "forest-waterfall.jpg",
"alt": "A tranquil forest with a cascading waterfall amidst greenery."
},
"lat": 44.5588,
"lon": -80.344
},...
]
// src/components/Places.jsx
export default function Places({ title, places, fallbackText, onSelectPlace }) {
console.log(places);
return (
<section className="places-category">
<h2>{title}</h2>
{places.length === 0 && <p className="fallback-text">{fallbackText}</p>}
{places.length > 0 && (
<ul className="places">
{places.map((place) => (
<li key={place.id} className="place-item">
<button onClick={() => onSelectPlace(place)}>
{/* src={place.img.src}에서 다음과 같이 변경 */}
<img
src={`http://localhost:3000/${place.image.src}`}
alt={place.image.alt}
/>
<h3>{place.title}</h3>
</button>
</li>
))}
</ul>
)}
</section>
);
}

async / await 사용하기import { useState, useEffect } from "react";
import Places from "./Places.jsx";
export default function AvailablePlaces({ onSelectPlace }) {
const [availablePlaces, setAvailablePlaces] = useState([]);
useEffect(() => {
async function fetchPlaces() {
const response = await fetch("http://localhost:3000/places");
const resData = await response.json();
setAvailablePlaces(resData.places);
}
fetchPlaces(); // 정의 후 생성한 함수 실행
}, []);
return (
<Places
title="Available Places"
places={availablePlaces}
fallbackText="No places available."
onSelectPlace={onSelectPlace}
/>
);
}
import { useState, useEffect } from "react";
import Places from "./Places.jsx";
export default function AvailablePlaces({ onSelectPlace }) {
const [availablePlaces, setAvailablePlaces] = useState([]);
const [isFetching, setIsFetching] = useState(false);
useEffect(() => {
setIsFetching(true);
async function fetchPlaces() {
const response = await fetch("http://localhost:3000/places");
const resData = await response.json();
setAvailablePlaces(resData.places);
setIsFetching(false); // 데이터를 다 받아온 경우
}
fetchPlaces(); // 정의 후 생성한 함수 실행
}, []);
return (
<Places
title="Available Places"
places={availablePlaces}
isLoading={isFetching}
loadingText="Fetching place data..."
fallbackText="No places available."
onSelectPlace={onSelectPlace}
/>
);
}
export default function Places({
title,
places,
fallbackText,
onSelectPlace,
isLoading,
loadingText,
}) {
console.log(places);
return (
<section className="places-category">
<h2>{title}</h2>
{/* 로딩과 관련된 조건 및 출력값 설정 */}
{isLoading && <p className="fallback-text">{loadingText}</p>}
{!isLoading && places.length === 0 && (
<p className="fallback-text">{fallbackText}</p>
)}
{!isLoading && places.length > 0 && (
<ul className="places">
{places.map((place) => (
<li key={place.id} className="place-item">
<button onClick={() => onSelectPlace(place)}>
<img
src={`http://localhost:3000/${place.image.src}`}
alt={place.image.alt}
/>
<h3>{place.title}</h3>
</button>
</li>
))}
</ul>
)}
</section>
);
}

// src/components/AvailablePlaces.jsx
import { useState, useEffect } from "react";
import Places from "./Places.jsx";
import Error from "./Error.jsx";
export default function AvailablePlaces({ onSelectPlace }) {
const [availablePlaces, setAvailablePlaces] = useState([]);
const [isFetching, setIsFetching] = useState(false);
const [error, setError] = useState();
useEffect(() => {
setIsFetching(true); // fetchPlaces안에 작성해도 됨
async function fetchPlaces() {
try {
const response = await fetch("http://localhost:3000/places");
const resData = await response.json();
if (!response.ok) {
// 성공적인 응답(200,300 응답코드)
// 실패 = 400, 500
throw new Error("Failded to fetch places"); // 이렇게 하면 앱 충돌
}
setAvailablePlaces(resData.places);
} catch (error) {
// 에러가 발생할 경우 실행해야할 코드 -> 앱 충돌을 막고 대신에 실행할 코드
// react에서 catch는 에러에 대한 UI 업데이트
setError({
message:
error.message || "Could not fetch places, plz try again later.",
});
}
setIsFetching(false); // 데이터를 다 받아온 경우 => 에러가 나든 안나든 로딩은 끝낼 거임<div className=""></div>
}
fetchPlaces();
}, []);
if (error) {
return <Error title="An error occurred!" message={error.message} />;
}
return (
<Places
title="Available Places"
places={availablePlaces}
isLoading={isFetching}
loadingText="Fetching place data..."
fallbackText="No places available."
onSelectPlace={onSelectPlace}
/>
);
}
// src/components/Error.jsx
export default function Error({ title, message, onConfirm }) {
return (
<div className="error">
<h2>{title}</h2>
<p>{message}</p>
{onConfirm && (
<div id="confirmation-actions">
<button onClick={onConfirm} className="button">
Okay
</button>
</div>
)}
</div>
);
}
try~catch문과 에러를 다루는 상태를 이용하여 에러가 발생했을 때의 UI를 리턴하도록 한다.
import { useState, useEffect } from "react";
import Places from "./Places.jsx";
import Error from "./Error.jsx";
import { sortPlacesByDistance } from "../loc.js";
export default function AvailablePlaces({ onSelectPlace }) {
const [availablePlaces, setAvailablePlaces] = useState([]);
const [isFetching, setIsFetching] = useState(false);
const [error, setError] = useState();
useEffect(() => {
setIsFetching(true);
async function fetchPlaces() {
try {
const response = await fetch("http://localhost:3000/places");
const resData = await response.json();
if (!response.ok) {
throw new Error("Failded to fetch places");
}
// 여기선 async, await을 사용하지 않고 콜백함수를 사용.
// setIsFetching 상태 업데이트 함수 위치를 변경해야한다. => 시간차로 인해서 이 상태 업데이트 함수가 더 일찍 실행될 수 있다.
navigator.geolocation.getCurrentPosition((position) => {
const sortedPlaces = sortPlacesByDistance(
resData.places,
position.coords.latitude,
position.coords.longitude
);
setAvailablePlaces(sortedPlaces);
setIsFetching(false); // 분류 후 표시가 끝난 뒤에 로딩 종료
});
} catch (error) {
setError({
message:
error.message || "Could not fetch places, plz try again later.",
});
setIsFetching(false); // 오류가 발생했다면 오류 상태 업데이트 후 로딩 종료
}
}
fetchPlaces(); /
}, []);
if (error) {
return <Error title="An error occurred!" message={error.message} />;
}
return (
<Places
title="Available Places"
places={availablePlaces}
isLoading={isFetching}
loadingText="Fetching place data..."
fallbackText="No places available."
onSelectPlace={onSelectPlace}
/>
);
}
setAvailablePlaces 상태 업데이트 함수 실행.navigator~는 콜백함수를 사용하기 때문에 setIsFetching(false)를 이전에 작성한 것처럼 두면 안된다.
export async function fetchAvailablePlaces() {
const response = await fetch("http://localhost:3000/places");
const resData = await response.json();
if (!response.ok) {
throw new Error("Failded to fetch places");
}
return resData.places;
}
import { useState, useEffect } from "react";
import Places from "./Places.jsx";
import Error from "./Error.jsx";
import { sortPlacesByDistance } from "../loc.js";
import { fetchAvailablePlaces } from "../http.js";
export default function AvailablePlaces({ onSelectPlace }) {
const [availablePlaces, setAvailablePlaces] = useState([]);
const [isFetching, setIsFetching] = useState(false);
const [error, setError] = useState();
useEffect(() => {
setIsFetching(true); // fetchPlaces안에 작성해도 됨
async function fetchPlaces() {
try {
const places = await fetchAvailablePlaces();
navigator.geolocation.getCurrentPosition((position) => {
const sortedPlaces = sortPlacesByDistance(
places,
position.coords.latitude,
position.coords.longitude
);
setAvailablePlaces(sortedPlaces);
setIsFetching(false);
});
} catch (error) {
setError({
message:
error.message || "Could not fetch places, plz try again later.",
});
setIsFetching(false);
}
}
fetchPlaces();
}, []);
if (error) {
return <Error title="An error occurred!" message={error.message} />;
}
return (
<Places
title="Available Places"
places={availablePlaces}
isLoading={isFetching}
loadingText="Fetching place data..."
fallbackText="No places available."
onSelectPlace={onSelectPlace}
/>
);
}
app.get("/user-places", async (req, res) => {
const fileContent = await fs.readFile("./data/user-places.json");
const places = JSON.parse(fileContent);
res.status(200).json({ places });
});
app.put("/user-places", async (req, res) => {
const places = req.body.places;
await fs.writeFile("./data/user-places.json", JSON.stringify(places));
res.status(200).json({ message: "User places updated!" });
});
/user-places endpoint에서 선택한 장소를 저장.export async function updateUserPlaces(places) {
const response = await fetch("http://localhost:3000/user-places", {
method: "PUT",
body: JSON.stringify({ places }),
headers: {
"Content-Type": "application/json", // 이 요청에 첨부될 데이터가 JSON 형식이다. -> 이렇게해야 성공적으로 백엔드에 추출
},
});
const resData = await response.json();
if (!response.of) {
throw new Error("Failed to update user data.");
}
return resData.message; // 백엔드에서 put메서드에 res.status(200).json({message:'User places updated!'})라고 했기 때문
}
body: JSON.stringify({ places }){places : places}로 해야되지만 단축키로 {places}로만 전달해도 됨async function handleSelectPlace(selectedPlace) {
setUserPlaces((prevPickedPlaces) => {
if (!prevPickedPlaces) {
prevPickedPlaces = [];
}
if (prevPickedPlaces.some((place) => place.id === selectedPlace.id)) {
return prevPickedPlaces;
}
return [selectedPlace, ...prevPickedPlaces];
});
try {
await updateUserPlaces([selectedPlace, ...userPlaces]); // 아직 상태 업데이트가 반영이 안될테니 선택한 장소와 이전 상태의 장소들을 전달.
} catch (err) {
setUserPlaces(userPlaces); // 만약 POST 요청으로 PUT에 실패했다면 단순히 이전 상태로 돌아감.
}
}
updateUserPlaces 요청을 보내기 전에 local 상태를 업데이트 했다. (setUserPlaces -> updateUserPlaces) → 낙관적 업데이트(optimistic updating)
import { useRef, useState, useCallback } from "react";
import Error from "./components/Error.jsx";
function App() {
const [errorUpdatingPlaces, setErrorUpdatingPlaces] = useState();
async function handleSelectPlace(selectedPlace) {
setUserPlaces((prevPickedPlaces) => {
if (!prevPickedPlaces) {
prevPickedPlaces = [];
}
if (prevPickedPlaces.some((place) => place.id === selectedPlace.id)) {
return prevPickedPlaces;
}
return [selectedPlace, ...prevPickedPlaces];
});
try {
await updateUserPlaces([selectedPlace, ...userPlaces]); // 아직 상태 업데이트가 반영이 안될테니 선택한 장소와 이전 상태의 장소들을 전달.
} catch (err) {
setUserPlaces(userPlaces); // 만약 POST 요청으로 PUT에 실패했다면 단순히 이전 상태로 돌아감.
setErrorUpdatingPlaces({
message: err.message || "Failed to update places.",
});
}
}
function handleError() {
setErrorUpdatingPlaces(null);
}
return (
<>
<Modal open={errorUpdatingPlaces} onClose={handleError}>
{errorUpdatingPlaces && (
<Error
title="An error occurred!"
message={errorUpdatingPlaces.message}
onConfirm={handleError}
/>
)}
</Modal>
...
</>
);
}
export default App;

import { useRef, useState, useCallback } from "react";
import Error from "./components/Error.jsx";
import { updateUserPlaces } from "./http.js";
function App() {
const selectedPlace = useRef();
const [userPlaces, setUserPlaces] = useState([]);
const [errorUpdatingPlaces, setErrorUpdatingPlaces] = useState();
const [modalIsOpen, setModalIsOpen] = useState(false);
async function handleSelectPlace(selectedPlace) {
setUserPlaces((prevPickedPlaces) => {
if (!prevPickedPlaces) {
prevPickedPlaces = [];
}
if (prevPickedPlaces.some((place) => place.id === selectedPlace.id)) {
return prevPickedPlaces;
}
return [selectedPlace, ...prevPickedPlaces];
});
try {
await updateUserPlaces([selectedPlace, ...userPlaces]); // 아직 상태 업데이트가 반영이 안될테니 선택한 장소와 이전 상태의 장소들을 전달.
} catch (err) {
console.log(err);
setUserPlaces(userPlaces); // 만약 POST 요청으로 PUT에 실패했다면 단순히 이전 상태로 돌아감.
setErrorUpdatingPlaces({
message: err.message || "Failed to update places.",
});
}
}
const handleRemovePlace = useCallback(
async function handleRemovePlace() {
setUserPlaces((prevPickedPlaces) =>
prevPickedPlaces.filter(
(place) => place.id !== selectedPlace.current.id
)
);
try {
await updateUserPlaces(
userPlaces.filter((place) => place.id !== selectedPlace.current.id)
);
} catch (err) {
setUserPlaces(userPlaces); // 이전 상태로 되돌아감
setErrorUpdatingPlaces({
message: err.message || "Faild to delete place.",
});
}
setModalIsOpen(false);
},
[userPlaces]
);
function handleError() {
setErrorUpdatingPlaces(null);
}
return (
<>
<Modal open={errorUpdatingPlaces} onClose={handleError}>
{errorUpdatingPlaces && (
<Error
title="An error occurred!"
message={errorUpdatingPlaces.message}
onConfirm={handleError}
/>
)}
</Modal>
<Modal open={modalIsOpen} onClose={handleStopRemovePlace}>
<DeleteConfirmation
onCancel={handleStopRemovePlace}
onConfirm={handleRemovePlace}
/>
</Modal>
...
</>
);
}
export default App;

export async function fetchUserPlaces() {
const response = await fetch("http://localhost:3000/user-places");
const resData = await response.json();
if (!response.ok) {
throw new Error("Failded to fetch user places");
}
return resData.places;
}
import { useRef, useState, useCallback, useEffect } from "react";
import { updateUserPlaces, fetchUserPlaces } from "./http.js";
function App() {
const selectedPlace = useRef();
const [userPlaces, setUserPlaces] = useState([]);
const [isFetching, setIsFetching] = useState(false);
const [error, setError] = useState();
const [errorUpdatingPlaces, setErrorUpdatingPlaces] = useState();
useEffect(() => {
async function fetchPlaces() {
setIsFetching(true);
try {
const places = await fetchUserPlaces();
setUserPlaces(places);
} catch (err) {
setError({ message: err.message || "Failed to fetch user places." });
}
setIsFetching(false);
}
fetchPlaces();
}, []);
return (
<>
...
<main>
{error && <Error title="An error occurred" message={error.message} />}
{!error && (
<Places
title="I'd like to visit ..."
fallbackText="Select the places you would like to visit below."
isLoading={isFetching}
loadingText="Fetching your places..."
places={userPlaces}
onSelectPlace={handleStartRemovePlace}
/>
)}
</main>
</>
);
}
export default App;
