๐ŸŒŸ [SEB FE] Section 2_ํšŒ๊ณ 

๊ฐ•์„ฑ์ผยท2023๋…„ 6์›” 11์ผ
0
post-thumbnail

โœ… ToDo


  • Notion(์ฝ”๋“œ) -> codesandbox
  • ์ฝ”ํ”Œ๋ฆฟ
  • ๋‚˜๋งŒ์˜ ์•„๊ณ ๋ผ์Šคํ…Œ์ด์ธ  ์ฝ”๋“œ ๋ฆฌํŒฉํ† ๋ง(discussion ์ปดํฌ๋„ŒํŠธ)
  • ๋‚˜๋งŒ์˜ ์•„๊ณ ๋ผ์Šคํ…Œ์ด์ธ  ๋””ํ…Œ์ผ ๊ตฌํ˜„(Method, Css)
  • Error Note ์ž‘์„ฑ
  • 6, 7, 8์ฃผ์ฐจ ํšŒ๊ณ 


๐Ÿ’ฌ ์žก๋‹ด


์‚ด๋ฉด์„œ ์ผ์ฃผ์ผ์ด ์ด๋ ‡๊ฒŒ ์งง์€์ง€ ๋ชฐ๋ž์Šต๋‹ˆ๋‹ค.. ๐Ÿ˜…

๊นƒํ—™ ์ž”๋””๋ฐญ์—๋Š” fork ํ•ด์™€์„œ ๊ทธ๋Ÿฐ์ง€ ์˜ฌ๋ผ๊ฐ€์ง€ ์•Š์ง€๋งŒ, ๋งค์ผ ์ปค๋ฐ‹๋„ ํ•˜๊ณ  ์žˆ์Šด๋‹ˆ๋‹ค ใ…Ž.ใ…Ž

๋ญ”๊ฐ€ ์š”์ฆ˜ ํ•˜๋ฃจ๋Š” ๊ณต๋ถ€ + ๋ธ”๋กœ๊น…์ด๋ฉด ์ •๋ง ๋š๋”ฑ์ž…๋‹ˆ๋‹น

๊ทผ๋ฐ ์šฐ๋ฆฌ ๊ทธ๋Ÿฐ ๋ง์ด ์žˆ์ž–์•„์š”. ์ด์—ด์น˜์—ด์ด๋ผ๊ณ  ใ…Ž

์ด ์ƒํƒœ์—์„œ ์ฝ”๋“œ์Šคํ…Œ์ด์ธ  ์Šคํ„ฐ๋””๊นŒ์ง€ ๋“ค์–ด๊ฐ”์Šต๋‹ˆ๋‹ค :)

์ •๋ง ์ข‹์€ ์Šคํ„ฐ๋”” ์žฅ๋‹˜์ด ๊ณ„์‹  ์Šคํ„ฐ๋””์—ฌ์„œ ํ˜ธ์‹œํƒํƒ ๋…ธ๋ฆฌ๊ธฐ๋งŒ ํ•˜๋‹ค๊ฐ€ ๋“ค์–ด์™”๋„ค์š” ๐Ÿ˜š

์•„ ๊ทธ๋ฆฌ๊ณ  ํšŒ๊ณ ๋ก์„ ์ฃผ๊ณ ๋ฐ›๋Š” ์‹œ๊ฐ„์ด ์žˆ์—ˆ๋Š”๋ฐ ์ œ ๋ธ”๋กœ๊ทธ๋ฅผ ๋ด์ฃผ์‹ ๋‹ค๊ณ  ํ•˜์‹  ๋ถ„๋“ค
(์ œ๊ฐ€ ์„œ์šธ ์ƒ๋‚จ์ž๋ผ ๋ง์„ ์ž˜ ๋ชปํ•ด์„œ..) โฃ๏ธ ๋“œ๋ฆผ๋‹ˆ๋‹ค.

์•ž์œผ๋กœ ๋”์šฑ๋” ํ„ฐ์งˆ๋„๋ก ๋‹ฌ๋ฆด ์˜ˆ์ •์ž…๋‹ˆ๋‹ค. ์—ฐ๋ฃŒ๋Š” ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๋‹ค.

์š”์•ฝ์€, ์˜ค๋Š˜๋„ ํ™”์ดํŒ… ๐Ÿ”ฅ๐Ÿ”ฅ

๐Ÿ ์ž~ ๊ทธ๋Ÿผ ์ถœ๋ฐœ~! ๐Ÿ



๐Ÿ”ฅ Review


1. ๋‚˜๋งŒ์˜ ์•„๊ณ ๋ผ์Šคํ…Œ์ด์ธ  ์ฝ”๋“œ ๋ฆฌํŒฉํ† ๋ง(discussion ์ปดํฌ๋„ŒํŠธ)


์ €๋ฒˆ์— ์•„๊ณ ๋ผ์Šคํ…Œ์ด์ธ  ํšŒ๊ณ ๋ฅผ ์ž‘์„ฑํ–ˆ์„ ๋•Œ 1์ฐจ๋กœ ์™„๋ฃŒํ•œ ์‚ฌํ•ญ์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  1. ๊ธฐ์กด ํด๋ผ์ด์–ธํŠธ ํŒŒ์ผ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋˜ mock ํŒŒ์ผ ์‚ญ์ œ
  2. ํฐ ํ‹€๋กœ ์‚ฌ์šฉํ•  ํด๋” 2๊ฐœ ์ƒ์„ฑ (Server, Client)
  3. Client๋Š” React ์‹์œผ๋กœ ๋ฆฌํŒฉํ† ๋ง(useState, useEffect, map), Server๋Š” ๊ทธ๋Œ€๋กœ ๊ฐ€์ ธ์˜ค๊ธฐ
  4. Client์— Axios๋กœ ์‹ค์ œ ์„œ๋ฒ„ ์ฃผ์†Œ Get ํ•ด์˜ค๊ธฐ
  5. Client ๋””ํ…Œ์ผํ•œ ์ฝ”๋“œ(discussion ์ปดํฌ๋„ŒํŠธ), CSS ์ฝ”๋“œ ๋ฆฌํŒฉํ† ๋ง

๊ทธ๋ฆฌ๊ณ  ์ด์–ด์„œ ์ง„ํ–‰ํ•  2์ฐจ ์ž‘์—… ToDO๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์•˜๋‹ค.

  1. discussion ๋ˆŒ๋ €์„ ๋•Œ, URL ์ œ๋Œ€๋กœ ์ˆ˜์ •
  2. discussion ๋ˆŒ๋ €์„ ๋•Œ, ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ ์ œ์™ธํ•˜๊ณ  ๋ชจ๋‘ ์ˆจ๊น€ ์ฒ˜๋ฆฌ
  3. POST, DELETE ๊ธฐ๋Šฅ ์ถ”๊ฐ€
  4. CSS ๋ฆฌํŒฉํ† ๋ง

๊ฒฐ๋ก ์ ์œผ๋กœ๋Š” ์ผ๋ถ€๋Š” ์ˆ˜ํ–‰ํ–ˆ์ง€๋งŒ, ์ผ๋ถ€๋Š” ์ˆ˜ํ–‰ํ•˜์ง€ ๋ชปํ–ˆ๋‹ค.

๋Œ€์‹  ๋‹ค๋ฅธ ๊ธฐ๋Šฅ์ด ์ถ”๊ฐ€๋˜์—ˆ๋‹ค.
์ด ๊ธฐ๋Šฅ์— ์ •์‹ ์ด ํŒ”๋ ค์„œ ์‹œ๊ฐ„์ด ์กฐ๊ธˆ ๋ชจ์ž๋ž๋‹ค ๐Ÿ˜…



2. ๋‚˜๋งŒ์˜ ์•„๊ณ ๋ผ์Šคํ…Œ์ด์ธ  ๋””ํ…Œ์ผ ๊ตฌํ˜„(Method, Css)

๊ตฌํ˜„์„ ์™„๋ฃŒํ•œ ๋ฆฌ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜์ž๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  1. ๊ธฐ์กด ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ๋“ค์„ ์ปดํฌ๋„ŒํŠธํ™”์‹œ์ผœ ๋ฆฌํŒฉํ† ๋ง
  2. POST, PUT, DELETE ๋ฉ”์„œ๋“œ ์„œ๋ฒ„์— ๊ตฌํ˜„
  3. POST, PUT, DELETE ๋ฉ”์„œ๋“œ ํด๋ผ์ด์–ธํŠธ์— ๊ตฌํ˜„
  4. ์„œ๋ฒ„ ์ž‘๋™ ์‹œ, ์ •์ƒ์ ์œผ๋กœ ํด๋ผ์ด์–ธํŠธ์˜ ๋ชจ๋“  ๋ฉ”์„œ๋“œ๊ฐ€ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธ

์ด์ œ ๊ฐ๊ฐ ์–ด๋–ป๊ฒŒ ์ง„ํ–‰ํ–ˆ๋Š”์ง€ ๋ฆฌ๋ทฐํ•ด๋ณด๋ฉด์„œ, ๋œฏ์–ด๋ณด์ž !


1. ๊ธฐ์กด ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ๋“ค์„ ์ปดํฌ๋„ŒํŠธํ™”์‹œ์ผœ ๋ฆฌํŒฉํ† ๋ง

์ „์— Html ์ฝ”๋“œ๋ฅผ React ์‹์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ์ž‘์—…์€ ์ฒ˜์Œ์ด๋ผ ๋ฏธ์ˆ™ํ•˜๊ธฐ๋„ ํ–ˆ๊ณ , ์‹œ๊ฐ„์ด ์—†์–ด ๋ถˆ๋งŒ์กฑ์Šค๋Ÿฌ์› ๋‹ค.

๊ฐœ์ธ์ ์œผ๋กœ ๋ฉ”์„œ๋“œ๋“ค์„ ์ถ”๊ฐ€์‹œ์ผœ ์ ์šฉ์‹œํ‚ค๋Š” ๊ฒƒ์ด ๋ฉ”์ธ์ด์—ˆ๋‹ค๊ณ  ์ƒ๊ฐํ•˜๊ธด ํ•˜์ง€๋งŒ,
์ด ์ž‘์—… ๋˜ํ•œ ๋งค์šฐ ๋„์›€๋˜๋Š” ์‹œ๊ฐ„์ด์—ˆ๋‹ค๊ณ  ์Šค์Šค๋กœ ์ƒ๊ฐํ•˜๋ฉฐ ๋งŒ์กฑํ•˜๊ณ  ์žˆ๋‹ค.

์ผ๋‹จ ๊ธฐ์กด ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ App.js์— ์ปดํฌ๋„ŒํŠธ 4๊ฐœ๋ฅผ ์ถ”๊ฐ€์‹œ์ผœ ๋ฆฌํŒฉํ† ๋ง์„ ์ง„ํ–‰ํ–ˆ๋‹ค.
๊ธฐ์กด์— ๋น„ํ•ด, ๋ฉ”์„œ๋“œ๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ์Œ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ์ฝ”๋“œ๊ฐ€ ๋งค์šฐ ๊ฐ„๊ฒฐํ•ด์ง„ ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค.

import React, { useState, useEffect } from "react";
import "./App.css";
import axios from "axios";
import { Form, Discussions } from "./components";

const App = () => {
  const [discussions, setDiscussions] = useState([]);
  const [error, setError] = useState(null);

  const addDiscussion = ({ title, author, bodyText }) => {
    const newDiscussionData = {
      title,
      author,
      bodyHTML: bodyText,
    };
    axios
      .post("http://localhost:4000/discussions/", newDiscussionData)
      .then((response) => {
        if (response.status === 201) {
          getDiscussion();
        }
      })
      .catch((error) => {
        setError("Failed to add discussion.");
      });
  };
  const getDiscussion = () => {
    axios
      .get("http://localhost:4000/discussions/")
      .then((response) => {
        setDiscussions(response.data);
      })
      .catch((error) => {
        setError("Failed to fetch discussions.");
      });
  };

  const deleteDiscussion = (id) => {
    axios
      .delete(`http://localhost:4000/discussions/${id}`)
      .then((response) => {
        if (response.status === 202 || response.status === 204) {
          getDiscussion();
        }
      })
      .catch((error) => {
        setError("Failed to delete discussion.");
      });
  };

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

  return (
    <main>
      <div className="form__info">
        <h1>My Agora States</h1>
        <Form addDiscussion={addDiscussion} />
      </div>
      <div className="form__active">
        {error ? (
          <div className="error">{error}</div>
        ) : (
          <Discussions
            discussions={discussions}
            deleteDiscussion={deleteDiscussion}
          />
        )}
      </div>
    </main>
  );
};

export default App;

์ถ”๊ฐ€๋œ ์ปดํฌ๋„ŒํŠธ๋“ค์„ ์†Œ๊ฐœํ•˜๊ฒ ๋‹ค.

  1. Discussion.js - ๊ฐœ์ธ discussion ํ•˜๋‚˜์˜ ์ปดํฌ๋„ŒํŠธ
  2. Discussions.js - discussion ๋“ค์„ ๋ชจ์•„๋‘” ๊ตฌ์—ญ์˜ ์ปดํฌ๋„ŒํŠธ
  3. Form.js - ์งˆ๋ฌธ์„ ์ž‘์„ฑํ•˜๋Š” form ์ปดํฌ๋„ŒํŠธ
  4. index.js - ์œ„ ์ปดํฌ๋„ŒํŠธ 3๊ฐœ ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ˜„์žฌ ํŒŒ์ผ์—์„œ ๋‚ด๋ณด๋‚ด๋Š” ์—ญํ• 

1. Discussion.js

import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTrashAlt } from "@fortawesome/free-solid-svg-icons";

// ๊ธฐ์กด map์œผ๋กœ ๋ฟŒ๋ ค์ฃผ๋˜ ๋Œ€์ƒ์ธ ๋‚ด๋ถ€ Props ์—ญํ• ์„ ๋Œ€์ฒด (DiscussionItem)
// discussion ๊ฐ์ฒด์™€ deleteDiscussion ํ•จ์ˆ˜๋ฅผ ๋ฐ›์•„์™€์„œ ํ™”๋ฉด์— ํ‘œ์‹œํ•˜๋Š” ์—ญํ• 
export const Discussion = ({ discussion, deleteDiscussion }) => {
  const { id, url, avatarUrl, title, author, createdAt, answer } = discussion;
  return (
    <li className="discussion__container">
      <div className="discussion__avatar--wrapper">
        <img
          className="discussion__avatar--image"
          src={avatarUrl}
          alt="avatar"
        />
      </div>
      <div className="discussion__content">
        <h2 className="discussion__title">
          <a href={url}>
            {title.length > 53 ? `${title.slice(0, 53)}...` : title}
          </a>
        </h2>
        <div
          className="discussion__information"
          dangerouslySetInnerHTML={{
            __html: `${author} / ${new Date(createdAt).toLocaleTimeString()}`,
          }}
        ></div>
      </div>

      <div className="discussion__answered">
        <p>{answer ? "โœ…" : "โŒ"}</p>
      </div>
      // ์‚ญ์ œ ๋ฒ„ํŠผ fontawesome package๋กœ ์•„์ด์ฝ˜์œผ๋กœ ๊ตฌํ˜„ & ๊ธฐ๋Šฅ ์ถ”๊ฐ€
      <button
        className="discussion__deleteButton"
        onClick={() => deleteDiscussion(id)}
      >
        <FontAwesomeIcon icon={faTrashAlt} /> 
      </button>
    </li>
  );
};

2. Discussions.js

import { Discussion } from "./Discussion";

// ๊ธฐ์กด map์œผ๋กœ ๋ฟŒ๋ ค์ฃผ๋˜ DiscussionItem ์ปดํฌ๋„ŒํŠธ ์—ญํ•  ๋Œ€์ฒด
// discussions ๋ฐฐ์—ด์„ ๋ฐ›์•„์™€์„œ Discussion ์ปดํฌ๋„ŒํŠธ๋ฅผ ์—ฌ๋Ÿฌ ๊ฐœ ์ƒ์„ฑํ•˜์—ฌ ํ™”๋ฉด์— ๋‚˜ํƒ€๋‚ด๋Š” ์—ญํ• 
export const Discussions = ({ discussions, deleteDiscussion }) => {
  return (
    <section className="discussion__wrapper">
      <ul className="discussions__container">
        {discussions.map((discussion) => {
          return (
            <Discussion
              key={discussion.id}
              discussion={discussion}
              deleteDiscussion={deleteDiscussion}
            />
          );
        })}
      </ul>
    </section>
  );
};

3. Form.js

export const Form = ({ addDiscussion }) => {
  // ์ œ์ถœ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅผ ์‹œ, form ์ž‘์„ฑ ์ •๋ณด๋ฅผ ๋„˜๊ฒจ์ฃผ๋Š” ์—ญํ•  
  const handleSubmit = (event) => {
    event.preventDefault();
    const author = event.target[0].value;
    const title = event.target[1].value;
    const bodyText = event.target[2].value;
    addDiscussion({ author, title, bodyText });
  };

  return (
    <section className="form__container">
      <form action="" method="get" className="form" onSubmit={handleSubmit}>
        <div className="form__input--wrapper">
          <div className="form__input--name">
            <label htmlFor="name">Enter your name: </label>
            <input type="text" name="name" id="name" required />
          </div>
          <div className="form__input--title">
            <label htmlFor="title">Enter your title: </label>
            <input type="text" name="title" id="title" required />
          </div>
          <div className="form__textbox">
            <label htmlFor="story">Your question: </label>
            <textarea
              id="story"
              name="story"
              placeholder="์งˆ๋ฌธ์„ ์ž‘์„ฑํ•˜์„ธ์š”"
              required
            ></textarea>
          </div>
        </div>
        <div className="form__submit">
          <input type="submit" value="submit" />
        </div>
      </form>
    </section>
  );
};

4. Index.js

export * from "./Form";
export * from "./Discussions";
export * from "./Discussion";


2. POST, PUT, DELETE ๋ฉ”์„œ๋“œ ์„œ๋ฒ„์— ๊ตฌํ˜„

๋จผ์ € ์„œ๋ฒ„์— ๊ตฌํ˜„ํ•˜๊ณ  ํด๋ผ์ด์–ธํŠธ์— ๊ด€๋ จ ๊ธฐ๋Šฅ๋“ค์„ Merge ํ•˜๋Š” ๊ฒƒ์ด ๋งž๋‹ค๊ณ  ์ƒ๊ฐํ•˜์—ฌ, ์„œ๋ฒ„๋ฅผ ๋จผ์ € ๊ตฌํ˜„ํ–ˆ๋‹ค.

๋ผ์šฐํ„ฐ๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์€ ์ผ๋„ ์•„๋‹ˆ์—ˆ์ง€๋งŒ... ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ์—ญ์‹œ ๋ณธ์ฒด์˜€๋‚˜.. ๋„ˆ๋ฌด ํž˜๋“ค์—ˆ๋‹ค.. ใ…Ž


1. server/router/discussions.js

const { discussionsController } = require("../controller");
const { findAll, findById, createOne, updateById, deleteById } =
  discussionsController;
const express = require("express");
const router = express.Router();

// TODO: ๋ชจ๋“  discussions ๋ชฉ๋ก์„ ์กฐํšŒํ•˜๋Š” ๋ผ์šฐํ„ฐ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

router.get("/", findAll);

// TODO: :id์— ๋งž๋Š” discussion์„ ์กฐํšŒํ•˜๋Š” ๋ผ์šฐํ„ฐ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

router.get("/:id", findById);

// โœจ TODO: discussion ํ•˜๋‚˜๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๋ผ์šฐํ„ฐ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. 

router.post("/", createOne);

// โœจ TODO: discussion ํ•˜๋‚˜๋ฅผ ์ˆ˜์ •ํ•˜๋Š” ๋ผ์šฐํ„ฐ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

router.put("/:id", updateById);

// โœจ TODO: discussion ํ•˜๋‚˜๋ฅผ ์‚ญ์ œํ•˜๋Š” ๋ผ์šฐํ„ฐ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

router.delete("/:id", deleteById);

module.exports = router;

2. server/controller/index.js

const { agoraStatesDiscussions } = require("../repository/discussions");
const discussionsData = agoraStatesDiscussions;

// ์š”์ฒญ ๋ณธ๋ฌธ(form ์ž‘์„ฑ ์ •๋ณด)์„ ๊ฒ€์‚ฌํ•˜์—ฌ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๊ณ ,
// ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๊ฑฐ๋‚˜ ์ž˜๋ชป๋œ ๊ฒฝ์šฐ์—๋Š” ์ ์ ˆํ•œ ์‘๋‹ต์„ ๋ณด๋‚ด๋Š” ์—ญํ• 
const handleRequestBody = (req, res) => {
  if (!req.body) return res.status(400).send("no request body");
  const { title, author, bodyHTML } = req.body;
  if (!title && !author && !bodyHTML)
    return res.status(400).send("bad request");
  return true;
};

const discussionsController = {
  findAll: (req, res) => {
    const { author } = req.query;
    if (author) {
      res.json(discussionsData.filter((item) => item.author === author));
    } else {
      res.json(discussionsData);
    }
  },
  findById: (req, res) => {
    const { id } = req.params;
    if (id) {
      const discussion = discussionsData.find((item) => item.id === Number(id));
      if (discussion) {
        res.json(discussion);
      } else {
        res.status(404).json({ error: "Discussion not found." });
      }
    }
  },
  // ํด๋ผ์ด์–ธํŠธ๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ ์š”์ฒญ์˜ ๋ณธ๋ฌธ(req.body)์—์„œ title, author, bodyHTML์„ ์ถ”์ถœํ•˜๊ณ ,
  // ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๊ฐ€ ํ†ต๊ณผ๋˜๋ฉด ์ƒˆ๋กœ์šด ํ† ๋ก (discussion) ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๊ณ , ์ด๋ฅผ discussionsData ๋ฐฐ์—ด์˜ ๊ฐ€์žฅ ์•ž์— ์ถ”๊ฐ€ํ•˜๋Š” ์—ญํ• 
  createOne: (req, res) => {
    const { title, author, bodyHTML } = req.body;

    if (handleRequestBody(req, res) !== true) return;
    const id = parseInt(Math.random() * 10000);
    const avartarId = parseInt(Math.random() * 100);
    const url =
      "https://github.com/codestates-seb/agora-states-fe/discussions/" + id;
    const newDiscussion = {
      id,
      createdAt: new Date().toISOString(),
      title,
      url,
      author,
      answer: null,
      bodyHTML,
      avatarUrl: `https://randomuser.me/api/portraits/men/${avartarId}.jpg`,
    };
    discussionsData.unshift(newDiscussion);
    return res.status(201).send("resource created successfully: ID " + id);
  },
  // ํด๋ผ์ด์–ธํŠธ๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ ์š”์ฒญ์˜ ๋ณธ๋ฌธ(req.body)์„ ์ด์šฉํ•˜์—ฌ ํ† ๋ก ์˜ ์—…๋ฐ์ดํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” ์—ญํ• 
  updateById: (req, res) => {
    if (handleRequestBody(req, res) !== true) return;
    const idx = discussionsData.findIndex(
      (d) => d.id === Number(req.params.id)
    );
    const updated = {
      ...discussionsData[idx],
      ...req.body,
      updatedAt: new Date().toISOString(),
    };

    if (idx !== -1) {
      discussionsData.splice(idx, 1, updated);
      return res.status(200).send("resource updated successfully");
    } else {
      return res.status(404).send("Not found");
    }
  },
  // ํด๋ผ์ด์–ธํŠธ๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ ์š”์ฒญ์˜ URL ํŒŒ๋ผ๋ฏธํ„ฐ(req.params.id)๋ฅผ ์ด์šฉํ•˜์—ฌ ํ† ๋ก ์„ ์‚ญ์ œํ•˜๋Š” ์—ญํ• 
  deleteById: (req, res) => {
    const idx = discussionsData.findIndex(
      (d) => d.id === Number(req.params.id)
    );
    if (idx !== -1) {
      discussionsData.splice(idx, 1);
      return res.status(202).send("resource deleted successfully");
    } else {
      return res.status(404).send("Not found");
    }
  },
};

module.exports = {
  discussionsController,
};


3. POST, PUT, DELETE ๋ฉ”์„œ๋“œ ํด๋ผ์ด์–ธํŠธ์— ๊ตฌํ˜„

์•„! ํด๋ผ์ด์–ธํŠธ๋Š” ๊ตฌํ˜„ํ•˜๋‹ค๊ฐ€ ์˜๋ฌธ์ ์ด ํ•˜๋‚˜ ๋“ค์—ˆ์—ˆ๋‹ค.

๋ฐ”๋กœ ๊ธฐ์กด Html ์‹œ์ ˆ ๋•Œ๋Š” script.js ์— submit ์‹œ, discussion ๋ฐฐ์—ด ๊ฐ€์žฅ ์•ž์— ์ถ”๊ฐ€ํ•ด์ฃผ๋Š”
์ฝ”๋“œ๊ฐ€ ์žˆ์—ˆ๋Š”๋ฐ ์ด๋ฒˆ์— ๊ตฌํ˜„ํ•˜๋ฉด์„œ ์ด ๋ถ€๋ถ„์ด ์„œ๋ฒ„์—๋Š” ์žˆ์œผ๋‚˜, ํด๋ผ์ด์–ธํŠธ์—์„œ ๋น ์กŒ๋˜ ๊ฒƒ์ด์—ˆ๋‹ค.

๊ทธ๋Ÿฌ๋ ค๋‹ˆ ํ•˜๊ณ  ๋„˜๊ธฐ๊ณ  ์žˆ์—ˆ๋Š”๋ฐ ํ™•์‹คํ•˜๊ฒŒ ์งš๊ณ  ๊ฐ€์•ผํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค.

๊ฒฐ๋ก ์€ '๋น ์ ธ๋„ ๋œ๋‹ค' ์˜€๊ณ , ์ด์œ ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

๊ธฐ์กด ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์ฝ”๋“œ์—์„œ๋Š” ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ์ง์ ‘ ๋ฐฐ์—ด ์•ž์— ํ† ๋ก ์„ ์ถ”๊ฐ€ํ•˜๊ณ  ์žˆ์—ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ๋ฆฌ์•กํŠธ๋กœ ๋ณ€๊ฒฝ๋œ ์ฝ”๋“œ์—์„œ๋Š” ์ƒํƒœ ๊ด€๋ฆฌ์™€ UI ๊ฐฑ์‹ ์„ ๋ฆฌ์•กํŠธ๊ฐ€ ๋‹ด๋‹นํ•˜๊ณ  ์žˆ์œผ๋ฉฐ (use ์‹œ๋ฆฌ์ฆˆ),
ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ๋ฐฐ์—ด ์•ž์— ํ† ๋ก ์„ ์ถ”๊ฐ€ํ•˜๋Š” ๋™์ž‘์€ ์ง์ ‘ ๊ตฌํ˜„๋˜์–ด ์žˆ์ง€ ์•Š๋‹ค.

  • ์—ญํ•  ๋ณ€๊ฒฝ: ์ง์ ‘ ๋ฐฐ์—ด ์•ž์— ํ† ๋ก ์„ ์ถ”๊ฐ€ โžก๏ธ ์ƒํƒœ ๊ด€๋ฆฌ์™€ UI ๊ฐฑ์‹ 

๋”ฐ๋ผ์„œ, ๋ฆฌ์•กํŠธ๋กœ ๋ณ€๊ฒฝ๋œ ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ๋ฐฐ์—ด ์•ž์— ํ† ๋ก ์„ ์ถ”๊ฐ€ํ•˜๋Š” ๋™์ž‘์€ ์„œ๋ฒ„ ์ธก์—์„œ ๊ตฌํ˜„๋˜์–ด์•ผ ํ•œ๋‹ค.

ํด๋ผ์ด์–ธํŠธ๋Š” ์„œ๋ฒ„์— ํ† ๋ก  ์ถ”๊ฐ€ ์š”์ฒญ์„ ๋ณด๋‚ด๊ณ ,
์„œ๋ฒ„์—์„œ ํ•ด๋‹น ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜์—ฌ ๋ฐฐ์—ด ์•ž์— ํ† ๋ก ์„ ์ถ”๊ฐ€ํ•˜๋Š” ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•œ๋‹ค.


1. App.js

import React, { useState, useEffect } from "react";
import "./App.css";
import axios from "axios";
import { Form, Discussions } from "./components";

const App = () => {
  const [discussions, setDiscussions] = useState([]);
  
  // ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋ฅผ useState๋กœ ์ƒํƒœ ๊ด€๋ฆฌ
  const [error, setError] = useState(null);
  
  // ์ƒˆ๋กœ์šด ํ† ๋ก ์„ ์ถ”๊ฐ€ํ•˜๋Š” ์—ญํ• 
  // title (์ œ๋ชฉ), author (์ž‘์„ฑ์ž), bodyText (๋ณธ๋ฌธ ํ…์ŠคํŠธ)๋ฅผ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ๋ฐ›๋Š”๋‹ค.
  // ์ƒˆ๋กœ์šด ํ† ๋ก  ๋ฐ์ดํ„ฐ๋ฅผ ์ƒ์„ฑํ•˜๊ณ , ์ƒ์„ฑ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์„œ๋ฒ„๋กœ ์ „์†กํ•œ๋‹ค.
  // ๋งŒ์•ฝ ์š”์ฒญ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋˜๋ฉด getDiscussion ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์—…๋ฐ์ดํŠธ๋œ ํ† ๋ก  ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
  const addDiscussion = ({ title, author, bodyText }) => {
    const newDiscussionData = {
      title,
      author,
      bodyHTML: bodyText,
    };
    axios
      .post("http://localhost:4000/discussions/", newDiscussionData)
      .then((response) => {
        if (response.status === 201) {
          getDiscussion();
        }
      })
      .catch((error) => {
        setError("Failed to add discussion.");
      });
  };
  // ๋ชจ๋“  ํ† ๋ก  ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜ค๋Š” ์—ญํ• 
  // ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ํ† ๋ก  ๋ชฉ๋ก์„ ๊ฐ€์ ธ์™€์„œ response.data์— ์ €์žฅํ•˜๊ณ , ์ด๋ฅผ ์ƒํƒœ์— ์„ค์ •
  const getDiscussion = () => {
    axios
      .get("http://localhost:4000/discussions/")
      .then((response) => {
        setDiscussions(response.data);
      })
      .catch((error) => {
        setError("Failed to fetch discussions.");
      });
  };
  // ์ฃผ์–ด์ง„ id์— ํ•ด๋‹นํ•˜๋Š” ํ† ๋ก ์„ ์‚ญ์ œํ•˜๋Š” ์—ญํ• 
  // ์„œ๋ฒ„์— ์‚ญ์ œ ์š”์ฒญ์„ ๋ณด๋‚ด๊ณ , ์š”์ฒญ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋˜๋ฉด getDiscussion ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์—…๋ฐ์ดํŠธ๋œ ํ† ๋ก  ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜จ๋‹ค.
  const deleteDiscussion = (id) => {
    axios
      .delete(`http://localhost:4000/discussions/${id}`)
      .then((response) => {
        if (response.status === 202 || response.status === 204) {
          getDiscussion();
        }
      })
      .catch((error) => {
        setError("Failed to delete discussion.");
      });
  };
  // ๋ Œ๋”๋ง๋  ๋•Œ์™€ ์˜์กด์„ฑ ๋ฐฐ์—ด์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ์‹คํ–‰
  // ์ดˆ๊ธฐ ๋ Œ๋”๋ง ์‹œ์— getDiscussion ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์ดˆ๊ธฐ ํ† ๋ก  ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ
  useEffect(() => {
    getDiscussion();
  }, []);

  return (
    <main>
      <div className="form__info">
        <h1>My Agora States</h1>
        <Form addDiscussion={addDiscussion} />
      </div>
      <div className="form__active">
        {error ? (
          <div className="error">{error}</div>
        ) : (
          <Discussions
            discussions={discussions}
            deleteDiscussion={deleteDiscussion}
          />
        )}
      </div>
    </main>
  );


4. ์„œ๋ฒ„ ์ž‘๋™ ์‹œ, ์ •์ƒ์ ์œผ๋กœ ํด๋ผ์ด์–ธํŠธ์˜ ๋ชจ๋“  ๋ฉ”์„œ๋“œ๊ฐ€ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธ



๐Ÿ’ก Error Note.


๐Ÿ’ก ์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ์˜ Mock ๋ฐ์ดํ„ฐ ์ฐจ์ด

  • ์„œ๋ฒ„ ๊ป๋‹ค ํ‚ค๋ฉด ๋ฉ”์„œ๋“œ ์ž‘์—…์„ ํ–ˆ๋˜ ๋ถ€๋ถ„์ด ์ดˆ๊ธฐํ™”๋˜๋Š”๋ฐ ์ด๊ฒŒ mock์ด๋ž‘ ๋ญ๊ฐ€ ๋‹ค๋ฅธ์ง€ ์˜๋ฌธ์ด ์ƒ๊ฒผ๋‹ค.
  • ํ˜„์žฌ๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋‚˜ ํŒŒ์ผ ์‹œ์Šคํ…œ๊ฐ™์€ ์˜๊ตฌ ์ €์žฅ์†Œ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์ง€ ์•Š๊ณ ,
    agoraStatesDiscussions ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€์„œ ๊ทธ๋ ‡๋‹ค.
  • ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋‚˜ ํŒŒ์ผ ์‹œ์Šคํ…œ๊ฐ™์€ ์˜๊ตฌ ์ €์žฅ์†Œ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด, ์„œ๋ฒ„์˜ ์ง„์งœ ์กด์žฌ ์ด์œ ๊ฐ€ ์ƒ๊ธด๋‹ค.

๐Ÿ’ก ์„œ๋ฒ„์˜ ๋Œ€ํ‘œ์ ์ธ ์—ญํ•  ์˜ˆ์‹œ

  • ํด๋ผ์ด์–ธํŠธ์—์„œ ์š”์ฒญํ•œ ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•˜๊ณ , ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌํ•˜๋ฉฐ, ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ ์ ˆํ•œ ์‘๋‹ต์„ ์ œ๊ณตํ•˜๋Š” ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.
  1. ์‚ฌ์šฉ์ž ๋“ฑ๋ก: ํด๋ผ์ด์–ธํŠธ์—์„œ ์‚ฌ์šฉ์ž๊ฐ€ ํšŒ์› ๊ฐ€์ž…์„ ์š”์ฒญํ•˜๋ฉด, ์„œ๋ฒ„๋Š” ํ•ด๋‹น ์š”์ฒญ์„ ๋ฐ›์•„์„œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅํ•˜๊ณ , ์ ์ ˆํ•œ ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

  2. ๋ฐ์ดํ„ฐ ์ €์žฅ: ํด๋ผ์ด์–ธํŠธ์—์„œ ์ƒ์„ฑํ•œ ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋ฅผ ์„œ๋ฒ„๋กœ ์ „์†กํ•˜๋ฉด, ์„œ๋ฒ„๋Š” ํ•ด๋‹น ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋‚˜ ํŒŒ์ผ ์‹œ์Šคํ…œ์— ์ €์žฅํ•˜๊ณ , ์ €์žฅ ์™„๋ฃŒ ํ›„ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ ์ ˆํ•œ ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

  3. ์ธ์ฆ๊ณผ ๊ถŒํ•œ ๋ถ€์—ฌ: ํด๋ผ์ด์–ธํŠธ์—์„œ ๋กœ๊ทธ์ธ ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉด, ์„œ๋ฒ„๋Š” ์‚ฌ์šฉ์ž์˜ ์ธ์ฆ์„ ๊ฒ€์ฆํ•˜๊ณ , ์œ ํšจํ•œ ์ธ์ฆ์ด๋ฉด ์•ก์„ธ์Šค ํ† ํฐ์„ ๋ฐœ๊ธ‰ํ•˜์—ฌ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ œ๊ณตํ•œ๋‹ค. ๋˜ํ•œ, ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋ณด์œ ํ•œ ์•ก์„ธ์Šค ํ† ํฐ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์š”์ฒญ๋œ ์ž‘์—…์— ๋Œ€ํ•œ ๊ถŒํ•œ์„ ๊ฒ€์‚ฌํ•˜๊ณ , ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ์ž‘์—…์— ๋Œ€ํ•ด์„œ๋Š” ์ ์ ˆํ•œ ๊ถŒํ•œ ์˜ค๋ฅ˜ ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

  4. ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์ฒ˜๋ฆฌ: ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์š”์ฒญํ•œ ์ž‘์—…์— ๋Œ€ํ•ด ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•  ๊ฒฝ์šฐ, ์„œ๋ฒ„๋Š” ํ•ด๋‹น ๋กœ์ง์„ ์‹คํ–‰ํ•˜๊ณ  ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์ƒํ’ˆ ๊ตฌ๋งค ์š”์ฒญ์— ๋Œ€ํ•ด์„œ๋Š” ์žฌ๊ณ  ์ฒดํฌ, ๊ฒฐ์ œ ์ฒ˜๋ฆฌ, ๋ฐฐ์†ก ์ •๋ณด ์—…๋ฐ์ดํŠธ ๋“ฑ์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.

profile
์•„์ด๋””์–ด๊ฐ€ ๋„˜์น˜๋Š” ํ”„๋ก ํŠธ์—”๋“œ๋ฅผ ๊ฟˆ๊ฟ‰๋‹ˆ๋‹ค ๐Ÿ”ฅ

0๊ฐœ์˜ ๋Œ“๊ธ€