티스토리 뷰
프로젝트 개요
▶ 간단한 게시판 제작하기
- PUG Template Engine 사용하기
- 기본적인 CRUD 동작을 하는 웹 만들기
- PM2 Process Manager 이용하여 Node.js 어플리케이션 관리하기
▶ 구현할 기능들
게시판 기능 | 게시글 목록 / 상세보기 / 작성 / 수정 / 삭제 |
회원 기능 | 회원가입 / 로그인 / 비밀번호 찾기 |
추가 기능 | Pagination / 구글 로그인 / 유저 작성글 모아보기 |
Template Engine
▶ Template Engine 이란 ?
서버에서 클라이언트로 보낼 HTML의 형태를 미리 템플릿으로 저장
→ 동작 시에 미리 작성된 템플릿에 데이터를 넣어서 완성된 HTML을 생성하여 서버가 클라이언트에 HTML을 보냄
→ 템플릿 엔진은 템플릿 작성 문법과 작성된 템플릿을 HTML로 변환하는 기능을 제공
▶ 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기 강의를 들으며 정리한 내용입니다.
'개발공부 > 🟩 Node.js' 카테고리의 다른 글
[NodeJS] Express와 MongoDB로 웹서비스 만들기 - 2 (1) (0) | 2022.08.04 |
---|---|
[NodeJS] Express와 MongoDB로 웹서비스 만들기 - 1 (2) (0) | 2022.08.02 |
[NodeJS] Mongoose ODM (0) | 2022.07.31 |
[NodeJS] Node.js와 MongoDB (0) | 2022.07.31 |
[NodeJS] Express.js로 REST API 구현하기 (0) | 2022.07.30 |
프론트엔드 개발자 삐롱히의 개발 & 공부 기록 블로그