지난시간 정리
- 데이터를 보내는 기능인 action을 이용함. action은 값으로 함수를 받고, 이 부분은 해당 라우트에 정의하고 export 함.
- form 요소를 Form 컴포넌트로 교체. + 여러 프로퍼티들 추가해서 클라이언트가 사용자가 입력한 데이터 정보에 접근하게 이름붙여줌.
- Form 기능으론 서버에 데이터 보내지 않게 막아주고, action 함수 호출
- redirect 컴포넌트 추가해서 데이터 보내고 난 후 메인페이지로 돌아가게 경로 설정.
- 새로운 라우트 추가.
내 목표는 어떤 포스팅이 추가되든, 기존에 있든 상관없이 각기 다른 포스트들의 상세페이지를 보려고한다. 그럴려면 뭔가 형태는 동일하지만, 구별할 수있는 식별자를 추가해서 구분해서 렌더링 해준다. 그 형태를 잡게 해주는 작업이다.
- Posts 라우트 안에 생성.
NewPost에 준 것과 동일한 오버레이 효과를 주기 위해서.
- 상세페이지 라우트 속성 설정.
- path: 메인페이지 url 뒤에 id로 구분짓기. (절대경로
/
, 그리고 경로를 동적으로 설정 하기 위해콜론(:)
을 붙이고 구분지을 변수 추가.
ex)path: '/:postId'
- element: 새롭게 렌더링할 컴포넌트
- 새 컴포넌트 추가.
- loader 함수를 비동기적으로 추가하기.
- 실제로 포스팅 된 포스트 하나하나를 클릭할 수 있게 만들기.
✍ main.jsx (새 라우트 추가 후)
import React from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import ErrorBoundary from "./components/ErrorBoundary";
import Posts, { loader as postsLoader } from "./routes/Posts";
import NewPost, { action as newPostAction } from "./routes/NewPost";
import RootLayout from "./routes/RootLayout";
import "./index.css";
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
children: [
{
path: "/",
element: (
<ErrorBoundary>
<Posts />
</ErrorBoundary>
),
loader: postsLoader,
children: [
{ path: "/create-post", element: <NewPost />, action: newPostAction },
{
path: "/:postId",
},
],
},
],
},
]);
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
이제 새롭게 새 라우트로 이동했을때 렌더링해줄 컴포넌트가 필요함.
✍ PostDetails.jsx (loader 까지 추가 후)
import { useLoaderData, Link } from "react-router-dom";
import Modal from "../components/Modal";
import classes from "./PostDetails.module.css";
function PostDetails() {
const post = useLoaderData();
if (!post) {
return (
<Modal>
<main className={classes.details}>
<h1>Could not find post</h1>
<p>Unfortunately, the requested post could not be found.</p>
<p>
<Link to=".." className={classes.btn}>
Okay
</Link>
</p>
</main>
</Modal>
);
}
return (
<Modal>
<main className={classes.details}>
<p className={classes.author}>{post.author}</p>
<p className={classes.text}>{post.body}</p>
</main>
</Modal>
);
}
export default PostDetails;
export async function loader({ params }) {
const response = await fetch("http://localhost:8080/posts/" + params.postId);
const resData = await response.json();
return resData.post;
}
코드 설명
1. useLoaderData 훅으로post
가져오기 (단, loader 함수가 정의되어 있어서 데이터를 백엔드에서 가져오는 함수가 정의되어 있어야함)
2. 만약 데이터 없다면
똑같이 모달로 래핑해주고, 아무것도 없을 시 ok 버튼누르면 부모 라우터로 이동하도록 설정.
3. 만약 데이터 있다면
똑같이 모달로 래핑해주고, post 객체에서 가져온 글쓴이, 내용 추가해서 렌더링
4. 이 컴포넌트를 렌더링 하기전에useLoaderData
로 데이터를 가져와야함.
5. 위에 있는 1~3번 작업을 위해서 비동기적으로 loader 함수 정의.
6.main.jsx
에서loader
를 import 하고loader
프로퍼티 추가.
7. 백엔드로 데이터 요청하기.
다시useLoaderData
로 돌아와서 데이터를 가져와야하니까
--> 해당 백엔드에서 params라는 속성안에 있는 데이터를 가져와서 json으로 파싱.
--> 그 파싱데이터 안에 resData.post 를 return.
.post
를 반환하는 이유는 백엔드에서 post 객체에 가져온 포스트 데이터를 담아 반환하기 때문
7-1.
ID : loader()에서 인자로 받는 데이터 객체에서 접근
7-2.
params : loader 함수 안에서 매개변수로 쓰이는데 이 매개변수로 들어온 데이터 객체안에 있는 프로퍼티중 하나임. 나머지는 action에서 썼던 request 임.
활성화된 라우트의 ID에 접근가능함.
8. PostDetails.jsx loader 함수에서 반환한 한 개의 포스트 데이터는 PostDetails.jsx 에서 useLoaderData()를 호출해 데이터를 받는다.
이전 설명에서는 내부컴포넌트도 똑같은 방식으로 받을 수 있다고 했음.
✍ app.jsx (backend 코드고 :/id
부분만 보면 됨 )
const express = require("express");
const bodyParser = require("body-parser");
const { getStoredPosts, storePosts } = require("./data/posts");
const app = express();
app.use(bodyParser.json());
app.use((req, res, next) => {
// Attach CORS headers
// Required when using a detached backend (that runs on a different domain)
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET,POST");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
next();
});
app.get("/posts", async (req, res) => {
const storedPosts = await getStoredPosts();
// await new Promise((resolve, reject) => setTimeout(() => resolve(), 1500));
res.json({ posts: storedPosts });
});
app.get("/posts/:id", async (req, res) => {
const storedPosts = await getStoredPosts();
const post = storedPosts.find((post) => post.id === req.params.id);
res.json({ post });
});
app.post("/posts", async (req, res) => {
const existingPosts = await getStoredPosts();
const postData = req.body;
const newPost = {
...postData,
id: Math.random().toString(),
};
const updatedPosts = [newPost, ...existingPosts];
await storePosts(updatedPosts);
res.status(201).json({ message: "Stored new post.", post: newPost });
});
app.listen(8080);
✍ main.jsx (element, loader 속성 추가)
import React from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import ErrorBoundary from "./components/ErrorBoundary";
import Posts, { loader as postsLoader } from "./routes/Posts";
import NewPost, { action as newPostAction } from "./routes/NewPost";
import PostDetails, { loader as postDatailsLoader } from "./routes/PostDetails";
import RootLayout from "./routes/RootLayout";
import "./index.css";
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
children: [
{
path: "/",
element: (
<ErrorBoundary>
<Posts />
</ErrorBoundary>
),
loader: postsLoader,
children: [
{ path: "/create-post", element: <NewPost />, action: newPostAction },
{
path: "/:postId",
element: <PostDetails />,
loader: postDatailsLoader,
},
],
},
],
},
]);
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
✍ Post.jsx (Link 로 래핑)
import { Link } from "react-router-dom";
import classes from "./Post.module.css";
function Post({ id, author, body }) {
return (
<li className={classes.post}>
<Link to={id}>
<p className={classes.author}>{author}</p>
<p className={classes.text}>{body}</p>
</Link>
</li>
);
}
export default Post;
✍ PostList.jsx (Post에 id 속성 추가, key 값 변경)
import { useLoaderData } from "react-router-dom";
import Post from "./Post";
import classes from "./PostList.module.css";
function PostList() {
const posts = useLoaderData();
return (
<>
{posts.length > 0 && (
<ul className={classes.posts}>
{posts.map((post) => (
<Post
key={post.id}
id={post.id}
author={post.author}
body={post.body}
/>
))}
</ul>
)}
{posts.length === 0 && (
<div style={{ textAlign: "center", color: "white" }}>
<h2>포스트가 없습니다.</h2>
<p>여기에 내용을 추가해보세요 !</p>
</div>
)}
</>
);
}
export default PostList;
이걸 끝으로 이 데모 앱 개발은 끝났다.
CRUD에서 cr만 구현했으므로 사실 반쪽짜리라고 보긴 해야하지만
금방 구현 가능하다. 이후에 업데이트 해서 update, delete 기능도 추가 하겠다.
좋았다