Get a full fake REST API with zero coding in less than 30 seconds (seriously) ๊ณต์๋ฌธ์
json-server๋ ์ง์ DB๋ฅผ ๋ง๋ค๊ณ ์๋ฒ๋ฅผ ๊ตฌ์ถํ ํ์ ์์ด json ํ์ผ์ ์ด์ฉํ์ฌ REST API ์๋ฒ๋ฅผ ๊ตฌ์ถํด์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ค.
REST API์ ๊ธฐ๋ณธ์ ์ธ ์ฑ๋ฅ์ ์ ๋ถ ๊ฐ์ถ๊ณ ์์ผ๋ ์ค์ ์ฑ์ ์ฌ์ฉ๋๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ์๋๊ธฐ ๋๋ฌธ์ ์ค์ ์ฑ์ json-server ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ด์ฉํด ์๋ฒ๋ฅผ ๊ตฌ์ถํ์ง๋ ์๋๋ค๊ณ ํจ.
json-server๋ ์ฑ์ ํ๋กํ ํ์
์ ๋ง๋ค๊ฑฐ๋ ๊ณต๋ถ๋ฅผ ์ํด ์๋ฒ๊ฐ ํ์ํ ๋ ์ฌ์ฉํ๋ ์ฉ๋๋ก ์ด์ฉํด์ผ ํ๋ค.
//ํฐ๋ฏธ๋์์ ์ ์ญ ์ค์น
npm i -g json-server
//db.json ๋ฐ์ดํฐ ์์
{
"posts": [
{ "id": 1, "title": "json-server", "author": "typicode" }
],
"comments": [
{ "id": 1, "body": "some comment", "postId": 1 }
],
"profile": { "name": "typicode" }
}
//๊ณผ์ data.json์ ์ด๋ฐ ๋ธ๋ก๊ทธ ๊ฒ์๊ธ ๋ฐ์ดํฐ ํํ์์.
{
"blogs": [
{
"id": 2,
"title": "๋ฐํด์ปค์ ์ถ",
"author": "๋ฐํด์ปค",
"body": "์ค๋๋ ๋ฐํด์ปค๋ ์ด์ฌํ ์ฝ๋ฉ์ ํ๋ค",
"likes": 26
},
{
"id": 3,
"title": "์๋
ํ์ธ์?",
"body": "๋ฐ๊ฐ์ต๋๋ค",
"author": "๊น์ฝ๋ฉ",
"likes": 0
}
]
}
๊ณผ์ ์์๋ data.json ์ด ์ด๋ฏธ ์์ฑ๋์ด ์์์ง๋ง, ๋ง์ฝ ํผ์ ๋ง๋ค์ด ์ฌ์ฉํ ๋๋ ์ง์ ์์ฑํด์ค๋ค.
json-server --watch db.json
๊ณผ์ ์์๋ โport 3001์ด๋ผ๋ ์ต์ ์ ๋ถ์ฌ์ฃผ์๋๋ฐ, ์ต์ ์ด ์์ผ๋ฉด json-server๋ ์ ์ ๋ก 3000๋ฒ ํฌํธ๋ฅผ ์ ์ ํ๊ณ ์๋ฒ๋ฅผ ์ฐ๋ค.
์๋ฒ๊ฐ ๋ฌธ์ ์์ด ์ด๋ฆฌ๋ฉด ํฐ๋ฏธ๋์๋ ์๋์ ๊ฐ์ ํ๋ฉด์ด ๋ฌ๋ค ~~
localhost:3001 ๋ก ์ ์ํด๋ณด๋ฉด
ํฌ์คํธ๋งจ์ Workspaces์์ HTTP request๋ฅผ ์๋ก์ด ์์ฑํ์ฌ, json-server๊ฐ ๋ง๋ค์ด์ค API์ GET ์์ฒญ์ ๋ณด๋ด๋ณด๋ฉด, json ํ์ผ์ ๋ค์ด์๋ ๋ด์ฉ ๊ทธ๋๋ก ์๋ต์ ํ๊ณ ์์์ ์ ์ ์๋ค. (์๋ฒ๊ฐ ์ ๋ง๋ค์ด์ก๊ตฌ๋๐คญ)
๊ทธ๋ฅ lazy()๋ฅผ ์ฌ์ฉํ ์๋ ์๊ณ , React.lazy()๋ฅผ ์ฌ์ฉํ ์๋ ์๋ค.
lazy๋ฅผ import ํด์ ์ฌ์ฉํ๋๋, React ๋ฅผ import ํด์ ์ฌ์ฉํ๋๋์ ์ฐจ์ด
์๋๋ App.js ์ต์๋จ์ import๋ฌธ์ผ๋ก ๋ถ๋ฌ์์ฃผ์๋ Home, CreateBlog, BlogDetails, NotFound ๋ฑ์ ํ์ด์ง๋ค์ ์๋์ ๊ฐ์ด lazy๋ฅผ ์ฌ์ฉํด import ํด์ฃผ์๋ค.
import { Suspense, lazy } from "react";
const Home = lazy(() => import("./Home"));
const CreateBlog = lazy(() => import("./blogComponent/CreateBlog"));
const BlogDetails = lazy(() => import("./blogComponent/BlogDetail"));
const NotFound = lazy(() => import("./component/NotFound"));
//๊ณผ์ ์์๋ ํ์ด์ง์ ํด๋นํ๋ ๊ฒ๋ค์๋ง lazy๋ฅผ ์ ์ฉํ๋๋ฐ,
//๋ํผ๋ฐ์ค์์๋ ๋ชจ๋ ์ปดํฌ๋ํธ๋ค์ lazy๋ฅผ ์ ์ฉํด์ค.
//์๋๊ฐ ๋ํผ๋ฐ์ค ์ฝ๋
import React, { Suspense } from 'react';
const Home = React.lazy(() => import("./Home"));
const Navbar = React.lazy(() => import('./component/Navbar'));
const CreateBlog = React.lazy(() => import('./blogComponent/CreateBlog'));
const BlogDetails = React.lazy(() => import('./blogComponent/BlogDetail'));
const NotFound = React.lazy(() => import('./component/NotFound'));
const Footer = React.lazy(() => import('./component/Footer'));
const Loading = React.lazy(() => import('./component/Loading'));
๋ก๋ฉ ์ปดํฌ๋ํธ๊ฐ ์ด๋ฏธ ๊ตฌํ์ด ๋์ด ์์๊ธฐ ๋๋ฌธ์ ๋ก๋ฉ ์ปดํฌ๋ํธ๋ฅผ Suspense์ fallback์ผ๋ก ์ง์ ํด์ค ๊ฒ์ด๋ค.
์ง์ฐ ์ ๋ก๋ฉ ํ๋ฉด์ ๋ด์ผ๋ฉด ํ๋ ๊ณณ์ Suspense๋ก ๋ฌถ์ด์ค๋ค.
//๋ง์ฐฌ๊ฐ์ง๋ก ๋ํผ๋ฐ์ค ์ฝ๋์์๋ Navbar, Footer๊น์ง ํฌํจํด์ฃผ๊ณ ์์ง๋ง
//๋์ ํ์ด๋ ํ์ด์ง์ ๋ํด์๋ง Suspense๋ฅผ ์ ์ฉ์์ผฐ๋ค.
return (
<BrowserRouter>
{error && <div>{error}</div>}
<div className="app">
<Navbar />
<div className="content">
<Suspense fallback={<Loading />}>
<Routes>
<Route
exact
path="/"
element={<Home blogs={blogs} isPending={isPending} />}
/>
<Route path="/create" element={<CreateBlog />} />
<Route
path="/blogs/:id"
element={<BlogDetails blogs={blogs} />}
/>
<Route path="/blogs/:id" element={<NotFound />} />
</Routes>
</Suspense>
</div>
<Footer />
</div>
</BrowserRouter>
);
useParams๋ ๋ฆฌ์กํธ์์ ๋ผ์ฐํฐ๋ฅผ ์ฌ์ฉํ ๋ ํ๋ผ๋ฏธํฐ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ ํ์ฉํ๊ณ ์ถ์ ๋ ์ฌ์ฉํ ์ ์๋ Hook์ด๋ค. (ํ๋ผ๋ฏธํฐ๊ฐ ์๋ Pathname์ ๊ฐ์ ธ์ค๊ณ ์ถ์ ๋ ์ฌ์ฉํ๋ useLocation์ด๋ผ๋ Hook๋ ์๋ค.)
BlogDetail์ด๋ผ๋ ๋ธ๋ก๊ทธ ์ปดํฌ๋ํธ๋ ๋ฉ์ธ ํ๋ฉด์์ ๋ธ๋ก๊ทธ ๊ฒ์๊ธ์ ํด๋ฆญํ์ ๋ ๋ธ๋ก๊ทธ ์์ธํ๋ฉด์ ๋์ฐ๋ ์ปดํฌ๋ํธ์ธ๋ฐ, ์ด ์์ธํ๋ฉด์ ๊ตฌํํ ๋ fetch๋ก ํด๋น ๊ฒ์๊ธ์ ํด๋นํ๋ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์์ผํ๋ค. App.js์์ ๋ผ์ฐํฐ๋ฅผ ์ฌ์ฉํด์ค ๋, ์์ธ ํ๋ฉด์ผ๋ก ๊ฐ๋ path๋ฅผ "/blogs/:id"๋ผ๊ณ ์ง์ ํด์ฃผ์๊ธฐ ๋๋ฌธ์ fetch์๋ ํด๋น ๊ฒ์๊ธ์ id๊ฐ ํ๋ผ๋ฏธํฐ๋ก ํฌํจ๋ Url์ ์ ๋ฌํด์ฃผ์ด์ผ ํ๋ค. ์ด๋ id ๊ฐ์ ๊ฐ์ ธ์ฌ ์ ์๋๋ก ํ๋ ๊ฒ์ด useParams์ด๋ค.
const BlogDetails = ()=>{
let { id } = useParams();
//์ค๊ฐ์๋ต
useEffect(() => {
setTimeout(() => {
fetch(`http://localhost:3001/blogs/${id}`)
//ํ
ํ๋ฆฟ ๋ฆฌํฐ๋ด์ ์ฌ์ฉํด์ id ๊ฐ์ url์ ํฌํจ์์ผ์ค
.then((res) => {
if (!res.ok) {
throw Error("could not fetch the data for that resource");
}
return res.json();
})
.then((data) => {
setIsPending(false);
setBlogs(data);
console.log(data);
setError(null);
})
.catch((err) => {
setIsPending(false);
setError(err.message);
});
}, 1000);
}, []);
//์๋ต
}
์ด๋ ๊ฒ useParams๋ฅผ ์ด์ฉํด ๋ฐ์ id๋ fetch๋ก ๊ฒ์๊ธ์๋ํ Delete ์์ฒญ์ ๋ณด๋ผ๋๋ patch ์์ฒญ์ ๋ณด๋ผ ๋๋ ์ฌ์ฉํ ์ ์๋ค.
๋ธ๋ก๊ทธ ์์ธํ๋ฉด์ ์๋ ํํธ ๋ฒํผ์ ๋๋ ์ ๋ ์์ธํ๋ฉด์์ ํํธ ์์ด ๋ฐ๋๊ณ , ์ค์ ๋ก ๋ฉ์ธ ํ๋ฉด์์๋ ํํธ ์๊ฐ ํ์๋๋ ๋ถ๋ถ์ ๋ณํ๊ฐ ์ผ์ด๋๋๋ก ํ๊ธฐ ์ํด์ fetch ์์ฒญ์ ๋ณด๋ด์ค ๋ PATCH๋ก ๋ณด๋ด์ค ์ ์์๋๋ฐ PUT์ผ๋ก ๋ณด๋ด์ฃผ์๋ค.
PUT์ ๋ฆฌ์์ค์ ๋ชจ๋ ๊ฒ์ ์ ๋ฐ์ดํธ ํ๊ธฐ ๋๋ฌธ์ ๋ํผ๋ฐ์ค ์ฝ๋์ ๋น๊ตํด๋ณด๋... ์์ฑํ ๊ธธ์ด์ ๊ฝค ์ฐจ์ด๊ฐ ๋ฌ๋ค. ๊ธธ์ด๊ฐ ์ ๋ถ๋ ์๋๊ฒ ์ง๋ง, ๊ทธ๋๋ ํ์ด๊ฐ ์ด์ผ๊ธฐํ ๋ ๋ด๊ฐ PATCH๋ฅผ ์ฐ๋ ๊ฑด ์ด๋จ๊น์~ ์ด์ผ๊ธฐํด๋ณผ ์ ์์๋ค๋ฉด ์ข์์ ๊ฑฐ ๊ฐ๋จ ์๊ฐ์ด ๋ ๋ค.
//patch ์ฌ์ฉํ ๋ํผ๋ฐ์ค ์ฝ๋
const handleLikeClick = () => {
/* ํํธ๋ฅผ ๋๋ฅด๋ฉด home์์ ์๋ก๊ณ ์นจ์ ํ์ ๋ ์ซ์๊ฐ ์ฌ๋ผ๊ฐ์ผ ํฉ๋๋ค. */
/* isLike์ blog.likes๋ฅผ ์ด์ฉํ์ฌ handleLikeClick์ ๋ก์ง์ ์์ฑํด์ฃผ์ธ์. */
setIsLike(!isLike);
let patchData = {"likes" : blog.likes + 1};
fetchPatch('http://localhost:3001/blogs/', id, patchData);
}
//Put์ ์ฌ์ฉํ ๋ด ์ฝ๋
const handleLikeClick = () => {
setIsLike(!isLike);
let boll = !isLike;
let likeData = blog.likes;
boll ? likeData++ : likeData--;
const putData = {
id: blog.id,
title: blog.title,
author: blog.author,
body: blog.body,
likes: likeData,
};
fetch(`http://localhost:3001/blogs/${id}`, {
method: "PUT",
body: JSON.stringify(putData),
headers: {
"Content-Type": "application/json",
},
});
/* ํํธ๋ฅผ ๋๋ฅด๋ฉด home์์ ์๋ก๊ณ ์นจ์ ํ์ ๋ ์ซ์๊ฐ ์ฌ๋ผ๊ฐ์ผ ํฉ๋๋ค. */
/* isLike์ blog.likes๋ฅผ ์ด์ฉํ์ฌ handleLikeClick์ ๋ก์ง์ ์์ฑํด์ฃผ์ธ์. */
console.log("like!");
};
์ฐธ๊ณ ๋ก ์์ ์ ๋ํผ๋ฐ์ค ์ฝ๋์์๋ fetchPatch์ ๊ฐ์ ํํ๋ก ์ฐ๋ฆฌ๊ฐ ์ฌ์ฉํ๋ API๋ฅผ ๋งค์๋ํ(?) ํด์ ์ฌ์ฉํ๋๋ฐ, ์ ๋ ๊ฒ ์ฌ์ฉํ๊ธฐ ์ํด util ํด๋์ api.js ๋ผ๋ api ๋ฌธ์๋ฅผ ๋ง๋ค์ด์ฃผ์๋ค.
//์ด๋ฐ ์์ผ๋ก api๋ฅผ ๋ฌธ์ํ ์ํฌ ์ ์์ต๋๋ค.
const BASE_URL = 'http://localhost:3000/';
const BLOG_URL = 'http://localhost:3000/blogs/';
export const fetchCreate = (url, data) => {
fetch(url, {
method: "POST",
headers: {"Content-Type" : "application/json"},
body: JSON.stringify(data)
})
.then(() => {
window.location.href = BASE_URL;
})
.catch((error) => {
console.error('Error', error);
})
}
export const fetchDelete = (url, id) => {
fetch(`${url}${id}`, {
method: "DELETE",
})
.then(() => {
window.location.href = BASE_URL;
})
.catch((error) => {
console.error('Error', error);
})
}
export const fetchPatch = (url, id, data) => {
fetch(`${url}${id}`, {
method : "PATCH",
headers: {"Content-Type" : "Application/json"},
body: JSON.stringify(data)
})
.then(() => {
window.location.href = `${BLOG_URL}${id}`;
})
.catch((error) => {
console.error('Error', error);
})
}
์ ๋ํผ๋ฐ์ค ์ฝ๋์ api.js ๋ฌธ์๋ฅผ ๋ณด๋ฉด ์ ์ ์๊ฒ ์ง๋ง, ๋ฆฌ๋ค์ด๋ ์ ์ญ์ BASE_URL์ ํตํด์ ํด์คฌ์ง๋ง, ๊ณผ์ ๋ฅผ ํ ๋๋ react-router-dom์ useNavigate๋ฅผ ์ฌ์ฉํด์ฃผ์๋ค. (๋น์ทํ๊ฒ Link ์ปดํฌ๋ํธ๊ฐ ์๋๋ฐ Link ์ปดํฌ๋ํธ๋ ์ง์ ํ ๊ฒฝ๋ก๋ก ์ด๋์์ผ ์ค๋ค. ํด๋ฆญํ ๊ฒฝ์ฐ ๋ฐ๋ก ์ด๋ํ๋ ๊ฒฝ์ฐ Link ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํ๊ณ , useNavigate๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ํน์ ํ๋์ด๋ ์ถ๊ฐ๋ก ์ฒ๋ฆฌํด์ผํ๋ ๋ก์ง์ด ์๋ ๊ฒฝ์ฐ ์ฌ์ฉ)
useNavigate๋ ํน์ ํ๋์ ํ์ ๋ ์ง์ ํ ์ฃผ์๋ก ์ด๋ํ๊ฒ ํด์ค ์ ์๋ค.
๊ณผ์ ์์๋ ์๋ก์ด ๋ธ๋ก๊ทธ ๊ฒ์๊ธ์ ์์ฑํ๊ณ ๋์ ํ ํ๋ฉด์ผ๋ก ๋ฆฌ๋ค์ด๋ ํธ ํ๋๋ก ํ๋ค.
import { useNavigate } from "react-router-dom";
const CreateBlog = () => {
//์๋ต
const navigate = useNavigate();
const handleSubmit = (e) => {
e.preventDefault();
/* ๋ฑ๋ก ๋ฒํผ์ ๋๋ฅด๋ฉด ๊ฒ์๋ฌผ์ด ๋ฑ๋ก์ด ๋๋ฉฐ home์ผ๋ก ๋ฆฌ๋ค์ด๋ ํธ ๋์ด์ผ ํฉ๋๋ค. */
//ํ ์ฃผ์๋ฅผ ๊ฐ๋๋ก useNavigate ์ฌ์ฉํ๊ธฐ
const postData = {
title,
body,
author,
likes: 0,
};
fetch(`http://localhost:3001/blogs`, {
method: "POST",
body: JSON.stringify(postData),
headers: {
"Content-Type": "application/json",
},
})
.then(() => {
navigate("/");
window.location.reload(); //๋ฐ๋ก ์๋ก๊ณ ์นจ ํด์ฃผ๋ ์ฝ๋
})
.catch((err) => console.log(err));
console.log(e.type);
};
//์๋ต
}
GET ๋ฉ์๋๋ฅผ ํตํด ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ค๋ useEffect hook์ ์ปดํฌ๋ํธ ๋ด ์ฌ๊ธฐ์ ๊ธฐ์ ๋ฐ๋ณต์ด ๋๊ณ ์์ผ๋ฏ๋ก custom hook์ผ๋ก ๋ง๋ค์ด ๋ณด์๋ค.
import { useState, useEffect } from "react";
const useFetch = (url) => {
const [data, setData] = useState();
const [isPending, setIsPending] = useState();
const [error, setError] = useState();
useEffect(() => {
setTimeout(() => {
fetch(url)
.then((res) => {
if (!res.ok) {
throw Error("could not fetch the data for that resource");
}
return res.json();
})
.then((data) => {
setIsPending(false);
setData(data);
console.log(data);
setError(null);
})
.catch((err) => {
setIsPending(false);
setError(err.message);
});
}, 1000);
}, []);
return [data, isPending, error]; /* return ๋ฌธ์ ์์ฑํด์ฃผ์ธ์. */
};
export default useFetch;
import useFetch from "../util/useFetch";
const BlogDetails = () => {
//BlogDetail.js ํ์ผ ์์์ get์ ์ด๋ ๊ฒ ์งง๊ฒ ๋ฐ๊ฟ ์ ์๋ค!
const [blog, isPending, error] = useFetch(
`http://localhost:3001/blogs/${id}`
);
}
์ ์ฉ๋ ์ปดํฌ๋ํธ ์ง์ ์ ํ์ด์ง ๋งจ ์๋ก ์คํฌ๋กคํด์ฃผ๋ ๊ธฐ๋ฅ ๊ตฌํ
import { useEffect } from "react";
const useScroll = () => {
useEffect(() => {
window.scrollTo(0, 0);
}, []);
return null;
};
export default useScroll;
์ฌ์ฉํ ์ปดํฌ๋ํธ์์ ์๋์ ๊ฐ์ด ๋ฃ์ด์ฃผ๊ธฐ
import useScroll from "../util/useScroll";
//์๋ต
const BlogDetails = () => {
const scrollTop = useScroll();
//์๋ต
}