지난 글 ([Node.js를 통한 동적 웹사이트 구현 <1>] 중복되는 부분을 template으로! (생활코딩 강의 기반))에서는
동적 웹사이트 구현을 시작해봤었습니다. (아직 사용자의 참여는 불가능)
👇
[Node.js를 통한 동적 웹사이트 구현 <1>] 중복되는 부분을 template으로! (생활코딩 강의 기반) :: kyagile (tistory.com)
✅ 이 실습을 통해 우리가 만드려는 웹사이트는
이전 글에서 구현한 웹사이트 위에 추가적으로,
서버 운영자가 아닌 일반 사용자도
컨텐츠(가 담긴 웹페이지)의 생성, 업데이트, 삭제가 가능하게끔 합니다.
다만 이전 글에서와 달리 express를 활용합니다.
✅ 디렉토리 구성하기
아래와 같이 디렉토리를 구성하면 됩니다.

✅ main.js, index.js, topic.js, template.js 전체 코드와 설명(주석)
// 주석은 크게 나뉘어진 단계이고,
//// 주석은 해당 코드에 대한 자세한 설명입니다.
main.js
// express를 통해 application 객체 생성 var express = require('express'); var app = express(); // 초기에 설정해줘야 하는 미들웨어 var helmet = require('helmet') //// 보안상 목적 var bodyParser = require('body-parser'); //// request의 body 속성을 활용하기 위함. 이게 없으면 undefined됨 var compression = require('compression'); //// 서버가 클라이언트에 응답할 때 데이터를 "압축"하여 보냄 var fs = require('fs'); app.use(helmet()); app.use(bodyParser.urlencoded({ extended: false })); app.use(compression()); app.use(express.static('public')); //// 'public' 폴더에 있는 정적 파일들을 사용하겠다는 뜻 app.get('*', function(request, response, next){ //// request에 list라는 속성을 만들어 줌으로써 추후 활용하려 함. "*"이므로, get 방식을 통해 어떤 경로를 접속하든 적용됨 fs.readdir('./data', function(error, filelist){ request.list = filelist; next(); }); }); // 홈페이지 혹은 상세 페이지로의 라우팅. 자세한 기능은 routes 폴더 내 파일에 기술되어 있음 //// "/"로 들어온 경우 index.js에서 가져온 라우터를, "/topic"로 들어온 경우 topic.js에서 가져온 라우터를 사용 var indexRouter = require('./routes/index'); var topicRouter = require('./routes/topic'); app.use('/', indexRouter); app.use('/topic', topicRouter); // 에러 처리 app.use(function(req, res, next) { res.status(404).send('Error (404) - Not found'); }); app.use(function (err, req, res, next) { console.error(err.stack) res.status(500).send('Something broke!') }); // 서버 열기 app.listen(3000, function() { console.log('Example app listening on port 3000!') });
index.js
// router 객체 생성 var express = require('express'); var router = express.Router(); // 필요한 것 준비 var template = require('../lib/template.js'); // 홈페이지 router.get('/', function(request, response) { // 페이지 내의 요소들 준비 var title = 'Welcome'; var description = 'Here is the root page'; var list = template.list(request.list); // 위 요소들을 가지고 html 페이지 완성하여 send var html = template.HTML( title, list, `<a href="/topic/create">create</a>`, ` <h2>${title}</h2>${description} <img src="/images/hello.jpg" style="width:300px; display:block; margin-top:10px;"> ` ); response.send(html); }); // export module.exports = router;
topic.js
// router 객체 생성 var express = require('express'); var router = express.Router(); // 필요한 것 준비 var path = require('path'); var fs = require('fs'); var sanitizeHtml = require('sanitize-html'); //// html 코드 중 민감한 tag는 삭제함으로써 보안 강화 var template = require('../lib/template.js'); // 생성 페이지 router.get('/create', function(request, response){ // 페이지 내의 요소들 준비 var title = 'WEB - create'; var list = template.list(request.list); // 위 요소들을 가지고 html 페이지 완성하여 send //// form의 action: submit 시 action에 설정된 경로로 이동함 var html = template.HTML( title, list, '', ` <form action="/topic/create_process" method="post"> <p><input type="text" name="title" placeholder="title"></p> <p> <textarea name="description" placeholder="description"></textarea> </p> <p> <input type="submit"> </p> </form> ` ); response.send(html); }); // 생성 버튼 클릭한 순간 router.post('/create_process', function(request, response){ // request.body로부터 필요한 요소 추출 var post = request.body; //// 이 때를 위해 body-parser 사용한 것 var title = post.title; var description = post.description; // 페이지 작성한 뒤 리다이렉트 fs.writeFile(`data/${title}`, description, 'utf8', function(err){ response.redirect(`/topic/${title}`); //// 생성한 페이지로 리다이렉트: 이게 없으면 response가 끝나지 않아 error 발생 }); }); // 업데이트 페이지 router.get('/update/:pageId', function(request, response){ // 페이지 내의 요소들 준비 var filteredId = path.parse(request.params.pageId).base; //// 보안을 위해 id filter fs.readFile(`data/${filteredId}`, 'utf8', function(err, description){ var title = request.params.pageId; var list = template.list(request.list); // 위 요소들을 가지고 html 페이지 완성하여 send //// form의 action: submit 시 action에 설정된 경로로 이동함 var html = template.HTML( title, list, '', ` <form action="/topic/update_process" method="post"> <input type="hidden" name="id" value="${title}"> <p><input type="text" name="title" placeholder="title" value="${title}"></p> <p> <textarea name="description" placeholder="description">${description}</textarea> </p> <p> <input type="submit"> </p> </form> ` ); response.send(html); }); }); // 업데이트 버튼 클릭한 순간 router.post('/update_process', function(request, response){ // request.body로부터 필요한 요소 추출 var post = request.body; //// 이 때를 위해 body-parser 사용한 것 var id = post.id; var title = post.title; var description = post.description; // 제목 변경하고 페이지 작성한 뒤 리다이렉트 fs.rename(`data/${id}`, `data/${title}`, function(error){ fs.writeFile(`data/${title}`, description, 'utf8', function(err){ response.redirect(`/topic/${title}`); //// 업데이트한 페이지로 리다이렉트: 이게 없으면 response가 끝나지 않아 error 발생 }) }); }); // 삭제 버튼 클릭한 순간 router.post('/delete_process', function(request, response){ // request.body로부터 필요한 요소 추출 var post = request.body; //// 이 때를 위해 body-parser 사용한 것 var id = post.id; var filteredId = path.parse(id).base; //// 보안을 위해 id filter // 페이지 삭제 fs.unlink(`data/${filteredId}`, function(error){ response.redirect('/'); //// 홈페이지로 리다이렉트: 이게 없으면 response가 끝나지 않아 error 발생 }); }); // 상세 페이지 router.get('/:pageId', function(request, response, next) { // 페이지 내의 요소들 준비 var filteredId = path.parse(request.params.pageId).base; //// 보안을 위해 id filter fs.readFile(`data/${filteredId}`, 'utf8', function(err, description){ if(err){ //// error 있을 시, main.js의 error handler 실행됨 next(err); } else { var title = request.params.pageId; var sanitizedTitle = sanitizeHtml(title); var sanitizedDescription = sanitizeHtml(description, { allowedTags:['h1'] //// 이 tag는 포함되어도 괜찮음 }); var list = template.list(request.list); // 위 요소들을 가지고 html 페이지 완성하여 send //// form의 action: submit 시 action에 설정된 경로로 이동함 var html = template.HTML( sanitizedTitle, list, ` <a href="/topic/create">create</a> <a href="/topic/update/${sanitizedTitle}">update</a> <form action="/topic/delete_process" method="post"> <input type="hidden" name="id" value="${sanitizedTitle}"> <input type="submit" value="delete"> </form> `, `<h2>${sanitizedTitle}</h2>${sanitizedDescription}` ); response.send(html); } }); }); // export module.exports = router;
template.js
module.exports = { // 페이지의 html 코드 양식 HTML:function(title, list, control, content){ return ` <!doctype html> <html> <head> <title>WEB - ${title}</title> <meta charset="utf-8"> </head> <body> <h1><a href="/">WEB</a></h1> ${list} ${control} ${content} </body> </html> `; }, list:function(filelist){ var list = '<ul>'; var i = 0; while(i < filelist.length){ //// 반복문을 통해, filelist에 존재하는 모든 요소를 누적 list = list + `<li><a href="/topic/${filelist[i]}">${filelist[i]}</a></li>`; i = i + 1; } list = list + '</ul>'; return list; } }
✅ 실제 구동해보기
1. pm2 설치
이번 글에서는 node.js의 프로세스 매니저인 pm2를 사용할 것입니다.
커맨드창에
npm install pm2 -g
을 입력해줍니다.
2. 필요한 module들을 설치
커맨드창의 현재 위치를 우리의 디렉토리로 설정하고,
npm install
을 입력합니다.
3. 서버 열기
커맨드창에
pm2 start main.js --watch
를 입력합니다.
4. 웹브라우저로 접속, 홈페이지 방문
웹브라우저의 주소창에
localhost:3000
을 입력합니다.
- localhost: 이 글에서는, 서버를 연 컴퓨터로 다시 클라이언트로서 접속하고 있기에, 자기 자신을 의미하는 localhost로 설정합니다.
- 3000: 우리가 3000번 포트로 서버를 열었기 때문입니다.
첫 화면으로는 홈페이지가 보입니다.

5. 상세 페이지 방문
리스트의 각 제목을 클릭하면, 각 컨텐츠가 담긴 웹페이지를 볼 수 있습니다.
이 컨텐츠들은 서버 컴퓨터의 작업 디렉토리 내의 data 폴더의 파일들이 반영된 것입니다.

※
아래 단계들은 사용자 쪽에서 해볼 것입니다.
서버 쪽에서는 당연히 할 수 있으며, 이는 지난 글에 설명되어 있습니다.
6. 컨텐츠 생성
create 버튼을 누르면 입력창이 뜨고,
여기에 원하는 내용을 입력한 뒤 제출 버튼을 누르면
작성한 페이지로 리다이렉션됩니다.


이 컨텐츠는 서버 쪽 컴퓨터의 작업 디렉토리에서도 확인할 수 있습니다.

7. 컨텐츠 업데이트
title1 컨텐츠를 업데이트하기 위해,
리스트에서 title1을 클릭하고, 입력창에 원하는 내용을 채운 뒤 제출 버튼을 누르면
업데이트한 페이지로 리다이렉션됩니다.


서버 쪽 컴퓨터의 작업 디렉토리에서도 업데이트된 것을 확인할 수 있습니다.

8. 컨텐츠 삭제
title2 컨텐츠를 삭제하기 위해,
리스트에서 title2를 클릭한 뒤, delete 버튼을 누르면
홈페이지로 리다이렉션되며, 리스트에서 title2가 없어진 것을 볼 수 있습니다.

물론 서버 쪽 컴퓨터의 작업 디렉토리에서도 title2 파일이 없어졌습니다.

9. 에러 처리
우리가 구현한 적 없는, 존재하지 않는 주소를 입력하면 에러가 발생합니다.
에러 처리를 위한 미들웨어 1에 의한 것입니다.

다만 "/topic" 이후에 존재하지 않는 주소를 입력한 경우에는
에러 처리를 위한 미들웨어 2에 의해 핸들링됩니다.

감사합니다!
'컴퓨터과학 > 웹' 카테고리의 다른 글
[Node.js와 Session] Session을 이용한 인증 구현 (생활코딩 강의 기반) (2) | 2024.02.01 |
---|---|
[Node.js와 Cookie] Cookie의 생성, 읽기, 활용 (생활코딩 강의 기반) (0) | 2024.01.28 |
[Node.js를 통한 동적 웹사이트 구현 <1>] 중복되는 부분을 template으로! (생활코딩 강의 기반) (0) | 2024.01.19 |
[웹서버/웹사이트, 웹클라이언트 실습] with HTML, Disqus, Tawk.to (생활코딩 강의 기반) (0) | 2024.01.17 |