본문 바로가기
생각정리

WIL(Weekly I Learned) , CRUD구현 후기

by 물고기고기 2021. 3. 29.

드디어 얼레벌레 뚝딱만들기위한 웹페이지는 끝이나고 본격적으로 백엔드를 배우기 시작했다. 그 중 선택한 언어는 node js인데 이를 고른 이유는 풀스텍 개발자가 되기에 더 빠른길 같아서..였다. 자바스프링으로 백엔드를 시작하면 프론트까지 하기엔 너무 멀리가버릴것 같아서.. 였다.(결론은 자바스크립트만으로 모든걸 끝내고 싶다는 의미)

 

저번주에도 WIL을 적었어야했는데 프로젝트하느냐고 정신이 팔려서 적는걸 잊어버렸다.. 모쪼록 이번주엔 nodejs로 구현한 CRUD페이지 후기와 그 다음프로젝트를 위한 선수지식을 리뷰해보도록하겠다.

 

우선 CRUD기능만 있는 페이지는 밑의 깃허브 주소로 가면 볼 수 있다.

github.com/silano08/TIL-Today_I_Learnd-/tree/main/nodejs/project

 

silano08/TIL-Today_I_Learnd-

Contribute to silano08/TIL-Today_I_Learnd- development by creating an account on GitHub.

github.com

그리고 AWS에 투고했다.. 도메인..은 못붙였지만

http://13.125.206.190/home

투고된 링크

 

1. CRUD

백엔드 개발을 할 수 있냐?의 시작점은 CRUD를 스스로 구현할 수 있냐라고들 한다. 이를 위해서 필요한 API, 그리고 최소한의 보안을 위해 데이터를 받아올때 POST와 GET중 어느것을 써야하는지를 구분해야한다.

 

이번 CRUD에선 nodejs중 express를 메인으로 ejs템플릿 + 몽고DB + 몽고DB 라이브러리 몽구스를 사용했다.

 

거두절미하고 내가 메인으로 개발한 CRUD 코드 리뷰를 짤막하게 해보자면,

(veiw파일 등 화면을 띄우기위한 html코드는 생략한다.)

const express = require('express')
const app = express()
const port = 3000

// 미들웨어
app.use(express.json());
app.use(express.urlencoded({ extended: false }))
app.use(express.static('public'));

// ejs
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');


// DB연결코드
// schemas파일로 분할함
const connect = require('./schemas');
connect();

// post API 서버 불러오기
const postRouter = require("./routers/post");
app.use("/api", [postRouter]);

app.get('/home', (req, res) => {
    res.render('index');
})

app.get('/post', (req, res) => {
    res.render('post');
})

app.get('/detail', (req, res) => {
    res.render('viewpost');
})
 
app.get('/edit', (req, res) => {
    res.render('revise');
})


app.listen(port, () => {
  console.log(`listening at http://localhost:${port}`)
})

nodejs(express) 기초 강의를 들었으나 강의를 보며 따라하기는 싫어서(애초에 겹치는 부분이 없기도했고) 처음부터 끝까지 내가 싹다 코딩하느라 가독성에 신경을 쓴 코드이다.

 

우선 미들웨어나 ejs의 존재같은 것은 이후 설명하고 DB와 API의 기능에 집중을해야한다.

 

작업 순서는

DB설정(스키마셋팅) > 페이지별 UI 작업 > 게시글 POST API개발(DB에 게시글이 들어가야 이후 메인페이지 업데이트 기능을 추가했을때 잘추가됐는지 확인이 편하다) > 메인페이지 게시글 업데이트 API개발 > 상세페이지 조회 GET API 개발 > 상세페이지를 받아왔다면 특정데이터에 접근하는 API만들어둔 것이니 비슷한방식으로 수정/삭제기능을 개발하면 된다.

 

DB설정(몽구스 라이브러리 사용)

더보기
// 몽고디비 연결코드
const mongoose = require("mongoose");
 
const connect = () => {
mongoose
.connect("mongodb://localhost:27017/admin", {
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true,
ignoreUndefined: true,
user:"test",
pass:"test"
})
.catch(err => console.log(err));
};
 
mongoose.connection.on("error", err => {
console.error("몽고디비 연결 에러", err);
});
 
module.exports = connect;
const mongoose = require("mongoose");

const { Schema } = mongoose;
const postSchema = new Schema({
  username: {
    type: String,
    required: true
  },
  title: {
    type: String,
    required: true
  },
  date: {
    type: Date, 
    default: Date.now
  },
  password: {
    type: Number
  },
  mainpost: {
    type: String
  }
});



module.exports = mongoose.model("post", postSchema);

몽고DB는 SQL과는 다르게 columns의 제한없이 어떤 데이터도 넣을 수 있다는 특징을 가지고 있다. 그러나 대규모 서비스를 운영하게된다면 이렇게 아무런 데이터나 넣게되는현상이 마냥 좋지만은 않다. 이유는 많이 있는데 굳이 예시를 들자면 필요없는 데이터가 잔뜩 들어온다면 공간낭비가 될수도 있고 필요한 데이터가 없다면 개인별 페이지를 보여주지 못하는 경우가 생길수도 있기때문이다. 이를 위해 등장한 것이 몽구스라이브러리이다.

몽구스 라이브러리는 스키마를 통해 우리가 받아줘야할 데이터가 무엇인지 미리 정해두게한뒤, 이를 활용해 데이터를 형식적으로 받을 수 있게 만들어준다.

 

게시글 POST API개발(DB에 게시글이 들어가야 이후 메인페이지 업데이트 기능을 추가했을때 잘추가됐는지 확인이 편하다)

router.post('/posts', async (req, res) => {
  const { username, title, password, mainpost } = req.body;
  Posts.create({ username, title, password, mainpost });
  console.log("sssssssss");
  res.send({ result: "success" });
});

이를 보면 /post라는 URL을 통해 기능을 구현함을 볼 수 있다. 게시글 작성 html에서 id값을 기준으로 jquery를 사용해 값을 받아오고 변수에 저장한뒤 이를 ajax를 사용해 api로 넘겨주는 구조이다.

 function upload() {
            let title = $('#title').val()
            let username = $('#reg_id').val()
            let content = $('#content').val()
            let password = $('#password').val()
            $.ajax({
                type: "POST",
                url: "/api/posts",
                data: {title:title,username:username,password:password,mainpost:content},
                success: function () {
                    alert("저장되었습니다!");
                }
            })
            window.location.href = "/home";
        }

프론트단은 이런느낌인셈.

 

메인페이지 게시글 조회 개발

 

router.get("/posts/:_id", async (req, res) => {
  const { _id } = req.params;
  post = await Posts.findOne({ _id: _id });
  res.json({ detail: post });
});
<!DOCTYPE html>
<html lang="en">

<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
        integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <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>
    <title>게시판</title>

    <script>
        $(document).ready(function () {
            get_posts();
        })
        function get_posts(username) {
            $("#postList").empty()
            console.log(username)
            $.ajax({
                type: "GET",
                url: `/api/posts${username ? "?username=" + username : ""}`,
                data: {},
                success: function (response) {
                    let post = response["post"]
                    for (let i = 0; i < post.length; i++) {
                        make_card(post[i])
                    }
                }
            })
        }
        function make_card(item) {
            let htmlTemp = `<tbody onclick="location.href='/detail?_id=${item["_id"]}'">
                <tr>
                    <th scope="row">${item["title"]}</th>
                    <td>${item["username"]}</td>
                    <td>${item["date"]}</td>
                </tr> 
            </tbody>`
            $("#postList").append(htmlTemp)
        }
        function popo(){
            window.location.href = "/post";
        }
    </script>

메인페이지에서 사용된 함수이다. 데이터를 불러와 각 게시글마다 몽고DB에서 자체적으로 제공해주는 _id값을 URL로 부여해주고 이를 상세페이지단작업에서 활용한다. 그리고 ready함수를 통해 다 준비가 되었으면 get_posts로 게시글 데이터마다 htmltemp를 부여해 자동업데이트를 해준다.

 

상세페이지 조회 GET API 개발

 

이때부터 작업난이도가 제법 올라간다. 왜그러냐면 게시글마다 개별화를 해줘야하는데 전부불러오는게 아니라 특정한 값을 기준으로 데이터를 불러오는 API가 필요해지기 때문이다. 라고 생각했는데 제법 간단했다. 사실 우리는 전체게시글조회페이지에서 개별 게시글마다 URL을 부여해주는 작업을 이미 끝냈는데 get api를 재활용하면되는문제였다.

const queryString = window.location.search;
        const urlParams = new URLSearchParams(queryString);
        const _id = urlParams.get("_id");

이렇게 URL에 있는 _id값을 불러와준다.

router.get("/posts/:_id", async (req, res) => {
  const { _id } = req.params;
  post = await Posts.findOne({ _id: _id });
  res.json({ detail: post });
});

이후 API에 넘겨줘서 _id값을 기준으로 데이터를 불러온뒤

const queryString = window.location.search;
        const urlParams = new URLSearchParams(queryString);
        const _id = urlParams.get("_id");
        $(document).ready(function () {
            get_detail();
        })
        function get_detail() {
            $.ajax({
                type: "GET",
                url: `/api/posts/${_id}`,
                data: {},
                error: function (xhr, status, error) {
                    if (status == 404) {
                        alert("존재하지 않는 게시글입니다.");
                    }
                    window.location.href = "/posts";
                },
                success: function (response) {
                    let postsDetail = response["detail"];
                    $("#posttitle").text(postsDetail["title"]);
                    $("#username").text(postsDetail["username"]);
                    $("#postdate").text(postsDetail["date"]);
                    $("#board_content").text(postsDetail["mainpost"]);
                }
            });
        }
        function edit(item){
            window.location.href = `/edit?_id=${_id}`;
        }
        function home(){
            window.location.href = "/home";
        }

상세페이지 태그들에있는 id값을 불러와 jquery로 API에서 불러온 데이터값들을 붙여넣기 해주면된다.

 

이후 정말 어려운 데이터 수정작업이 남았는데.

수정/삭제 API 개발

여기서 장장 하루를 헤매게된다. 상세페이지에서 개별데이터를 불러오는걸 성공했으니 개별데이터를 수정/삭제해주는건 제법 쉬운일일줄 알았다. 수정값을 받고, 게시글비밀번호를 확인하고 API에 넘긴뒤 DB에 넣어주고 다시 홈페이지로 리로드해주면 되는일 아닌가? 맞다. 그런데 멍청비용을 낭비했다.

 

아까 상세페이지를 불러왔듯이 URL에 들어가있는 게시글 id값을 기준으로 데이터를 불러와준다.

<script>
const queryString = window.location.search;
        const urlParams = new URLSearchParams(queryString);
        const _id = urlParams.get("_id");
        $(document).ready(function () {
            get_detail();
        })
        function get_detail() {
            $.ajax({
                type: "GET",
                url: `/api/posts/${_id}`,
                data: {},
                error: function (xhr, status, error) {
                    if (status == 404) {
                        alert("존재하지 않는 게시글입니다.");
                    }
                    window.location.href = "/posts";
                },
                success: function (response) {
                    alert("불러왔습니다");
                    let postsDetail = response["detail"];
                    $("#title").text(postsDetail["title"]);
                    $("#reg_id").text(postsDetail["username"]);
                    $("#content").text(postsDetail["mainpost"]);
                }
            });
        }
</script>

그리고나서

API단을 작업해준다.

router.delete("/posts/:_id/edit", async (req, res) => {
  const { _id } = req.params;
  const post = await Posts.find({ _id });

  posts = await Posts.findOne({ _id: _id });
  if (post.length > 0) {
    await Posts.deleteOne({ _id });
  }
  res.send({ result: "success", detail: posts });
})

router.get("/posts/:_id/edit", async (req, res) => {
  const { _id } = req.params;
  post = await Posts.findOne({ _id: _id });
  res.json({ detail: post });
});

router.patch("/posts/:_id/edit", async (req, res) => {
  const { _id } = req.params;

  posts = await Posts.findOne({ _id: _id });
  const { username, title, mainpost } = req.body;

  isPost = await Posts.find({ _id });
  if (isPost.length) {
    await Posts.updateOne({ _id }, { $set: { username, title, mainpost } });
  }

  res.send({ result: "success", detail: posts });
})
 <script>
       // 여기서 _id값을 넣은게 문제가된다.
       function postdelete(_id) {
                  console.log(_id);
                  $.ajax({
                      type: "DELETE",
                      url: `/api/posts/${_id}/edit`,
                      data: {},
                      success: function (response) {
                          if (response["result"] == "success") {
                              window.location.href = "/home";
                          }
                      }
                  });
              }
        function edit() {
            $.ajax({
                type: "PATCH",
                url: `/api/posts/${_id}/edit`,
                data: {
                    username: $("#reg_id").val(),
                    title: $("#title").val(),
                    mainpost: $("#content").val()
                },
                success: function (response) {
                    console.log(response["detail"]);
                    if (response["result"] == "success") {
                        window.location.href = "/home";
                    }
                }
            });
        }
 </script>

이런식으로 개발해줬는데 딜리트가 작동이 안하는 것 아닌가.. 알고보니 postdelete함수안에 _id값을 넣어줬는데 temphtml에서 업데이트할때 postdelete함수에 변수를 안받아줬었다.. 그래서 global한 id값을 받아오려면 변수를 받아주면 안됐었는데 저런식으로 _id넣어줘서 global한값을 받지못한거였다..

 

이후는 비밀번호를 확인 후 수정/삭제기능이 작동되게해주면됐는데 단순히 delete/edit함수안에 success의 경우 모든걸 실행하고 /home으로 돌아가라고 명령해도 작동이 안돼서

        function passveri() {
            $.ajax({
                type: "GET",
                url: `/api/posts/${_id}`,
                data: {},
                success: function (response) {
                    let postsDetail = response["detail"];
                    console.log(postsDetail);
                    const testpass = $("#password").val();
                    const trainpass = postsDetail["password"];
                    if (testpass == trainpass) {
                        postdelete()
                        alert("삭제되었습니다");
                        window.location.href = "/home";
                    } else { alert("비밀번호를 확인해주세요"); }
                }
            });
        }
        function passveri2() {
            $.ajax({
                type: "GET",
                url: `/api/posts/${_id}`,
                data: {},
                success: function (response) {
                    let postsDetail = response["detail"];
                    console.log(postsDetail);
                    const testpass = $("#password").val();
                    const trainpass = postsDetail["password"];
                    if (testpass == trainpass) {
                        edit()
                        alert("수정되었습니다");
                        window.location.href = "/home";
                    } else { alert("비밀번호를 확인해주세요"); }
                }
            });
        }

이런식으로 passveri()함수들을 따로 작업해주었다. 사실 왜 바로 넘어가는게 안되는지는 아직도 잘모르겠으나.. 해결했으니 됐지 뭐 하고 넘어갔다. 추후 알게되면 서술하겠다.

 

모쪼록 이렇게해서 얼레벌레 CRUD개발이 끝났다. 이해한부분도 있고 못한 부분도 있으나 처음부터 끝까지 자력으로 개발하고 투고해본것은 처음이라.. (팀프로젝트가 아니었으니까) 한층 성장한 기분이 들어서 좋다. 개발에도 점점 재미가 붙고있다. 신기한 일이다..

댓글