티스토리 뷰

개발공부/🟩 Node.js

[NodeJS] Express와 MongoDB로 웹서비스 만들기 - 1 (1)

2022. 8. 2. 16:42

 

 

Node.js

Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine.

nodejs.org

 

 

 

 프로젝트 개요 

 

▶ 간단한 게시판 제작하기

 

-  PUG Template Engine 사용하기

-  기본적인 CRUD 동작을 하는 웹 만들기

-  PM2 Process Manager 이용하여 Node.js 어플리케이션 관리하기

 

 

▶ 구현할 기능들

 

게시판 기능 게시글 목록 / 상세보기 / 작성 / 수정 / 삭제
회원 기능 회원가입 / 로그인 / 비밀번호 찾기
추가 기능 Pagination / 구글 로그인 / 유저 작성글 모아보기

 

 

 

 

 

 Template Engine 

 

▶ Template Engine 이란 ?

 

서버에서 클라이언트로 보낼 HTML의 형태를 미리 템플릿으로 저장

→  동작 시에 미리 작성된 템플릿에 데이터를 넣어서 완성된 HTML을 생성하여 서버가 클라이언트에 HTML을 보냄

→  템플릿 엔진은 템플릿 작성 문법과 작성된 템플릿을 HTML로 변환하는 기능을 제공

 

이미지출처 : 엘리스 Node.js와 MongoDB Ⅱ - 02 Express.js와 MongoDB로 웹서비스 만들기1 강의자료

 

 

 

 

▶ Express.js의 Template Engine

 

-  EJS  :  html과 유사한 문법의 템플릿 엔진

-  Mustache  :  간단한 데이터 치환 정도만 제공하는 경량화된 템플릿 엔진

-  Pug  :  들여쓰기 표현식을 이용한 간략한 표기와 레이아웃 등 강력한 기능을 제공

 

 

우리가 사용할 것은 Pug ..!

-  Pug는 들여쓰기 표현식을 이용해 가독성이 좋고 개발 생산성이 높고 HTML을 잘 모르더라도 문법적 실수를 줄일 수 있음

-  layout, include, mixin 등 강력한 기능 제공

 

 

Pug 문법 1) 

html
    head
    	title= title
    body
        h1#greeting 안녕하세요
        a.link(href="/") 홈으로

-  HTML 닫기 태그 없이 들여쓰기로 블럭 구분

-  "=" 을 이용해서 전달받은 변수 사용 가능

-  id나 class는 태그 뒤에 이어서 바로 사용

-  () 을 이용해서 attribute 사용

 

 

 

Pug 문법 2)  each, if

each item in arr
    if item.name == 'new'
    	h1 New Document
    else
    	h1= `${item.name}`

-  each ~ in 표현식으로 주어진 배열의 값을 순환하며 HTML 태그 생성 가능

-  if, else if, else를 이용해 주어진 값의 조건을 확인하여 HTML 태그 생성 가능

 

 

 

Pug 문법 3)  layout

--- layout.pug ---
html
    head
    	title= title
    body
    	block content
--- main.pug ---
extends layout
block content
	h1 Main Page

-  block을 포함한 템플릿을 선언하면 해당 템플릿을 layout으로 사용 가능

-  layout을 extends 하면 block 부분에 작성한 HTML 태그가 포함됨

-  반복되는 웹사이트의 틀을 작성해 두고 extends 하며 개발하면 매우 편리한 기능

 

 

 

Pug 문법 4)  include

---title.pug---
h1= title
---main.pug
extend layout
block content
	include title
	div.content
		안녕하세요
	pre
		include article.txt

-  자주 반복되는 구문을 미리 작성해 두고 include하여 사용 가능

-  일반적인 텍스트 파일도 include하여 템플릿에 포함 가능

 

 

 

Pug 문법 5)  mixin

--- listItem.pug ---
mixin listItem(title, name)
    tr
        td title
        td name
--- main.pug ---
include listItem
table
    tbody
    	listItem('제목', '이름')

-  mixin 을 사용하여 템플릿을 함수처럼 사용할 수 있게 선언 가능

-  include는 값을 지정할 수 없지만 mixin 은 파라미터를 지정하여 값을 넘겨받아 템플릿에 사용 가능

 

 

 

 

▶ Express.js와 Pug 연동하기

--- app.js ---
app.set('views', path.join(__dirname, 'views'));	// 템플릿이 저장되는 디렉터리 선언
app.set('view engine', 'pug');				// Pug라는 템플릿엔진을 사용한다고 선언


--- request handler ---
res.render('main', { 			// views/main.pug를 render
    title: 'Hello Express',		// main 템플릿을 사용할때 전달될 데이터
});					// main 템플릿과 데이터가 결합하여 HTML 생성

-  app.set()을 이용해 템플릿이 저장되는 디렉터리를 지정하고, 어떤 템플릿 엔진을 사용할지 지정

-  res.render()는 app.set()에 지정된 값을 이용해 화면을 그리는 기능을 수행

-  render 함수의 첫 번째 인자는 템플릿의 이름, 두 번째 인자는 템플릿에 전달되는 값

 

 

 

 

 

* Express.js의 app.locals

:  Express.js의 app.locals를 사용하면 render함수에 전달되지 않은 값이나 함수를 사용 가능

→  템플릿에 전역으로 사용될 값을 지정하는 역할

--- app.js ---
app.locals.appName = "Express"

--- main.pug ---
h1= appName			// <h1>Express</h1>

 

 

 

* express-generator 사용 시 템플릿 엔진 지정하기

:  express-generator는 기본적으로 jade라는 템플릿 엔진을 사용.

→  jade는 pug의 이전 이름으로, 최신 지원을 받기 위해선 템플릿 엔진을 pug로 지정 해야함

 

"--view" 옵션을 사용하여 템플릿 엔진을 지정할 수 있음

// cmd

express --view=pug myapp

→  프로젝트 생성 시 템플릿 엔진을 지정하면,

템플릿 디렉토리가 어디인지 어떤 템플릿 엔진을 사용할 것인지가 자동으로 선언되기 때문에 간단하게 프로젝트 시작 가능

 

 

 

 

 

▶ 프로젝트 세팅하기

 

1)  프로젝트 생성할 디렉터리 안에서   npx express-generator --view=pug simple-borad 

→  아래와 같이 pug를 템플릿엔진으로 사용하는 express 프로젝트 기본 세팅이 완료됨

 

 

 

 

 

 

 

 

 

 CRUD 

 

▶ CRUD 란 ?

 

Create, Read, Update, Delete

 

데이터를 다루는 네 가지 기본적인 기능

→  일반적으로 위 네 가지에 대한 구현이 가능해야 서비스 개발에 필요한 요구사항을 충족할 수 있음

 

 

 

게시판에서의 CRUD

 

1)  Create

-  게시글 작성

-  작성 시 제목, 내용, 작성자, 작성 시간 등의 정보 기록

-  게시글의 제목과 내용은 최소 n글자 이상

 

2)  Read

-  게시글의 목록과 게시글의 상세보기

-  게시글 목록은 제목, 작성자, 작성 시간의 간략화된 정보를 보여줌

-  게시글 상세는 제목, 작성자, 내용, 작성 시간, 수정 시간 등의 상세한 정보를 보여줌

 

3)  Update

-  게시글 수정

-  수정 시 제목과 내용이 수정 가능하고, 수정 시간이 기록되어야 함

-  게시글의 제목과 내용은 최소 n글자 이상

-  게시글 수정은 작성자만 가능

 

4)  Delete

-  게시글 삭제

-  삭제 시 목록과 상세에서 게시글이 접근되지 않아야 함

-  게시글 삭제는 작성자만 가능

 

 

 

 

 

 

▶ CRUD 구현하기

 

1) 모델 선언하기

-  MongoDB의 ObjectID는 URL에 사용하기 좋은 값이 아니기 때문에 대체할 수 있는 아이디를 shortId로 생성

-  제목, 내용, 작성자를 String 타입으로 스키마에 선언 (추후 회원가입 로그인 후 작성자 연동 기능 구현 예정)

-  timestamps 옵션으로 작성 시간, 수정 시간을 자동으로 기록

--- ./models/schemas/post.js
const mongoose, { Schema } = require('mongoose');
const shortId = require('./types/short-id');
module.exports = new Schema({
    shortId,
    title: String,
    content: String,
    author: String,
}, {
	timestamps: true,
});

--- ./models/index.js ---
exports.Post = mongoose.model('Post', PostSchema);

 

 

 

2) shortId

-  MongoDB의 ObjectId를 대체할 shortId 타입을 Mongoose Custom Type으로 선언

-  중복 없는 문자열을 생성해 주는 nanoid 패키지 활용

-  default를 이용해 모델 생성 시 자동으로 ObjectId를 대체할 아이디 생성

const { nanoid } = require('nanoid');

const shortId = {
    type: String,
    default: () => {
    	return nanoid();
    },
    require: true,
    index: true,
}

module.exports = shortId;

 

 

 

3)  게시글 작성

 

게시글 작성 흐름

"/posts?write=true"로 작성페이지 접근   →   "<form action="/posts" method="post>"를 이용해 post 요청 전송

→  router.post를 이용하여 post 요청 처리   →   res.redirect를 이용하여 post 완료 처리

// ./routes/posts.js
// 작성페이지 접근

const { Router } = require('express');
const router = Router();
router.get('/', (req, res, next) => {
    if (req.query.write) {
        res.render('posts/edit');
        return;
    }
    ...
});

...
module.exports = router;
// ./views/posts/edit.pug
// 작성페이지

...
form(action="/posts", method="post")
    table
        tbody
            tr
                th 제목
                td: input(type="text" name="title")
            tr
                th 내용
                td: textarea(name="content")
                td
                td(colspan="2")
                    input(type="submit" value="등록")
// ./routes/posts.js  
// POST요청 처리

const { Post } = require('./models');
...
router.post('/', async (req, res, next) => {
    const { title, content } = req.body;
    try {
        await Post.create({
            title,
            content,
    	});
    	res.redirect('/');
    } catch (err) {
    	next(err);
    } 
});
...

 

 

 

4)  게시글 목록 및 상세

 

게시글 목록 및 상세 흐름 

"/posts"로 목록 페이지 접근  →   "<a href="/posts/:shortId">를 이용하여 상세 URL Link

→  "router.get('/:shortId') path parameter"를 이용하여 요청 처리

// ./routes/posts.js 
// 목록페이지 접근

router.get('/', async (req, res, next) => {
    const posts = await Post.find({});
    res.render('posts/list', { posts });
});
// ./views/posts/list.pug
// 목록페이지

...
table
    tbody
        each post in posts	// res.render() 시 보낸 posts 데이터를 받아와서 each
            tr
                td
                    a(href='/posts/${post.shortId}')= post.title
                td= post.author
                td= formatDate(post.createdAt)	// app.js에 app.locals 로 만들어놓은 함수
    tfoot
        tr
            td(colspan="3")
                a(href="/posts?write=true") 등록하기
// app.js
// list.pug에서 이용한 formatDate 함수 선언

const dayjs = require('dayjs');

app.locals.formatDate = (date) => {
    return dayjs(date).format('YYYY-MM-DD HH:mm:ss');
}
// ./routes/posts.js
// 게시글 상세페이지 접근

router.get('/:shortId', async (req, res, next) => {
    const { shortId } = req.params;
    const post = await Post.findOne({ shortId });
    
    if (!post) {
    	next(new Error('Post NotFound');
    	return;
    }
    ...
    res.render('posts/view', { post });
});
// ./views/posts/view.pug
// 게시글 상세페이지

...
table
    tbody
        tr
            td(colspan="2")= post.title
        tr
            td= post.author
            td= formatDate(post.createdAt)
        tr
            td(colspan="2"): pre= post.content
        tr
            td: a(href='/posts/${post.shortId}?edit=true') 수정
            td
                button(onclick='deletePost("${post.shortId")') 삭제

 

 

 

5)  게시글 수정

 

게시글 수정 흐름 

"/posts/{shortId}?edit=true"로 수정페이지 접근  →  작성페이지를 수정페이지로도 동작하도록 작성

→   "<form action="/posts/:shortId" method="post">"로 post 요청 전송

 

※ html form은 PUT method를 지원하지 않기 때문에 post 사용

// ./routes/posts.js
// 수정페이지 접근

router.get('/:shortId', async (req, res, next) => {
    ...
    if (req.query.edit) {	// 상세페이지와 같은 URL에 query가 edit이면 수정페이지로
    	res.render('posts/edit', { post });
    }
    ...
});
// ./views/posts/edit.pug
// 수정페이지

...
- var action = post ? '/posts/${post.shortId}' : "/posts"
form(action=action, method="post")
    table
        tbody
            tr
                th 제목
                td: input(type="text" name="title" value=post&&post.title)
            tr
                th 내용
                td: textarea(name="content")= post&&post.content
            tr
                td(colspan="2")
                    - var value = post ? "수정" : "등록"
                    input(type="submit" value=value)
// ./routes/posts.js
// 수정요청 처리하기

...
router.post('/:shortId', async (req, res, next) => {
    const { shortId } = req.params;
    const { title, content } = req.body;
    const post = await Post.findOneAndUpdate({ shortId }, {
    	title, content,
    });
    
if (!post) {
    next(new Error('Post NotFound');
    return;
}

res.redirect(`/posts/${shortId}`);

 

 

 

6)  게시글 삭제

 

게시글 삭제 흐름 

게시글 상세 페이지에 삭제 버튼 추가  →   html form은 DELETE 메서드를 지원하지 않음  

→   JavaScript를 이용해 fetch 함수로 HTTP Delete 요청 전송  →   router.delete의 응답을 fetch에서 처리

// posts/view.pug
// 삭제요청 전송 및 응답처리

td
    button.delete(onclick='deletePost("${post.shortId}")') 삭제
...

...
script(type="text/javascript").
    function deletePost(shortId) {
        fetch('/posts/' + shortId, { method: 'delete' })
            .then((res) => {
                if (res.ok) {
                    alert('삭제되었습니다.');
                    window.location.href = '/posts';
                } else {
                    alert('오류가 발생했습니다.');
                    console.log(res.statusText);
                }
            })
            .catch((err) => {
                console.log(err);
                alert('오류가 발생했습니다.');
            });
    }
// ./routes/posts.js
// 삭제요청 처리하기

const { Post } = require('./models');
...
router.delete('/:shortId', async (req, res, next) => {
	const { shortId } = req.params;
	try {
		await Post.delete({ shortId });
		res.send('OK');
	} catch (err) {
		next(err);
	} 
});
...

 

 

 

 

 


이 글은 엘리스 AI트랙 5기 강의를 들으며 정리한 내용입니다.

반응형
프로필사진
개발자 삐롱히

프론트엔드 개발자 삐롱히의 개발 & 공부 기록 블로그