[8주차] Node.js와 MongoDB 2 - Express.js와 MongoDB로 웹 서비스 만들기

minLuna·2023년 4월 22일
0

엘리스 AI트랙 7기

목록 보기
46/62

본 자료는 Elice 플랫폼의 자료를 사용하여 정리하였습니다.

Template Engine

Template Engine이란?

  • 서버에서 클라이언트로 보낼 HTML의 형태를 미리 템플릿으로 저장
  • 동작 시에 미리 작성된 템플릿에 데이터를 넘어서 완성된 HTML 생성
  • 템플릿엔진은 템플릿 작성문법과 작성된 템플릿을 HTML로 변환하는 기능을 제공

Express.js의 템플릿엔진

  • EJS - html과 유사한 문법
  • Mustache - 간단한 데이터 치환정도만 제공
  • Pug - 들여쓰기 표현식 이용

Pug란?

  • 들여쓰기 표현식 사용 \rarr 가독성이 올라가고 개발생산성이 올라간다.
  • 문법적 실수를 줄일 수 있다.
  • layout, include, mixin 등 강력한 기능을 제공한다.

Pug문법

html
  head
    title = title
  body
    h1#greeting 안녕하세요
    a.link(href="/") 홈으로
  • 닫기 태그를 사용하지 않고 들여쓰기로 문법을 구분한다.
  • =를 이용해 전달받은 변수를 사용할 수 있다.
  • id나 class는 태그 바로 뒤에 사용한다.

pug문법 - each, if

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

pug문법 - layout

// layout.pug

html
  head
    title = title
  body
    block content	// layout은 block을 포함한 템플릿
    
    
// main.pug
extends layout		// extends로 아래 부분에 작성한 HTML 태그가 block부분에 포함된다.
block content
  h1 Main Page

pug문법 - include

// title.pug

h1 = title

// main.pug

extends layout
  block content
    include title		// 자주 반복되는 구문에 사용
    div.content
      안녕하세요
    pre
      include. article.txt		// 텍스트 파일도 가능하다.

pug문법 - mixin

// listItem.pug

mixin listItem(title, name)
  tr
    td title
    td name
    
// main.pug

include listItem
table
  tbody
    listItem('제목', '이름')		// 파라미터로 값을 넘겨받아 사용한다.

Express.js와 pug 연동

// app.js

app.set('views', path.join(__dirname, 'views'));	// 템플릿이 저장되는 디렉토리 지정
app.set('view engine', 'pug');		// 템플릿 엔진 지정

// request handler

res.render('main', {	// res.render는 화면을 그리는 기능이다.
  title: 'Hello Express',		// res.render(템플릿 이름, 템플릿에 전달되는 값)
});

Express.js의 app.locals

// app.js

app.locals.appName = 'Express'		// render함수에 전달되지 않은 값이나 함수를 사용(템플릿에 전역으로 사용될 값을 지정)

// main.pug

h1 = appName

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

$ express --view=pug myapp

게시판 CRUD 만들기

게시판에서의 CRUD

Create

  • 게시글 작성
  • 제목, 내용, 작성자, 작성시간 등의 정보를 기록
  • 제목과 내용은 최소 n글자 이상

Read

  • 게시글의 목록과 게시글의 상세를 볼 수 있어야 한다.
  • 게시글 목록은 제목과 작성시간을 간략하게 보여준다.
  • 게시글 상세는 제목, 작성자, 내용, 작성시간, 수정시간 등을 상세하게 보여준다.

Update

  • 게시글 수정(작성자만 가능)
  • 제목과 내용이 수정되고 수정시간이 기록
  • 제목과 내용은 최소 n글자 이상

Delete

  • 게시글 삭제(작성자만 가능)
  • 삭제 후 게시글 목록과 상세에서 사라져야 한다.

shortID

const { nanoid } = require('nanoid');	// 중복없는 문자열을 생성해주는 패키지
const shortId = {
  type: String,
  default: () => {
    return nanoid();	// ObjectId를 대체할 아이디 생성
  },
  required: true,
  index: true,
}
module.exports = shortId;

게시글 작성

게시글 작성 흐름

  1. /posts?write=true로 작성페이지 접근
  2. ` 이용해 post 요청 전송
  3. router.post 이용하여 post 요청 처리
  4. 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="등록")

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);
  }
});

게시글 목록 및 상세

게시글 목록 및 상세 흐름

  1. /posts로 목록페이지 접근
  2. <a href='/posts/:shortId'> 이용하여 상세 URL Link
  3. 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
      tr
        td
          a(href=`/posts/${post.shortId}`)
            = post.title
        td= post.author
        td= formatDate(post.createdAt)
  tfoot
    tr
      td(colspan="3")
        a(href="/posts?write=true")
          등록하기

formatDate 함수 추가하기

// app.js

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}")`)
          삭제

게시글 수정

게시글 수정 흐름

  1. /posts/{shortId}?edit=true로 수정페이지 접근
  2. 작성페이지를 수정페이지로도 동작하도록 작성
  3. <form action="/posts/:shortId" method="post">로 post 요청 전송
  • html form은 put을 지원하지 않기 때문에 post를 사용한다.

수정 페이지 만들기

// ./routes/posts.js

router.get('/:shortId', async (req, res, next) => {
  ...
  if (req.query.edit) {
    res.render('posts/edir', { 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
      td
        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}`);
}

게시글 삭제

게시글 삭제 흐름

  1. 게시글 상세 페이지에 삭제 버튼 추가
  2. html form은 DELETE를 지원하지 않는다.
  3. JavaScript fetch 함수로 HTTP DELETE 요청 전송
  4. router.delete의 응답을 fetch에서 처리

HTTP Delete 요청 전송 및 응답 처리

// 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('오류가 발생했습니다.');
      });
  }

DELETE 요청 처리하기

// ./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) {
    nexst(err);
  }
});

Async Request Handler

request handler의 오류처리

request handler에서 오류를 처리하기 위한 방법

  1. promise().catch(next)
  2. async function, try ~ catch, next

async request handler

  • try ~ catch, next를 자동으로 할 수 있도록 구성
  • asyncHandler는 requestHandler를 매개변수로 갖는 함수형 미들웨어이다.
  • 전달된 requestHandler는 try ~ catch로 감싸져 asyncHandler 내에서 실행되고, throw 되는 에러는 자옹으로 오류처리 미들웨어로 전달되도록 구성된다.
const asyncHandler = (requestHandler) => {
  return async (req, res, next) => {
    try {
      await requestHandler(req, res);
    } catch (err) {
      next(err);
    }
  }
}

---
router.get('/', asyncHandler(async (req, res) => {
  const posts = await Posts.find({});
  if (posts.length < 1) {
    throw new Error('Not Found');
  }
  res.render('posts/list', { posts });
});

Pagination

Pagination 이란?

  • 데이터를 균일한 수로 나누어 페이지로 분리하는 것

사용방법

router.get(... => {
  const page = Number(req.query.page || 1);  // 현재 페이지
  const perPage = Number(req.query.perPage || 10);  // 페이지 당 게시글 수
  • /posts?page=1&perPate=10 일반적으로 url query를 사용해 전달한다.
  • query는 문자열로 전달되기 때문에 Number로 형변환이 필요하다.
router.get(... => {
  ...
  const total = await Post.countDocument({});
  const posts = await Post.find({});
    .sort({ createdAt: -1 })
    .skip(perPage * (page -1))		// 검색 시 포함하지 않을 데이터 수
    .limit(perPage);		// 검색 결과 수 제한
const totalPage = Math.ceil(total / perPage);
  • MongoDB의 limit와 skip을 사용하여 pagination 구현가능
  • pagination시에는 데이터의 순서가 유지될 수 있도록 sort를 사용할 수 있도록 한다.
  • 게시글 수 / 페이지 당 게시글 수 = 총 페이지 수
mixin pagination(path)
  p
    - for(let i = 1; i <= totalPage; i++)
    a(href=`${path}?page=${i}&perPage=${perPage}`)
      if i == page
        b= i
      else
        = i
    = " "
---
include pagination
tr
  td
    +pagination('/posts')
  • pagination을 mixin으로 선언
  • pagination이 필요한 페이지에서 해당 템플릿을 include한 후, +pagination으로 mixin을 사용한다.
  • 현재 페이지는 b 태그로 굵게 표시한다.

PM2 Process Manager

PM2란?

  • Node.js 작업을 관리해주는 Process Manager
  • node 명령어로 실행 시 오류 발생이나 실행 상태 관리를 할 수 없다.
  • pm2는 작업관리를 위한 다양한 유용한 기능을 제공해준다.

PM2를 사용하는 이유

  • 안정적인 프로세스 실행 - 오류발생 시 자동 재실행
  • 빠른 개발환경 - 소스코드 변경 시 자동 재실행
  • 배포 시 편리한 관리 - pm2에 모든 프로세스를 한 번에 관리

PM2 사용방법

  • $ pm2 init simple 혹은 $ pm2 init 명령어를 이용하여 pm2 설정파일 예제를 만들 수 있다.
  • 예제를 수정하여 설정파일을 생성한 후, $ pm2 start 명령어를 실행하면 어플리케이션을 pm2 데몬으로 실행해준다.
  • 개발 시 watch 옵션을 사용하여 파일 변경 시 서버 자동 재실행 구성을 할 수 있다.
module.exports = {
  apps : [{
    name: 'simple-board',
    script: './bin/www',
    watch: '.',
    ignore_watch: 'views',
  }],
};

---
$ pm2 start

PM2 Example

profile
열심히

0개의 댓글