IT/웹

[Node.js를 통한 동적 웹사이트 구현 <2>] 사용자도 생성/업데이트/삭제가 가능해졌다..!! with Express (생활코딩 강의 기반)

kykyky 2024. 1. 24. 18:42

지난 글 ([Node.js를 통한 동적 웹사이트 구현 <1>] 중복되는 부분을 template으로! (생활코딩 강의 기반))에서는

동적 웹사이트 구현을 시작해봤었습니다. (아직 사용자의 참여는 불가능)

 

👇

 

[Node.js를 통한 동적 웹사이트 구현 <1>] 중복되는 부분을 template으로! (생활코딩 강의 기반) :: kyagile (tistory.com)


✅ 이 실습을 통해 우리가 만드려는 웹사이트는

 

이전 글에서 구현한 웹사이트 위에 추가적으로,

서버 운영자가 아닌 일반 사용자도 

컨텐츠(가 담긴 웹페이지)의 생성, 업데이트, 삭제가 가능하게끔 합니다.

 

다만 이전 글에서와 달리 express를 활용합니다.

 

 

✅ 디렉토리 구성하기

아래와 같이 디렉토리를 구성하면 됩니다.

nodejs_express_final_blog.zip
1.70MB

 

 

✅ 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에 의해 핸들링됩니다.

 

 

 

 


 

감사합니다!