지난 글 ([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에 의해 핸들링됩니다.
감사합니다!
'IT > 웹' 카테고리의 다른 글
[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 |