JavaScript & Node.js

[node.js & express] 게시판 만들기

Haksae 2022. 1. 27. 16:45
💪🏻 목표
1) Node를 이용해서 웹 프레임워크를 구성
2) mongoDB와 mongoose를 이용하여 원하는 데이터베이스를 만들고 활용
3) express를 기반으로 CRUD(Create, Read, Update, Delete) 기능이 포함된 REST api 작성
4) AWS에 express와 mongoDB 서비스를 배포
🙌🏻  아쉬운 점
node로 작업한 첫 프로젝트고 아직은 미숙해서 코드가 너무 난잡한 느낌이다.
그리고 에러 코드 출력에 대해서 아직 완벽하게 구현을 못했다. (구현하다가 다른 에러가 발생해서 애쓰다가 우선 마무리했다.)
다음 프로젝트에서는 CRUD를 좀 더 깔끔하게 구현하는 방법. 쓸떼 없는 코드로 낭비하지 않도록 노력해야겠다.
더불어 로그인 관련은 아직 배우지 않았는데 그것에 대한 공부하며 구조에 대한 고민을 해야겠다.
본 글에서는 내가 짰던 코드를 돌아보면서 간단하게나마 설명하는 방식으로 써보겠다.
나중에 실력이 좀 더 쌓이면, 작성하는 순서나 디테일을 기록하겠다. 

참고로 본 글 코드는 깃허브에 올려놓았다.

https://github.com/Haksae90/node_project_1

 

Node.js를 아직 설치하지 못했다면 ➡ Node.js 설치

1. app.js

app.js가 웹 서버이다.

웹 서버에서 해야할 일은

1) 포트 연결

2) db 연결

3) URL 렌더링 파일 연결

4) 라우터 연결

5) 각종 셋팅

 - json으로 파싱

 - urlencode

 - ejs view engine

 - 정적 파일 셋팅

const express = require("express");
const connect = require("./schemas");
const app = express();
const port = 3000;
connect(); // db 연결

// articles 라우터 가져오기
const articlesRouter = require("./routes/articles");
// 콘솔 창에 어떤 요청이 있는지에 대해서 보여줌
const requestMiddleware = (req, res, next) => {
  console.log("Request URL:", req.originalUrl, " - ", new Date());
  next();
};
// 바디로 들어온 제이슨 형태를 파싱
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(requestMiddleware);

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

//정적파일 사용
app.use(express.static("./views"));

// article 라우터 사용
app.use("/", [articlesRouter]);

// home 페이지 렌더링
app.get('/', (req, res) => {
  res.render('board');
})

// post 페이지 연결
app.get('/post', (req, res) => {
  res.render('post');
})

  // 콘솔창에 이 포트로 서버가 켜졌다고 알려줌
app.listen(port, () => {
  console.log(port, "포트로 서버가 켜졌습니다.");
});

2.  routes/articles.js

웹 서버를 통해 라우터로 요청이 들어오고, 모든 API 요청을 여기서 해결한다.

라우터는 깔끔한게 좋아서, 상세 API는 모두 controller에 넣고, 모듈로 불러와서 사용한다.

-> 즉 프론트에서 요청하면 라우터에서 컨트롤러로 요청을 처리할 수 있도록 전달해준다. 그리고 다시 받아서 전달 (이것이 라우터의 역할)

const express = require("express");
const router = express.Router();

const {
    getAllArticles,
    getDetail,
    post,
    editPage,
    editArticle,
    deleteArticle
} = require("../controllers/postController");

// Articles 전체 목록 조회
router.get("/articles", getAllArticles);

// Articles 상세 페이지 조회
router.get("/articles/:_id", getDetail);

// Article 생성
router.post("/post", post);

// Article 수정 페이지 로드
router.get("/edit/:_id", editPage);

// Article 수정
router.put("/articles/edit/:_id", editArticle);

// Article 삭제
router.delete("/articles/delete/:_id", deleteArticle)

module.exports = router;

3. controller/postController.js

API의 실제 기능에 대한 코드는 컨트롤러에 다 들어가 있다.

DB와 연결한 코드 외에는 export 밖에 없다.

기본적으로 try, catch로 에러를 잡았고 특별한 것은 없다.

모든 API를 변수로 선언한 후, 모듈로 라우터와 연결해주었다.

const Articles = require("../schemas/articles");

// Articles 전체 목록 조회
const getAllArticles = async (req, res) => {
    try {
        const articles = await Articles.find({}).sort("-date").exec();
        res.json({
            articles,
        });
    } catch (error) {
        res.status(400).send ({ error: error.message})
    }
};

// Articles 상세 페이지 조회
const getDetail = async (req, res) => {
    try {
        const { params: { _id }, } = req;
        const article = await Articles.find({ _id: _id });
        res.status(200).render('detail', { article });
    } catch (error) {
        res.status(400).send({ error: error.message });
    }
};

// Article 생성
const post = async (req, res) => {
    const { title, authorId, password, content } = req.body;
    let date = new Date(+new Date() + 3240 * 10000).toISOString().replace("T", " ").replace(/\..*/, '');
    
    await Articles.create({
        title,
        authorId,
        password,
        content,
        date,
    });
    res.json({ success: true });
};

// Article 수정 페이지 로드
const editPage = async (req, res) => {
        try {
        const { params: { _id }, } = req;
        const article = await Articles.find({ _id: _id });
        res.status(200).render('edit', { article });
    } catch (error) {
        res.status(400).send({ error: error.message });
    }
};

// Article 수정
const editArticle = async (req, res) => {
    const { _id } = req.params;
    const { authorId, password, title, content } = req.body;
    const existsArticle = await Articles.find({ _id });
    if (!existsArticle.length) {
        return res.status(400).json({ success: false, errorMessage: "해당 게시글은 삭제된 상태입니다."})};
    if (Number(password) !== Number(existsArticle[0].password)) {
        return res.status(400).json({ success: false, errorMessage: "비밀번호가 틀립니다."})};
    await Articles.updateOne({ _id }, { $set: { authorId, title, content } });
    res.json({ success: true });
};
   

// Article 삭제
const deleteArticle = async (req, res) => {
    try {
    const { _id } = req.params;
    const { password } = req.body;
    const existsArticle = await Articles.find({ _id });
    if (Number(password) === Number(existsArticle[0].password)){
        await Articles.deleteOne({ _id });
        res.json({ sucess: true });
    }} catch (error) {
        res.status(400).send({ error: error.message })
    }
};

module.exports = {
    getAllArticles,
    getDetail,
    post,
    editPage,
    editArticle,
    deleteArticle
}

4. schema or model

- 몽고 DB는 비관계형 데이터베이스다. schema나 model이 없어도 데이터를 저장할 수 있지만, 균일한 사용을 위해 schema를 사용하면 좋다.

- 우선 몽구스를 불러오고, 연결해준다. 에러 캐치를 해주고 이를 사용할 수 있도록 export

const mongoose = require("mongoose");

const connect = () => {
    mongoose.connect("mongodb://localhost:27017/hanghae99", { ignoreUndefined: true}).catch((error) => {
        console.error(err);
    });
};

module.exports = connect;

- articles의 스키마를 설정해준다.

- required는 필수적이라는 뜻이다.

const mongoose = require("mongoose");

const articlesSchema = mongoose.Schema({
    title: {
        type: String,
        required: true,
    },
    authorId: {
        type: String,
        required: true,
    },
    password: {
        type: Number,
        required: true,
    },
    content: {
        type: String,
        required: true,
    },
    date:{
        type: String,
        required: true,
    }
});

module.exports = mongoose.model("articles", articlesSchema);

5. ejs

총 4개로 구성되어있다. CSS는 과감히 잘라버리고 올리겠다.

1) board.ejs (홈)

- 웹 서버에 홈 역할을 하는 ejs다. 해당 화면에는 3가지 기능이 있어야한다. 먼저는 모든 ariticle을 보여주는 것, 글쓰기 페이지로 이동, 상세 페이지로 이동

(1) articles 조회

- 모든 아티클을 조회하는 것은 ready 함수와 getArticles 함수를 이용했다. ajax로 GET을 보내고 다시 받아온 정보를 for문을 통해서 작업한 뒤 append 해준다.

(2) 글쓰기 페이지 이동

- 간단하게 window.location.replace를 사용하였다.

(3) 상세 페이지 이동

- (1)에서 apeend 할 때, <a href>를 기입하여 바로 해당 상세페이지로 이동할 수 있도록 작성 

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>학새의 항해 블로그</title>

    <!-- 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">


    <!-- 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>

    <!--font-->
    <link href="https://fonts.googleapis.com/css2?family=Sunflower:wght@300&display=swap" rel="stylesheet">
</head>
<script>
    $(document).ready(function () {
        getArticles();
    });

    function getArticles() {
        $.ajax({
            type: "GET",
            url: "/articles",
            data: {},
            success: function (response) {
                let articles = response["articles"];
                for (let i = 0; i < articles.length; i++) {
                    let authorId = articles[i]["authorId"];
                    let title = articles[i]["title"];
                    let date = articles[i]["date"];
                    let _id = articles[i]["_id"];
                    let temp_html = `<tr>
                                        <td>"${authorId}"</td>
                                        <td><a href="/articles/${_id}">"${title}"<a/></td>
                                        <td>"${date}"</td>
                                    </tr>`
                    $("#articleBox").append(temp_html);
                }
            }
        })
    }

    function post(){
    window.location.replace("/post")
}
</script>
<body>
    <form class="title_warp">
        <h2 class="title">학새의 항해 블로그</h2>
    </form> <br>
    <div class="button">
        <button type="button" class="btn btn-success" onclick="post()">글쓰기</button>
    </div>
    <section>
        <div class="articles">
            <table class="table">
                <thead>
                    <tr>
                        <th scope="col">작성자</th>
                        <th scope="col">제목</th>
                        <th scope="col">작성날짜</th>
                    </tr>
                </thead>
                <tbody id="articleBox">
                </tbody>
            </table>
        </div>
    </section>
</body>

</html>

2) detail.ejs

- board에서 a href를 클릭하면 해당 url이 라우터로 이동하였다가, 컨트롤러에서 상세 페이지 조회 함수로 넘어간다.

- 거기서 url로 온 값을 받아서 DB에서 해당 _id를 검색 후 detail.ejs를 렌더링하고, 해당 article 데이터를 전송한다.

- detail 페이지는 렌더링되면서 article 페이지에 필요한 정보가 작성된다.

- 게시글 수정도 동일한 방식으로 해당 _id를 url로 보내는 방식을 이용한다.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>학새의 항해 블로그</title>

    <!-- 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">


    <!-- 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>

    <!--font-->
    <link href="https://fonts.googleapis.com/css2?family=Sunflower:wght@300&display=swap" rel="stylesheet">
</head>
<script>
    // 게시글 수정
    function edit() {
             window.location.href = "/edit/<%=article[0]._id%>"
    }
</script>

<body>
    <form class="title_warp">
        <h2 class="title">학새의 항해 블로그</h2>
    </form> <br>
    <section>
        <div class="articles">
            <table class="table">
                <thead>
                    <tr>
                        <th scope="col">작성자</th>
                        <th scope="col">제목</th>
                        <th scope="col">내용</th>
                        <th scope="col">작성날짜</th>
                    </tr>
                </thead>
                <tbody id="articleBox">
                        <tr>
                            <td>
                                <%=article[0].authorId%>
                            </td>
                            <td>
                                <%=article[0].title%>
                            </td>
                            <td>
                                <%=article[0].content%>
                            </td>
                            <td>
                                <%=article[0].date%>
                            </td>
                        </tr>
                </tbody>
            </table>
        </div>
    </section> <br>
    <!-- Button -->
    <div class="button">
        <input id="edit" type="button" class="btn btn-success" onclick="edit()" value="수정하기">
    </div>
</body>

</html>

3) edit.ejs

- edit 페이지에서 신경써야할 부분은, 해당 DB를 수정하겠다고 하면, post 페이지와 동일한 화면에 미리 저장된 value 값이 띄워져야한다.(비밀번호란 제외) 이를 위해서 ready 함수와 input_Text 함수를 사용했다.

- 해당 페이지는 게시글 수정과 삭제가 가능한데, 둘 다 DB에 저장된 비밀번호와 입력한 비밀번호가 일치해야만 가능하다.

1) 게시글 수정 : 선택자를 통해서 받아온 값을 ajax를 통해서 url에는 _id, body에는 값을 넣어 전송한다.

2) 게시글 삭제 : 비밀번호만 DELETE로 보내고 비밀번호가 일치하면 삭제 

<!DOCTYPE html>
<html lang="us">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>글쓰기</title>

    <!-- 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">


    <!-- 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>
    <!--font-->
    <link href="https://fonts.googleapis.com/css2?family=Sunflower:wght@300&display=swap" rel="stylesheet">
</head>
<script>
    //자동으로 함수 실행
    $(document).ready(function () {
        input_Text()
    })

    // 작성자명에 이름 기입
function input_Text() {
    document.getElementById("authorId").value = "<%=article[0].authorId%>"
}

// 게시글 수정
function edit(_id) {
    let authorId = $("#authorId").val()
    let password = $("#password").val()
    let title = $("#title").val()
    let content = $("#content").val()
    $.ajax({
        type: "PUT",
        url: "/articles/edit/"+_id,
        data: {
            authorId,
            password,
            title,
            content
        },
          success: function(response) {
            alert("수정이 완료되었습니다");
            window.location.replace("/")
        }
    })
}

function deleteArticle(_id) {
    let password = $("#password").val()
            $.ajax({
                type: "DELETE",
                url: "/articles/delete/"+_id,
                data: { password },
                success: function (response) {
                    alert("게시글이 삭제되었습니다");
                    window.location.replace("/")
                }
            })};
</script>

<body>
<form class="WritePost">
    <div class="title_wrap">
        <h2 class="title">글쓰기</h2>
    </div>
    <section class="Basic_info_wrap">
        <div class="authorId">
            <label class="col-md-12" for="authorId">작성자명</label>
            <div class="col-md-12">
                <input id="authorId" type="text" placeholder="작성자명을 입력하세요." onclick="input_Text()" value="authorId"
                       class="form-control input-md" autofocus required>  <!--required : 공백입력시 경고-->
            </div>
        </div>
            <br>
            <div class="password">
            <label class="col-md-12" for="password">비밀번호</label>
            <div class="col-md-12">
                <input id="password" type="text" placeholder="해당 글의 비밀번호."
                       class="form-control input-md" autofocus required>
            </div>
        </div>
    </section>
    <section class="body_wrap">
        <div class="body_form">
            <label class="col-md-12" for="title">제목</label>
            <div class="col-md-12">
                        <textarea class="form-control" id="title" placeholder="제목은 간략하고 임팩트있게!"
                                  style="height:100px;" autofocus required><%=article[0].title%></textarea> <br>
            </div>
            <label class="col-md-12" for="content">내용</label>
            <div class="col-md-12">
                        <textarea class="form-control" id="content" placeholder="쓰고싶은 것을 자유롭게 써주세요."
                                  style="height:100px;" autofocus required><%=article[0].content%></textarea>
            </div>
            <!-- Button -->
            <div class="button">
                <input id="resister" class="btn btn-primary" type="submit"
                       onclick="edit(`<%=article[0]._id%>`)" value="수정완료">
                <input id="resister" class="btn btn-primary" type="submit"
                       onclick="deleteArticle(`<%=article[0]._id%>`)" value="삭제하기">

            </div>
        </div>
    </section>
</form>
</body>
</html>

4) post.ejs

1) 돌아가기, 2) 게시글 작성 기능이 들어가있다.

게시글 작성은 수정과 거의 비슷하게, 선택자를 통해서 정보 받고 Ajax post를 통해서 라우터에 전달한다. 그리고 해당 값은 DB에 저장된다.

<!DOCTYPE html>
<html lang="us">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>글쓰기</title>

    <!-- 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">


    <!-- 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>
    <!--font-->
    <link href="https://fonts.googleapis.com/css2?family=Sunflower:wght@300&display=swap" rel="stylesheet">
</head>
<script>

// 돌아가기
function moveindex(){
    window.location.replace("/")
}

// 게시글 작성
function post() {
    let authorId = $("#authorId").val()
    let password = $("#password").val()
    let title = $("#title").val()
    let content = $("#content").val()
    $.ajax({
        type: "POST",
        url: "/post",
        data: {
            authorId,
            password,
            title,
            content
        },
        success: function (response) {
            alert("게시글이 등록되었습니다");
            window.location.replace("/")
        }
    })
}
</script>

<body>
<form class="WritePost">
    <div class="title_wrap">
        <h2 class="title">글쓰기</h2>
    </div>
    <section class="Basic_info_wrap">
        <div class="authorId">
            <label class="col-md-12" for="authorId">작성자명</label>
            <div class="col-md-12">
                <input id="authorId" type="text" placeholder="작성자명을 입력하세요."
                       class="form-control input-md" autofocus required> <!--required : 공백입력시 경고-->
            </div>
        </div>
            <br>
            <div class="password">
            <label class="col-md-12" for="password">비밀번호</label>
            <div class="col-md-12">
                <input id="password" type="text" placeholder="해당 글의 비밀번호."
                       class="form-control input-md" autofocus required>
            </div>
        </div>
    </section>
    <section class="body_wrap">
        <div class="body_form">
            <label class="col-md-12" for="title">제목</label>
            <div class="col-md-12">
                        <textarea class="form-control" id="title" placeholder="제목은 간략하고 임팩트있게!"
                                  style="height:100px;" autofocus required></textarea> <br>
            </div>
            <label class="col-md-12" for="content">내용</label>
            <div class="col-md-12">
                        <textarea class="form-control" id="content" placeholder="쓰고싶은 것을 자유롭게 써주세요."
                                  style="height:100px;" autofocus required></textarea>
            </div>
            <div class="button">
                <input id="resister" class="btn btn-primary" type="submit"
                       onclick="post()" value="등록하기">
                <input id="resister" class="btn btn-primary" type="submit"
                       onclick="moveindex()" value="돌아가기">

            </div>
        </div>
    </section>
</form>
</body>
</html>