728x90
📍 32일 차 12.8. 수. 온라인 강의
오늘은 CRUD를 이용하여 게시판 만들기
,Template Engine
, Pug
, PM2
을 배웠다. node.js
의 기본언어는 JS
인데, 한가지의 언어로 프런트와 백을 다룬다고 생각하니까 가슴이 웅장해졌다. (아직은 어색하지만..) 자주 살펴보며 눈에 익히는 것이 아무래도 좋겠지??
❏ 게시판 만들기
- 웹 서비스 개발의 기본을 학습하기 좋다. 게시판을 통해 기본기를 잘 다지면 무엇이든 응용 가능
- 게시판 목록, 보기, 수정, 작성, 삭제
- 회원가입, 로그인, 비밀번호 찾기, pagination, 구글 로그인, 유저 작성글 모아보기
❏ Template Engine
- 서버에서 클라이언트로 보낼
HTML
형태를 미리 템플릿으로 작성하고 동작시에 미리 작성된 템플릿에 데이터를 넣어 완성된HTML
을 생성한다. - 템플릿 엔진은 템플릿 작성 문법과 템플릿을
HTML
형태로 변환하는 기능을 제공한다.
Express.js
의 템플릿엔진:EJS
(html과 유사한 문법),Mustache
(간단한 데이터 치환 정도만 제공하는 경량화된 템플릿 엔진),Pug
(들여쓰기 표현식을 사용한 간략한 표기와 레이아웃 등 강력한 기능을 제공)
❏ Pug
- 들여 쓰기 표현식을 이용해 가독성이 좋고 개발 생산성을 높인다.
HTML
을 잘 몰라도 문법적인 실수를 줄일 수 있다.layout
,include
,mixin
등 강력한 기능을 제공한다.HTML
닫기 태그 없이 들여 쓰기로 블록을 구분한다.=
을 이용해 전달받은 변수를 사용할 수 있다.id
나class
는 태그 뒤에 이어서 바로 사용하고,()
를 이용해서attribute
를 사용한다.each-in
:for
과 비슷한 태그if, else if, else
: 조건문
// index.pug
html
head
title= title // title 변수 사용
body
h1#greeting 안녕하세요
a.link(href="/") 홈으로
// each, if
each item in arr
if item.name == 'new'
h1 New Document
else
h1= `${item.name}`
// layout.pug
html
head
title= title
body
block content
// main.pug, 반복되는 웹사이트의 틀을 작성하고 extends하며 개발하면 매우 편리하다.
extends layout // layout을 extends하면 block 부분에 작성한 HTML 태그가 포함됨
block content // block을 포함한 템플릿은 layout으로 사용가능
h1 Main Page
// include, layout은 바깥에 선언하고 가져다 쓰고, include는 조각을 만들어서 가져다 쓴다.
// 자주 반복되는 구문을 미리 작성하고 include하여 사용한다.
// 일반적인 텍스트파일도 include하여 템플릿에 포함 가능하다.
// title.pug
h1= title
// main.pug
extend layout
block content
include titie
div.content
안녕하세요
pre
include article.txt
// mixin, 템플릿을 함수처럼 사용할 수 있다.
// include는 값을 지정할 수 없지만, mixin은 파라미터를 지정하여 값을 넘겨받아 템플릿에 사용할 수 있다.
// listItem.pug
mixin listItem(title, name)
tr
td title
td name
// main.pug
include listItem
table
body
listItem('제목', '이름')
❏ Express.js와 pug의 연동
app.set
을 이용해 템플릿이 저장되는 디렉터리를 지정하고, 어떤 템플릿 엔진을 사용할지 지정할 수 있다.res.render
함수는app.set
에 지정된 값을 이용해 화면에 그리는 기능을 수행한다.render
함수의 첫 번재 인자는 템플릿의 이름, 두번째인자는 템플릿에 전달되는 값
// app.js
app.set('views',
path.join(__dirname, 'views'));
app.set('view engine', 'pug');
// request handler
res.render('main', {
title: 'Hello Express',
});
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
: 템플릿 엔진을 지정할 수 있다.
$ express --view=pug myapp
728x90
❏ 게시판 CRUD 만들기
- 데이터를 다루는 네 가지 기본적인 기능
- Create: 게시글 작성 기능, 제목, 내용, 작성자, 작성 시간 등의 정보를 기록함, 게시글의 제목과 내용은 최소 n글자 이상이어야 함
- Read: 게시글의 목록과 게시글의 상세를 볼 수 있어야 함. 목록은 간략화된 정보를 보여줌, 게시글 상세는 제목, 작성자, 내용, 작성 시간, 수정 시간 등의 상세한 정보를 보여줘야 한다.
- Update: 게시글은 수정이 가능해야 한다. 제목, 내용을 수정하고, 수정 시간을 기록한다. (게시글 수정은 작성자만 가능하다. + 회원 기능)
- Delete: 게시글 삭제하기, (게시글 삭제는 작성자만 가능하다. + 회원 기능)
- 모델선언하기:
ID
는 url에 사용하기 좋은 값이 아니기 때문에shortId
로 생성한다. 제목, 내용, 작성자를string
타입으로 스키마에 선언한다.(작성자는 회원가입 후 실시)timestamps
옵션으로 작성 시간, 수정 시간을 자동으로 기록해준다.
// shortId 타입을 mongoose custom type으로 선언
// nanoid: 중복 없는 문자열 생성
// default를 이용해 모델 생성시 자동으로 아이디 생성
const { nanoid } = require('nanoid');
const shortId = {
type: String,
default: () => {return nanoid()},
require: true,
index: true,
}
module.exports = shortId;
❏ 게시글 작성 흐름
/posts?write=true
로 작성 페이지 접근<form action="/posts" method="post">
를 이용해post
요청 전송[router.post](http://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'); // write 값이 있으면 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
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);
}
}
❏ 게시글 목록 및 상세 흐름
/posts
로 목록 페이지 접근<a href='/posts/:shortId'>
를 이용하여 상세 URL linkrouter.get('/:shortId')
path parameter를 이용하여 요청을 처리함
// ./routes/posts.js
// 처음 접속시 모든 리스트를 불러온다.
router.get('/', async(req, res, next) => {
const posts = await Post.find({});
res.render('/posts/list', { posts }); // posts/list에 { posts } 값이 넘어간다.
});
router.get('/:shortId', async(req, rse, next) => {
const { shortId } = req.params;
const post = await Post.findOne({ shortId });
if(!post){
next(new Error('Post notfound!');
return;
}
res.render('posts/view', { post }); // posts/view로 값 보내기
});
// ./views/posts/list.pug
...
table
tbody
each post in posts // each로 반복문 돌기
tr
td
a(href='/posts/${post.shortId}') = post.title
td= post.author
td= formatDate(post.createdAt) // formatDate: custom function
tfoot
tr
td(colsapn='3')
a(href="/posts?write=true") 등록하기
// app.js
// dayjs 패키지 사용
const dayjs = require('dayjs');
app.locals.formatDate = (date) => {
return dayjs(date).format('YYYY-MM-DD HH:mm:ss');
}
// ./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}")`) 삭제
❏ 게시글 수정 흐름
/posts/{shortId}?edit=true
로 수정 페이지 접근- 작성 페이지를 수정 페이지로도 동작하도록 작성
<form action="/posts/:shortId" method="post">
를 이용해post
요청 전송- html form 은
PUT method
를 지원하지 않기 때문에post
사용 res.redirect
는 항상 GET 요청으로 넘어간다.
// ./routes/posts.js
router.get('/:shortId', async (req, res, next) => {
if (req.query.edit){
res.render('posts/edit', { post });
}
...
});
// 수정 요청 처리하기
router.get('/:shortId', async (req, res, next) => {
const { shortId } = req.params;
const { title, content } = req.body;
const post = await Post.findOneAndUpdate({ shortId }, { title, content }) // shortId를 찾아 새로운 title, content로 바꾸겠다.
if(!post){ // 상세 post가 없으면
next(new Error('Post notfound');
return;
}
res.redirect(`/posts/${shortId}`); // redirect는 항상 GET요청으로 넘어간다.
});
// ./views/posts/edit.pug
...
- var action = post ? `/posts/${post.shortId}` : "/posts" // post가 있는 경우에는 상세 페이지로 이동, 그렇지 않은 경우는 작성하기 위한 URL로 보낸다.
form(action=action, method="post")
table
tr
th 제목
td: input(type="text" name="title" value=post&post.title) // post가 있으면 제목을 post.title로 채우기
tr
th 내용
td: textarea(name="content")= post&&post.content // post가 있으면 내용을 post.content로 채우기
td
td(colspan="2")
- var value = post ? "수정" : "등록"
input(type="submit" value=value)
❏ 게시글 삭제 흐름
- 게시글 상세 페이지에 삭제 버튼 추가
html form
은 DELETE 메서드를 지원하지 않음JS
를 이용해fetch
함수로HTTP Delete
요청 전송router.delete
의 응답을fetch
에서 처리
// posts/view.pug
td
button.delete(onclick='deletePost("${post.shortId}")') 삭제
// delete.pug, promise로 실행된다.
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
router.delete('/:shortId', async (req, res, next) => {
const { shortId } = req.params;
try{
await Post.delete({ shortId }); // mongoDB 내 해당 데이터 삭제
res.send('OK');
}catch(e){
next(e);
}
});
❏ Async Request Handler
- 공식적으로 사용되는
express
기술은 아니고 패턴 중 하나임. async
함수를 조금 더 쉽게 사용할 수 있고, 비동기 실행 중 오류 처리를 더 간단하게 할 수 있는 장점이 있다.request handler
에서 오류를 처리하기 위한 방법 (promise().catch(next)
,async function, try - catch, next
)async
의 비동기 처리는 매우 편리하지만, 매번try-catch
구문을 작성하는 것은 귀찮고 실수하기 쉬움 따라서request handler
를async function
으로 작성하면서try-catch next
를 자동으로 할 수 있도록 구성한 아이디어asyncHandler
는requestHandler
를 매개변수로 갖는 함수형 미들웨어, 전달된requestHandler
는try-catch
로 감싸져asyncHandler
내에서 실행되고,throw
되는 에러는 자동으로 오류처리 미들웨어로 전달되도록 구성됨.- 오류처리를 올바르게 사용하지 않으면
express
앱이 종료될 수 있다. next
인자는 제거하고try-catch
구문을 제거해도 동일하게 오류 처리가 가능하다.
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 Post.find({});
if (posts.length < 1){
throw new Error('Not found');
}
res.render('posts/lists', { posts });
});
// 실제 적용 코드(next 인자는 빼줘야한다.)
// posts.js
const { Router } = require('express');
const { Post } = require('../models');
const asyncHandler = require('../utils/async-handler');
const router = Router();
router.get('/', asyncHandler(async (req, res) => {
if (req.query.write) {
res.render('post/edit');
return;
}
const posts = await Post.find({});
res.render('post/list', { posts });
}));
router.get('/:shortId', asyncHandler(async (req, res) => {
const { shortId } = req.params;
const post = await Post.findOne({ shortId });
if (req.query.edit) {
res.render('post/edit', { post });
return;
}
res.render('post/view', { post });
}));
router.post('/', asyncHandler(async (req, res) => {
const { title, content } = req.body;
if (!title || !content) {
throw new Error('제목과 내용을 입력해 주세요');
}
const post = await Post.create({ title, content });
res.redirect(`/posts/${post.shortId}`);
}));
router.post('/:shortId', asyncHandler(async (req, res) => {
const { shortId } = req.params;
const { title, content } = req.body;
if (!title || !content) {
throw new Error('제목과 내용을 입력해 주세요');
}
await Post.updateOne({ shortId }, { title, content });
res.redirect(`/posts/${shortId}`);
}));
router.delete('/:shortId', asyncHandler(async (req, res) => {
const { shortId } = req.params;
await Post.deleteOne({ shortId });
res.send('OK');
}));
module.exports = router;
❏ Pagination
- 데이터가 많아지면 한 페이지의 목록에 모든 데이터를 표현하기 어려움. 따라서 데이터를 균일한 수로 나누어 페이지로 분리하는 것(ex, 10개씩 나누어 1페이지는 10
20, 2페이지는 1120번까지 보여주기) page
: 현재 페이지,perPage
: 페이지 당 게시글 수/posts?page=1&perPage=10
처럼url query
를 사용해 전달,url query
는 문자열로 넘어가기 때문에 사용시number
로 형 변환을 해줘야 한다.
router.get( => {
const page =
Number(req.query.page || 1) // `page`가 없으면 default 1
const perPage =
Number(req.query.perPage || 10) // `perPage`가 없으면 default 10
})
mongoDB
의limit
,skip
을 사용하여 구현도 가능하다. (limit
: 검색 결과 수 제한, skip: 검색 시 포함하지 않을 데이터 수)
// 데이터의 순서가 유지 될 수 있도록 sort를 사용한다.
// 최신순으로 정렬하고 처음부터 몇개의 게시물을 제외시키고 나머지 게시물은 몇개씩 가져오는지
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); // 게시글 수 / 페이지 당 게시글 수 = 총 페이지 수
})
// pug
// patination이 필요한 페이지에서 해당 템플릿을 include한 후, + pagination으로 mixin을 사용 함.
// 현재 페이지는 b 태그로 굵게 표시
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")
❏ PM2 (Process Manager)
Node.js
의 작업을 관리해주는Process Manager
node
명령어로 실행 시 오류 발생이나 실행 상태 관리를 할 수 없음pm2
는 작업 관리를 위한 다양한 유용한 기능을 제공해 줌- 안정적인 프로세스 실행: 오류 발생 시 자동 재실행
- 빠른 개발환경: 소스 코드 변경 시 자동 재실행
- 배포 시 편리한 관리: pm2에 모든 프로세스를 한 번에 관리
pm2 init simple
,pm2 init
이후pm2 start
명령어 실행 개발 시watch
옵션 사용하여 파일 변경 시 서버 자동 재실행 구성
// ecosystem.config.js
module.exports = {
apps: [{
name: 'simple-board',
script: './bin/www',
watch: '.', // 모든 경로를 바라본다.
ignore_watch: 'views',
}],
}
$ pm2 start
반응형
'Frontend > 엘리스 SW 엔지니어 트랙' 카테고리의 다른 글
[ 엘리스 SW 엔지니어 트랙 ] 34일차 (0) | 2021.12.12 |
---|---|
[ 엘리스 SW 엔지니어 트랙 ] 33일차 (0) | 2021.12.09 |
[ 엘리스 SW 엔지니어 트랙 ] 31일차(7주차: 데이터베이스 연동 - Node.js, Session, JWT, 회원가입 및 로그인) (0) | 2021.12.07 |
[ 엘리스 SW 엔지니어 트랙 ] 30일차 (0) | 2021.12.04 |
[ 엘리스 SW 엔지니어 트랙 ] 29일차 (0) | 2021.12.03 |
댓글