My Boundary As Much As I Experienced

Express, ejs탬플릿과 MongoDB으로 게시판 CRUD 구현 본문

BackEnd/Node.js

Express, ejs탬플릿과 MongoDB으로 게시판 CRUD 구현

Bumang 2024. 7. 29. 02:28

express로 CRUD 구현하기

시용하는 미들웨어:

const express = require("express"); // Express 웹 애플리케이션 프레임워크
const app = express(); // Express 애플리케이션 인스턴스 생성
const methodOverride = require("method-override"); // HTTP 메서드 오버라이드를 위한 미들웨어

const { MongoClient, ObjectId } = require("mongodb"); // MongoDB와 상호작용하기 위한 MongoDB 드라이버

// 정적 파일들을 제공하기 위해 public 폴더를 사용하도록 설정.
app.use(express.static(__dirname + "/public")); 

// EJS를 템플릿 엔진으로 설정.
app.set("view engine", "ejs");

// JSON 형식의 본문을 파싱하기 위한 미들웨어 설정.
app.use(express.json()); 

// Form 형식으로 제출된, URL 인코딩된 본문을 JSON으로 파싱하기 위한 미들웨어 설정.
// 요샌 다 AJAX 요청을 하지만.. 만약을 위해서라도 해두는게 좋다.
app.use(express.urlencoded({ extended: true }));

// HTML 폼에서 PUT, DELETE 같은 HTTP 메서드를 사용할 수 있게 해주는 미들웨어 설정
app.use(methodOverride("_method"));

 

시용하는 DB(MongoDB):

연결 실패 시 fallback으로 포트를 열어주긴 하지만 db조회가 필요한 api들은 사용할 수 없다.

let db;
const url =
  "mongodb+srv://...";
const client = new MongoClient(url);
async function run() {
  await client
    .connect()
    .then((client) => {
      console.log("DB 연결 성공");
      db = client.db("forum");

      // 내 컴퓨터에서 포트 하나 오픈하는 코드..
      app.listen(8080, () => {
        console.log("http://localhost:8080에서 서버 실행 중");
      });
    })
    .catch((err) => {
      console.log(err);
      console.log("error");

      app.listen(8080, () => {
        console.log("http://localhost:8080에서 서버 실행 중 (fallback)");
      });
    });
}

run();

 

1. 게시판 조회

아주 기본적인 get요청:

get요청을 보내면 res객체의 send, sendFile 등으로 응답할 수 있다.

// 이런 것들이
app.get("/", (req, res) => {
  // __dirname은 현재 프로젝트 절대경로. package.json이 설치된 루트?
  res.sendFile(__dirname + "/index.html");
});

// 다 API만든거임.
app.get("/news", (req, res) => {
  db.collection("post").insertOne({ title: "어쩌구" });
  res.send("뉴스");
});

// 각종 다양한 페이지들 get요청 처리
app.get("/shop", (req, res) => {
  res.send("쇼핑 페이지입니다~!");
});

// 각종 다양한 페이지들 get요청 처리
app.get("/about", (req, res) => {
  res.sendFile(__dirname + "/about.html");
});

 

DB조회가 필요한 페이지 get요청의 경우:

리스트 페이지 같은 경우엔 db조회를 해야한다.

"post"라는 콜렉션을 모두 조회해서(find) 문서들을 배열화(toArray)했다.

ejs템플릿을 사용한다면 render 메소드로 렌더할 수 있다.

이때, 두 번째 파라미터 객체에 동적으로 할당할 데이터들을 넣어줄 수 있다.

app.get("/list", async (req, res) => {
  const col = db.collection("post");
  const doc = await col.find().toArray();

  // 기본 경로가 views폴더로 되어있기 때문에 굳이 '/views/...'를 안 써줘도 된다.
  // 첫 번째는 템플릿 에진 경로, 두 번째 파라미터는 보낼 데이터(주로 db조회한 결과값...)
  res.render("list.ejs", { doc: doc });
});

 

동적패러미터를 다루는 방식:

:id같이 : 뒤에 패러미터를 기입한다.

이는 req.params에서 추출하여 사용할 수 있다.

아주 민감한 정보는 url에 넣지 않고 body에 담아주는 것이 좋다.

app.get("/detail/:id", async (req, res) => {
  // ejs에 변수 주입
  let result;
  try {
    result = await db
      .collection("post")
      .findOne({ _id: new ObjectId(req.params.id) });
    console.log(result, "result");
  } catch (err) {
    return;
  }

  res.render("detail.ejs", { title: result.title, content: result.content });
});

 

2. 게시글 작성

사용자가 입력한 정보를 req의 body에서 객체형식으로 파싱한 후

collection에 insertOne 메소드로 주입해주면 된다.

클라이언트에 status를 설정해주고 message를 날려준다.

// 글쓰기 페이지 단순 get요청
app.get("/write", (req, res) => {
  res.render("write.ejs");
});

// '/write'로 post요청을 받았을 때
app.post("/write", async (req, res) => {
  try {
    if (req.body.content === "" || req.body.title === "") {
      throw new Error("no Title or no Content");
    }
    const col = db.collection("post");

    // req의 body의 정보들을 객체화한다.
    // express.json() 라이브러리 덕분에 req.body....로 접근 가능하다.
    const temp = { title: req.body.title, content: req.body.content };
    // insertOne으로 temp객체 주입
    const insert = await col.insertOne(temp);
    // 클라이언트에 status를 설정해주고 message를 날려준다.
    res.status(201).json({ message: "Item successfully saved!" });
  } catch (err) {
    if (err.message === "no Title or no Content") {
      res
        .status(400)
        .json({ message: "empty title or content are not allowed" });
    }
  }
});

 

3. 게시글 수정

수정 페이지:

get요청으로 수정 페이지에 접근할 때,

본문 내용을 채워주기 위해 findOne메소드로 id를 조회한다.

 

수정 요청:

updateOne메소드를 사용한다. 

const updateOne = await col.updateOne(filter, temp);에서

첫 번째 인자는 수정할 타겟, 두 번째 인자는 업데이트할 내용이다.

 

먼저, 첫 번째 인자이다.

const filter = { _id: new ObjectId(req.params.id) };

$gt, $gte, $lt, $lte 등으로 수량을 기준으로 조회할 수 있다.

 

두 번째 인자는 업데이트할 내용이다.

const temp = { $set: { title: req.body.title, content: req.body.content } };

$set, $inc, $mul, $unset 등의 키로 수정할 수 있다.

app.get("/modify/:id", async (req, res) => {
  // ejs에 변수 주입
  let result;
  try {
    // id가 같은 문서 하나를 조회.
    result = await db
      .collection("post")
      .findOne({ _id: new ObjectId(req.params.id) });
    console.log(result, "result");
  } catch (err) {
    return;
  }

  res.render("modify.ejs", {
    title: result.title,
    content: result.content,
    _id: req.params.id,
  });
});

// patch함수로 조회할 때
app.patch("/modify/:id", async (req, res) => {
  try {
    if (req.body.content === "" || req.body.title === "") {
      throw new Error("no Title or no Content");
    }
    const col = db.collection("post");
    const filter = { _id: new ObjectId(req.params.id) };
    // $set은 특정값으로 바로 세팅하는 키이다.
    // 넘버 값을 다룰 때 $set말고
    // $inc를 쓰면 기존값 +/- 를 할 수 있다.
    // $mul을 쓰면 기존값 * n을 할 수 있다.
    // $unset을 쓰면 필드를 삭제할 수 있다.
    // updateMany를 쓰면 해당하는 것들을 모두 바꿀 수 있다.
    // like가 10개 이상인 것들을 모두 선택하려면? { like: {gt: 10} }
    //  *gt는 greater than의 약자
    //  *gte는 greater than or equal의 약자
    //  *lt, lte도 less than, less than equal이다.
    //  *ne는 not equal.
    const temp = { $set: { title: req.body.title, content: req.body.content } };
    console.log(temp, "temp");

    const updateOne = await col.updateOne(filter, temp);
    console.log(updateOne);
    res.status(201).json({ message: "Item successfully saved!" });
  } catch (err) {
    if (err.message === "no Title or no Content") {
      res
        .status(400)
        .json({ message: "empty title or content are not allowed" });
      return;
    }
    console.log(err);
  }
});

 

4. 게시글 삭제

수정과 비슷하게 특정 문서를 찾은 다음에 deleteOne으로 삭제하면 된다.

app.delete("/delete/:id", async (req, res) => {
  const col = db.collection("post");
  const filter = { _id: new ObjectId(req.body["id"]) };

  const result = await col.deleteOne(filter);

  console.log(filter);

  if (result.deletedCount === 1) {
    console.log("Successfully deleted one document.");
    res.json({ success: true });
  } else {
    console.log("No documents matched the query. Deleted 0 documents.");
    res.json({ success: false });
  }
});

 

오히려 클라이언트 단에서 문서가 삭제된 후

새로고침을 하지 않아도 그 문서를 삭제해주는 로직을 설정해주는 것이 중요하다.

이를 위해 Ajax를 사용하는 것이 좋다.

  <script>
    const deleteEls = document.querySelectorAll(".delete");
    for (let i = 0; i < deleteEls.length; i++) {
      deleteEls[i].addEventListener("click", (e) => {
        e.preventDefault();
        fetch(`/delete/${i}`, {
          method: "DELETE",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({ id: e.target.dataset.id }),
        })
          .then((response) => response.json())
          .then((data) => {
            if (data.success) {
              deleteEls[i].closest("div.list-box").remove();
            }
          });
      });
    }
  </script>