나만의 아고라스테이츠 만들기 - 2

강성일·2023년 6월 7일
1
post-thumbnail

🗣️ Description


Section2에서 배운 내용을 바탕으로 나만의 아고라 스테이츠 서버를 만듭니다.

코드스테이츠 fe-sprint-my-agora-states-server Repository에서 자신의 Repository로 fork후 과제 진행합니다.



🔥 Bare Minimum Requirement Self Checklist


[✅] my-agora-states-server/app.js

  • 모든 Origin, 경로에 대해 CORS 요청을 허용하게 미들웨어를 적용합니다.
  • POST 요청 등에 포함된 body(payload)를 구조화하기 위한 미들웨어를 적용합니다.
  • 서버 상태 확인을 위해 / 에서 상태 코드 200으로 응답합니다.
  • discussionsRouter 를 이용하여 /discussions 경로로 라우팅합니다.

[✅] my-agora-states-server/router/discussions.js

  • GET /discussions
  • GET /discussions/:id

[✅] my-agora-states-server/controller/index.js

  • discussionsController.findAll
  • discussionsController.findById

[✅] my-agora-states-server 과제 제출 (Pull request)

  • Pull request로 과제 제출

[✅] my-agora-states-server 시작

  • package.json 을 참고하여 나만의 아고라 스테이츠 서버를 로컬 환경에서 실행합니다.

[✅] my-agora-states와 연동하기

  • my-agora-states-server가 켜져있는지 확인합니다.
  • 로컬 환경에서 실행한 나만의 아고라 스테이츠 서버에서 discussions 데이터를 조회합니다.


🔥 Optional Checklist


[✅] my-agora-states-in-react

  • create-react-app으로 프로젝트 생성
  • 기존에 만든 나만의 아고라 스테이츠를 React로 옮기기
    • 디스커션 나열 기능
  • 로컬 환경에서 실행한 나만의 아고라 스테이츠 서버에서 discussions 데이터를 조회합니다.


💬 회고


목차

1. 초기 세팅

2. 과정을 기록하기

3. Error note



⚙️ 초기 세팅


// 💬 주어진 코드 (app.js)

const express = require('express');
const app = express();

const cors = require('cors');
const morgan = require('morgan');

// morgan 미들웨어가 세팅되어 있습니다.
// HTTP 요청 logger를 편리하게 사용할 수 있는 미들웨어 입니다.
app.use(morgan('tiny'));

// TODO: cors를 적용합니다.

// TODO: Express 내장 미들웨어인 express.json()을 적용합니다.


const port = 4000;
const discussionsRouter = require('./router/discussions');

// TODO: app.use()를 활용하여 /discussions 경로로 라우팅합니다. 


app.get('/', (req, res) => {
  // 서버 상태 확인을 위해 상태 코드 200과 함께 응답을 보냅니다.
  res.status(200).send('fe-sprint-my-agora-states-server');
});

const server = app.listen(port, () => {
  console.log(`[RUN] My Agora States Server... | http://localhost:${port}`);
});
module.exports.app = app;
module.exports.server = server;
// 💬 주어진 코드 (index.js)

const { agoraStatesDiscussions } = require("../repository/discussions");
const discussionsData = agoraStatesDiscussions;
const discussionsController = {
  findAll: (req, res) => {
    // TODO: 모든 discussions 목록을 응답합니다.
    res.send('TODO:')
  },

  findById: (req, res) => {
    // TODO: 요청으로 들어온 id와 일치하는 discussion을 응답합니다.
    res.send('TODO:')
  }

};

module.exports = {
  discussionsController,
};
// 💬 주어진 코드 (discussions.js)

// TODO: discussions 라우터를 완성합니다.
const { discussionsController } = require('../controller');
const { findAll, findById } = discussionsController;
const express = require('express');
const router = express.Router();

// TODO: 모든 discussions 목록을 조회하는 라우터를 작성합니다.


// TODO: :id에 맞는 discussion을 조회하는 라우터를 작성합니다.


module.exports = router;


📝 과정을 기록하기


오늘 과제는 원래 아고라스테이츠 서버 구현이었다.

서버 구현은 아주 무난하게 끝났고, 이걸 기존 클라이언트 서버와 합치면 어떨까 하는 생각이 들었다.
한 파일 안에서 서버 2개를 열고, 동시에 돌리면 뭔가 멋있을 것(?) 같았다.

단순한 생각에서 시작했지만, 과정은 정말 쉽지 않았고 한 10시간은 투자한 것 같다.
결국 Post 기능을 포기하게 됐고, 사서 고생한 결과물이 만족스럽게 나왔다.

클라이언트 코드 파일에 서버 파일을 merge 하는 데에 성공했고,
정상적으로 데이터를 가져와서 discussion 들을 나열하는 것까지 성공했다.


1. 서버 구현하기

// ✅ 구현한 코드 (app.js)

const express = require("express");
const app = express();

const cors = require("cors");
const morgan = require("morgan");

// morgan 미들웨어가 세팅되어 있습니다.
// HTTP 요청 logger를 편리하게 사용할 수 있는 미들웨어 입니다.
app.use(morgan("tiny"));

// TODO: cors를 적용합니다.

app.use(cors());

// TODO: Express 내장 미들웨어인 express.json()을 적용합니다.

app.use(express.json());

const port = 4000;
const discussionsRouter = require("./router/discussions");

// TODO: app.use()를 활용하여 /discussions 경로로 라우팅합니다.

app.use("/discussions", discussionsRouter);

app.get("/", (req, res) => {
  // 서버 상태 확인을 위해 상태 코드 200과 함께 응답을 보냅니다.
  res.status(200).send("fe-sprint-my-agora-states-server");
});

app.use((req, res, next) => {
  res.status(404).send("Not Found!");
});

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send({
    message: "Internal Server Error",
    stacktrace: err.toString(),
  });
});

const server = app.listen(port, () => {
  console.log(`[RUN] My Agora States Server... | http://localhost:${port}`);
});
module.exports.app = app;
module.exports.server = server;
// ✅ 구현한 코드 (index.js)

const { agoraStatesDiscussions } = require("../repository/discussions");
const discussionsData = agoraStatesDiscussions;
const discussionsController = {
  findAll: (req, res) => {
    // TODO: 모든 discussions 목록을 응답합니다.
    const { author } = req.query;
    if (author) {
      res.json(discussionsData.filter((item) => item.author === author));
    } else {
      res.json(discussionsData);
    }
  },

  findById: (req, res) => {
    // TODO: 요청으로 들어온 id와 일치하는 discussion을 응답합니다.
    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." });
      }
    }
  },
};

module.exports = {
  discussionsController,
};
// ✅ 구현한 코드 (discussions.js)

// TODO: discussions 라우터를 완성합니다.
const { discussionsController } = require("../controller");
const { findAll, findById } = discussionsController;
const express = require("express");
const router = express.Router();

// TODO: 모든 discussions 목록을 조회하는 라우터를 작성합니다.

router.get("/", findAll);

// TODO: :id에 맞는 discussion을 조회하는 라우터를 작성합니다.

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

module.exports = router;


2. Server & Client ♻️


기존 클라이언트 파일에 서버 파일을 합치는 과정은 다음과 같았다.

  1. 기존 클라이언트 파일에서 데이터를 가져오던 mock 파일 삭제
  2. 큰 틀로 사용할 폴더 2개 생성 (Server, Client)
  3. Client는 React 식으로 리팩토링(useState, useEffect, map), Server는 그대로 가져오기
  4. Client에 Axios로 실제 서버 주소 Get 해오기
  5. Client 디테일한 코드(discussion 컴포넌트), CSS 코드 리팩토링

// ♻️ React 식으로 리팩토링한 코드 (Client - App.js)

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

const DiscussionItem = ({ avatarUrl, title, author, createdAt, answer }) => {
  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={title}>
            {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>
    </li>
  );
};

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

  useEffect(() => {
    axios
      .get("http://localhost:4000/discussions")
      .then((response) => {
        setDiscussions(response.data);
      })
      .catch((error) => {
        console.error("Error fetching discussions:", error);
      });
  }, []);

  return (
    <main>
      <div className="form__info">
        <h1>My Agora States</h1>
        <section className="form__container">
          <form action="" method="get" className="form">
            <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>
      </div>
      <div className="form__active">
        <section className="discussion__wrapper">
          <ul className="discussions__container">
            {discussions.map((discussion) => (
              <DiscussionItem
                key={discussion.id}
                avatarUrl={discussion.avatarUrl}
                title={discussion.title}
                author={discussion.author}
                createdAt={discussion.createdAt}
                answer={discussion.answer}
              />
            ))}
          </ul>
        </section>
      </div>
    </main>
  );
};

export default App;


3. Build


💡 Error Note.


💡 find와 filter의 차이

  • 요청으로 들어온 id와 일치하는 discussion에 응답하는 메서드를 filter로 구현했었다.
  • filter와 find는 배열에서 원하는 항목을 찾는 데 사용되는 JavaScript 배열 메서드이지만 서로 다르다.
  • filter 메서드는 주어진 조건을 만족하는 모든 항목을 찾아서 새로운 배열로 반환한다.
  • find는 조건을 만족하는 첫 번째 항목을 찾으면 검색을 멈추고 해당 항목을 반환하며, 없을 경우 undefined를 반환한다.
  • 따라서 개인 정보를 하나만 찾아서 객체로 표현하고자 할 때는 find 메서드가 적합하다.

💡 프로토콜 주의 !

  • 처음에 Client에 Server url을 http가 아닌 https로 연결했다가 고생 좀 했다..
  • SOP를 직접 몸소 체험해버린 경험이었다.

💡 dangerouslySetInnerHTML

  • dangerouslySetInnerHTML는 React에서 사용되는 특수한 prop이다.
  • 문자열 형식으로 제공된 HTML 코드를 직접 React 컴포넌트에 주입할 수 있다.
  • JSX를 사용하여 HTML을 작성하는 것보다 외부에서 가져온 HTML 문자열을 직접 사용할 때 사용한다.
  • function MyComponent() {
      const htmlString = '<div>Hello, <strong>World!</strong></div>';
      return <div dangerouslySetInnerHTML={{ __html: htmlString }} />;
    }

💡 axios

  • HTTP 요청 보내기: Axios는 GET, POST, PUT, DELETE와 같은 다양한 HTTP 요청 메서드를 지원한다.

  • 요청 및 응답 인터셉트: Axios는 요청과 응답을 인터셉트하고 수정하는 기능을 제공한다.
    이를 통해 요청 전에 인증 토큰을 추가하거나 응답을 가공하는 등의 작업을 수행할 수 있다.

  • 요청과 응답 데이터 변환: Axios는 요청과 응답 데이터를 자동으로 변환하는 기능을 제공한다.
    JSON, XML, FormData 등 다양한 데이터 형식을 자동으로 처리할 수 있다.

  • 요청 취소: Axios는 요청을 취소하는 기능을 제공한다.
    이를 통해 사용자가 요청을 취소하거나 중단할 수 있다.

  • 에러 처리: Axios는 HTTP 요청 중 발생하는 에러를 캐치하고 처리할 수 있는 기능을 제공한다.
    네트워크 오류, 타임아웃, 응답 상태 코드 등의 에러를 처리할 수 있다.



✅ ToDo


  1. discussion 눌렀을 때, URL 제대로 수정

  2. discussion 눌렀을 때, 해당 컴포넌트 제외하고 모두 숨김처리

  3. POST, DELETE 기능 추가

  4. CSS 리팩토링

profile
아이디어가 넘치는 프론트엔드를 꿈꿉니다 🔥

0개의 댓글