게시판 만들기 (프론트)

김주언·2022년 9월 28일
0

MERN

목록 보기
3/4
post-thumbnail

프로젝트 설정

이전 게시물에서 만들어 둔 client 폴더로 이동한다.
모듈을 추가해줄것임~

npm i react-router-dom

라우팅을 위해서 사용하는 모듈,,

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <script
      src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
      integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
      crossorigin="anonymous"
    ></script>
    <script
      src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
      integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
      crossorigin="anonymous"
    ></script>
    <link
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
      integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
      crossorigin="anonymous"
    />

    <!-- web font -->
    <!-- 1 -->
    <link
      href="https://fonts.googleapis.com/css?family=Open+Sans&display=swap"
      rel="stylesheet"
    />

    <!-- my css -->
    <link rel="stylesheet" href="/style.css" />
    <title>React App</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

웹 폰트 추가해주고 부트스트랩 cdn 추가해줌



Nav.jsx

client/src/components/Nav.jsx

import React from "react";

const Nav = () => {
  return (
    <nav className="navbar navbar-expand-sm navbar-light bg-light mb-3">
      <div className="container">
        <div className="navbar-brand">My Website</div>
        <button
          className="navbar-toggler"
          type="button"
          data-toggle="collapse"
          data-target="#navbarSupportedContent"
        >
          <span className="navbar-toggler-icon"></span>
        </button>
        <div className="collapse navbar-collapse" id="navbarSupportedContent">
          <ul className="navbar-nav">
            <li className="nav-item">
              <a href="/" className="nav-link">
                Home
              </a>
            </li>
            <li className="nav-item">
              <a href="/about" className="nav-link">
                About
              </a>
            </li>
            <li className="nav-item">
              <a href="/board" className="nav-link">
                Board
              </a>
            </li>
          </ul>
        </div>
      </div>
    </nav>
  );
};

export default Nav;

Home.jsx

import React from "react";

const Home = () => {
  return (
    <div className="container mb-3">
      <div className="jumbotron">
        <h1>MY WEBSITE</h1>
        <p>HOME</p>
      </div>
    </div>
  );
};

export default Home;

About.jsx

import React from "react";

const About = () => {
  return (
    <div className="container mb-3">
      <h2 className="mb-3">About</h2>
    </div>
  );
};

export default About;

App.js

import React from "react";
import Nav from "./components/Nav";
import Home from "./routes/Home";
import About from "./routes/About";
import { Route, Routes } from "react-router-dom";

const App = () => {
  return (
    <div>
      <Nav />
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </div>
  );
};

export default App;

index.js

import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);

이게 젤 기본...

이제 Board로 이동하면 포스트 리스트를 출력하도록 코드를 작성한다.


Board 화면

1. App.js 수정

// import 생략
const App = () => {
  return (
    <div>
      <Nav />
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/board" element={<PostList />} />
      </Routes>
    </div>
  );
};

export default App;

Board 페이지로 이동하는 Route를 추가해준다.

2. PostList.jsx

src/routes/PostList.jsx

import React from "react";

const Board = () => {
  return (
    <div className="container mb-3">
      <h2 className="mb-3">Board</h2>
      <table className="board-table table table-sm border-bottom">
        <thead className="thead-light">
          <tr>
            <th scope="col">Title</th>
            <th scope="col" className="date">
              Date
            </th>
          </tr>
        </thead>
        <tbody></tbody>
      </table>
      <div>
        <a className="btn btn-primary" href="/posts/new">
          New
        </a>
      </div>
    </div>
  );
};

export default Board;

실행결과는 위와 같다.

이제 <tbody> 부분에 포스트들을 넣어줄거임


2.1 PostItem.jsx

src/components/PostItem.jsx

import React from "react";

const PostItem = () => {
  return (
    <tr>
      <td>
        <a>
          <div className="ellipsis">포스트 타이틀</div>
        </a>
      </td>
      <td className="date">
        <span>포스트 생성일</span>
      </td>
    </tr>
  );
};

export default PostItem;

PostList.jsx 수정

// 생략
        </thead>
        <tbody>
          <PostItem />
        </tbody>
      </table>
      <div>
        <a className="btn btn-primary" href="/posts/new">
          New
        </a>
      </div>
    </div>
  );
};

export default Board;

tbody에 포스트 아이템을 넣어주면 아래처럼 된다.

이제 포스트들을 DB에서 가져와보자~~
fetch를 사용한다...


3. DB에서 포스트 가져오기

3.1 PostList.jsx

import React from "react";
import { useEffect, useState } from "react";
import PostItem from "../components/PostItem";

const PostList = () => {
  const [posts, setPosts] = useState([]);

  // 1. 
  async function getPosts() {
    const response = await fetch("http://localhost:5500/posts");

    if (!response.ok) {
      const message = `An error occurred: ${response.statusText}`;
      window.alert(message);
      return;
    }

    // 2.
    const postlist = await response.json();
    setPosts(postlist);
  }

  useEffect(() => {
    getPosts();
  }, []);
  

  // 3.
  const postItems = posts.map((post, idx) => (
    <PostItem post={post} key={idx} />
  ));

  return (
    <div className="container mb-3">
      <h2 className="mb-3">Board</h2>
      <table className="board-table table table-sm border-bottom">
        <thead className="thead-light">
          <tr>
            <th scope="col">Title</th>
            <th scope="col" className="date">
              Date
            </th>
          </tr>
        </thead>
        <tbody>{postItems}</tbody>
      </table>
      <div>
        <a className="btn btn-primary" href="/posts/new">
          New
        </a>
      </div>
    </div>
  );
};

export default PostList;

1. getPosts()

서버주소인 "http://localhost:5500/posts"로 GET 요청을 보낸다

fetch 함수를 사용한다. 서버에 네트워크 요청을 보내고 새로운 정보를 받아오는 함수이다.

fetch 기본 문법

let promise = fetch(url, [options])
  • url – 접근하고자 하는 URL
  • options – 선택 매개변수, method나 header 등을 지정할 수 있음

응답은 대개 두 단계를 거쳐 진행된다

  1. 서버에서 응답 헤더를 받자마자 fetch 호출 시 반환받은 promise가 내장 클래스 Response의 인스턴스와 함께 이행 상태가 된다
    이 단계는 아직 본문(body)이 도착하기 전이지만, 개발자는 응답 헤더를 보고 요청이 성공적으로 처리되었는지 아닌지를 확인가능하다.

  2. 추가 메서드를 호출해 응답 본문을 받기
    response 에는 프라미스를 기반으로 하는 다양한 메서드가 존재한다. 이 메서드들을 사용하면 다양한 형태의 응답 본문을 처리
    - response.text() : 응답을 읽고 텍스트를 반환
    - response.json() : 응답을 JSON 형태로 파싱


2.

const postlist = await response.json();
setPosts(postlist);

서버에 보낸 요청에 대한 응답을 JSON 형태로 파싱하여 postlist 변수로 저장하여 posts에 저장한다.


3.

  const postItems = posts.map((post, idx) => (
    <PostItem post={post} key={idx} />
  ));

posts 배열을 순회하면서 PostItem 컴포넌트를 반환한다. 이때 속성값으로 각 post를 지정해준다.

3.2 PostItem.jsx

import React from "react";

const PostItem = (props) => {
  const { post } = props;

  return (
    <tr>
      <td>
        <a>
          <div className="ellipsis">{post.title}</div>
        </a>
      </td>
      <td className="date">
        <span>{post.createdAt}</span>
      </td>
    </tr>
  );
};

export default PostItem;

PostList.jsx에서 전달받은 post를 이용하여 포스트 값을 나타낸다.

실행결과는 아래와 같다



Post 상세화면

1. App.js 수정

Route를 추가해준다

        <Route path="/posts/:id" element={<PostDetail />} />

:id 를 통해서 url에서 파라미터 값을 전달할 수 있게된다.


2. PostItem.jsx 수정

import React from "react";

const PostItem = (props) => {
  const { post } = props;

  return (
    <tr>
      <td>
        <a href={`/posts/${post._id}`}>
          <div className="ellipsis">{post.title}</div>
        </a>
      </td>
      <td className="date">
        <span>{post.createdAt}</span>
      </td>
    </tr>
  );
};

export default PostItem;

a 태그에 post._id를 전달한다.

<a href={`/posts/${post._id}`}>

3. PostDetail.jsx 작성

import React from "react";

const PostDetail = () => {
  return (
    <div className="container mb-3">
      <nav aria-label="breadcrumb">
        <ol className="breadcrumb p-1 pl-2 pr-2">
          <li className="breadcrumb-item">
            <a href="/">Home</a>
          </li>
          <li className="breadcrumb-item">
            <a href="/posts">Board</a>
          </li>
          <li className="breadcrumb-item active" aria-current="page">
            post.title
          </li>
        </ol>
      </nav>
      <div className="card">
        <h5 className="card-header p-2">post.title</h5>
        <div className="row">
          <div className="col-md-7 col-lg-8 col-xl-9 order-sm-2 order-md-1">
            <div className="post-body p-2">post.content</div>
          </div>

          <div className="col-md-5 col-lg-4 col-xl-3 order-sm-1 order-md-2">
            <div className="post-info card m-2 p-2">
              <div>
                <span>Created</span> : <span>post.createdAt</span>
                <br />
              </div>
            </div>
          </div>
        </div>
      </div>
      <div className="mt-3">
        <a className="btn btn-primary" href="/posts">
          Back
        </a>
        <a className="btn btn-primary">Edit</a>

        <button className="btn btn-primary">Delete</button>
      </div>
    </div>
  );
};

export default PostDetail;

여기까지 코드 작성 후에 결과를 확인해보면 아래와 같다. 포스트 리스트에서 하나의 포스트를 클릭하면 화면이 이동한다.

이제 하나의 포스트를 DB에서 추출해와서 이를 화면에 출력해주면 된다.

url 파라미터에서 id를 추출해서 해당 아이디를 이용하여 DB에서 포스트의 데이터를 추출한다

url 파라미터 사용을 위해서 useParams를 사용한다.

  const params = useParams();
  console.log(params.id);
<Route path="/posts/:id" element={<PostDetail />} />

라우트 path에 설정해준 id 값을 가져오게 되는 것임

3.1 PostDetail.jsx 수정

import React, { useEffect } from "react";
import { useState } from "react";
import { useParams } from "react-router-dom";

const PostDetail = () => {
  const params = useParams();
  const [post, setPost] = useState({});

  async function fetchData() {
    const id = params.id.toString();
    const response = await fetch(`http://localhost:5500/posts/${id}`);
    if (!response.ok) {
      const message = `An error has occurred: ${response.statusText}`;
      window.alert(message);
      return;
    }

    const post = await response.json();

    setPost(post);
  }

  useEffect(() => {
    fetchData();
  }, []);

  return (
    <div className="container mb-3">
      <nav aria-label="breadcrumb">
        <ol className="breadcrumb p-1 pl-2 pr-2">
          <li className="breadcrumb-item">
            <a href="/">Home</a>
          </li>
          <li className="breadcrumb-item">
            <a href="/posts">Board</a>
          </li>
          <li className="breadcrumb-item active" aria-current="page">
            {post.title}
          </li>
        </ol>
      </nav>
      <div className="card">
        <h5 className="card-header p-2">{post.title}</h5>
        <div className="row">
          <div className="col-md-7 col-lg-8 col-xl-9 order-sm-2 order-md-1">
            <div className="post-body p-2">{post.content}</div>
          </div>

          <div className="col-md-5 col-lg-4 col-xl-3 order-sm-1 order-md-2">
            <div className="post-info card m-2 p-2">
              <div>
                <span>Created</span> : <span>{post.createdAt}</span>
                <br />
                {post.updatedAt ? (
                  <span>Updated : {post.updatedAt} </span>
                ) : (
                  <span></span>
                )}
              </div>
            </div>
          </div>
        </div>
      </div>
      <div className="mt-3">
        <a className="btn btn-primary" href="/posts">
          Back
        </a>
        <a className="btn btn-primary" href={`/posts/${post._id}/edit`}>

        <button className="btn btn-primary">Delete</button>
      </div>
    </div>
  );
};

export default PostDetail;

실행결과

포스트 추가와 수정

App.js 수정

        <Route path="/posts/:id/edit" element={<Edit />} />
        <Route path="/posts/new" element={<New />} />

경로를 설정해준다

New.jsx

import React from "react";
import { useState } from "react";
import { useNavigate } from "react-router-dom";

const New = () => {
  const navigate = useNavigate();

  const [form, setForm] = useState({
    title: "",
    content: "",
  });

  function updateForm(value) {
    return setForm((prev) => {
      return { ...prev, ...value };
    });
  }

  async function onSubmit(e) {
    e.preventDefault();

    // fetch에 옵션 지정하여 서버에 요청을 전달한다.
    await fetch(`http://localhost:5500/posts/`, {
      method: "POST",
      body: JSON.stringify(form),
      headers: {
        "Content-Type": "application/json",
      },
    });

    // 수정 후 이전페이지로 돌아가기
    navigate(`/posts/`);
  }

  return (
    <div className="container mb-3">
      <nav aria-label="breadcrumb">
        <ol className="breadcrumb p-1 pl-2 pr-2">
          <li className="breadcrumb-item">
            <a href="/">Home</a>
          </li>
          <li className="breadcrumb-item">
            <a href="/posts">Board</a>
          </li>
          <li className="breadcrumb-item active" aria-current="page">
            Create Post
          </li>
        </ol>
      </nav>
      <form onSubmit={onSubmit}>
        <div className="form-group">
          <label htmlFor="title">Title</label>
          <input
            type="text"
            id="title"
            name="title"
            className="form-control"
            value={form.title}
            onChange={(e) => updateForm({ title: e.target.value })}
          />
        </div>

        <div className="form-group">
          <label htmlFor="content">Content</label>
          <textarea
            id="content"
            name="content"
            rows="5"
            className="form-control"
            value={form.content}
            onChange={(e) => updateForm({ content: e.target.value })}
          ></textarea>
        </div>
        <div>
          <a className="btn btn-primary" href={`/posts/`}>
            Back
          </a>
          <button type="submit" className="btn btn-primary">
            Submit
          </button>
        </div>
      </form>
    </div>
  );
};

export default New;

Edit.jsx

import React, { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";

const Edit = () => {
  // 파라미터를 받기 위해 사용
  const params = useParams();

  // 페이지 이동을 위해 사용
  const navigate = useNavigate();

  // 폼 상태를 관리한다.
  const [form, setForm] = useState({
    title: "",
    content: "",
  });

  // url로 넘어온 파라미터를 이용하여 특정 포스트의 내옹을 DB에서 불러온다
  async function fetchData() {
    const id = params.id.toString();
    const response = await fetch(`http://localhost:5500/posts/${id}`);
    if (!response.ok) {
      const message = `An error has occurred: ${response.statusText}`;
      window.alert(message);
      return;
    }

    const post = await response.json();

    // 불러온 포스트의 내용을 상태저장
    setForm(post);
  }

  // 폼 제출 시 이벤트
  async function onSubmit(e) {
    e.preventDefault();
    const editedPost = {
      title: form.title,
      content: form.content,
    };

    // fetch에 옵션 지정하여 서버에 요청을 전달한다.
    await fetch(`http://localhost:5500/posts/${params.id}`, {
      method: "PUT",
      body: JSON.stringify(editedPost),
      headers: {
        "Content-Type": "application/json",
      },
    });

    // 수정 후 이전페이지로 돌아가기
    navigate(`/posts/${form._id}`);
  }

  // 폼 상태 변화 제어
  function updateForm(value) {
    return setForm((prev) => {
      return { ...prev, ...value };
    });
  }

  useEffect(() => {
    fetchData();
  }, []);

  return (
    <div className="container mb-3">
      <nav aria-label="breadcrumb">
        <ol className="breadcrumb p-1 pl-2 pr-2">
          <li className="breadcrumb-item">
            <a href="/">Home</a>
          </li>
          <li className="breadcrumb-item">
            <a href="/posts">Board</a>
          </li>
          <li className="breadcrumb-item">
            <a href={`/posts/${form._id}`}>{form.title}</a>
          </li>
          <li className="breadcrumb-item active" aria-current="page">
            Edit Post
          </li>
        </ol>
      </nav>
      <form onSubmit={onSubmit}>
        <div className="form-group">
          <label htmlFor="title">Title</label>
          <input
            type="text"
            id="title"
            name="title"
            className="form-control"
            value={form.title}
            onChange={(e) => updateForm({ title: e.target.value })}
          />
        </div>

        <div className="form-group">
          <label htmlFor="content">Content</label>
          <textarea
            id="content"
            name="content"
            rows="5"
            className="form-control"
            value={form.content}
            onChange={(e) => updateForm({ content: e.target.value })}
          ></textarea>
        </div>
        <div>
          <a className="btn btn-primary" href={`/posts/${form._id}`}>
            Back
          </a>
          <button type="submit" className="btn btn-primary">
            Submit
          </button>
        </div>
      </form>
    </div>
  );
};

export default Edit;

추가랑 수정은 거의 비슷하다~~ fetch 할 때 보내주는 경로랑 메서드 확인!

profile
학생 점심을 좀 차리시길 바랍니다

0개의 댓글