TC - 21번일지 (동적라우트)

Debug-Life ·2023년 3월 28일
0

지난시간 정리

  • 데이터를 보내는 기능인 action을 이용함. action은 값으로 함수를 받고, 이 부분은 해당 라우트에 정의하고 export 함.
  • form 요소를 Form 컴포넌트로 교체. + 여러 프로퍼티들 추가해서 클라이언트가 사용자가 입력한 데이터 정보에 접근하게 이름붙여줌.
  • Form 기능으론 서버에 데이터 보내지 않게 막아주고, action 함수 호출
  • redirect 컴포넌트 추가해서 데이터 보내고 난 후 메인페이지로 돌아가게 경로 설정.



* 이 포스팅 목표 : 상세 페이지 구현(포스트 눌렀을 때)



  1. 새로운 라우트 추가.
    내 목표는 어떤 포스팅이 추가되든, 기존에 있든 상관없이 각기 다른 포스트들의 상세페이지를 보려고한다. 그럴려면 뭔가 형태는 동일하지만, 구별할 수있는 식별자를 추가해서 구분해서 렌더링 해준다. 그 형태를 잡게 해주는 작업이다.
  1. Posts 라우트 안에 생성.
    NewPost에 준 것과 동일한 오버레이 효과를 주기 위해서.
  1. 상세페이지 라우트 속성 설정.
  • path: 메인페이지 url 뒤에 id로 구분짓기. (절대경로 /, 그리고 경로를 동적으로 설정 하기 위해 콜론(:)을 붙이고 구분지을 변수 추가.
    ex) path: '/:postId'
  • element: 새롭게 렌더링할 컴포넌트
  1. 새 컴포넌트 추가.
  • loader 함수를 비동기적으로 추가하기.
  1. 실제로 포스팅 된 포스트 하나하나를 클릭할 수 있게 만들기.



1. 새 라우트 생성


✍ 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>
);

이제 새롭게 새 라우트로 이동했을때 렌더링해줄 컴포넌트가 필요함.



2. 새 컴포넌트 생성


✍ 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);



3. element, loader


✍ 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>
);



4. 포스팅 된 포스트 클릭할 수 있게 만들기.


✍ 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;
  • Link 속성 값은 동적으로 추가. (매번 id 가 다른 걸 렌더링 해야하므로)



✍ 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 기능도 추가 하겠다.

profile
인생도 디버깅이 될까요? 그럼요 제가 하고 있는걸요

1개의 댓글

comment-user-thumbnail
2023년 3월 29일

좋았다

답글 달기