업데이트:

실무에서 서브페이지 내용을 퍼블리싱할 때, 공통으로 들어가는 header/footer 등이 include된 작업 템플릿을 Node.js(Express)로 제작, 실무에 적용한 후기를 포스팅한다.

템플릿 구조

우선 공통 레이아웃을 담당하는 subLayout.html을 아래와 같이 구성하였다. 서브페이지 내용이 들어갈 영역을 비워놓아야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="ko">
<head>
  ...
</head>
<body>
  <header>header</header>

  <div id="view">
    <!-- 서브페이지 내용 -->
    
    <!-- //서브페이지 내용 -->
  </div>

  <footer>footer</footer>
</body>
</html>

그리고 서브페이지 내용이 들어갈 html 파일(예 : sub0101.html)을 만든다.

1
<p>내용 내용 내용...</p>

내가 구성한 로직은 sub0101.html 파일을 열면 해당 파일의 내용을 subLayout.html 파일의 <div id="view"></div> 영역에 넣고, 그 결과물을 브라우저에 출력하는 구조임.

Express 로직

다음은 서브페이지 내용 출력을 담당하는 subPageController.js이다. 보면 알겠지만, include가 아니라 크롤링을 응용한 것임. 위에서는 쉬운(?) 설명을 위해 include라고 말했지만 사용 편의를 위해 크롤링을 적용하였음.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// 환경변수 불러오기
const _ROOT = process.env.SUB_PAGE_ROOT;
const _HTML_PATH = process.env.SUB_PAGE_HTML_PATH;
const _VIEW = process.env.SUB_PAGE_VIEW;

// 모듈 불러오기
const express = require('express');
const router = express.Router();

const fs = require('fs');
const path = require('path');
const url = require('url');
const req = require('request');
const cheerio = require('cheerio');

renderHTMLfile('*.html', `../${_ROOT}`, _HTML_PATH, _VIEW);

function renderHTMLfile(to, ctx, layout, view){ // 라우터 path, 서브페이지 ROOT 경로, 서브레이아웃 HTML 경로, 서브페이지 VIEW container
  router.get(to, (request, response) => {
    response.writeHead(200, { 'Content-Type': 'text/html;charset=utf8' });
  
    let pathname = url.parse(request.url).pathname;
    let HTMLfile = decodeURI(pathname.substring(pathname.lastIndexOf('/')).replace('/', '')); // 현재 열고 있는 HTML 파일명을 가져온다.
  
    // 콜백 Hell 탈출을 위해 Promise를 학습, 적용해봤다. 다음에는 async/await를 써봐야겠음.
    new Promise((resolve, reject) => {
      fs.readFile(path.resolve(__dirname, `${ctx}/${layout}/${HTMLfile}`), 'utf8', (error, data) => {
        if (error) {
          return response.end('파일을 찾을 수 없습니다.', 'utf-8');
        }
        resolve(data);
      });
    })
    .then((resolvedData) => {
      req({
        url: `http://${process.env.HOST}:${process.env.PORT}/${layout}/subLayout.html`,
        method: 'GET',
      }, (err, res, body) => {
        const a = cheerio.load(body, {
          decodeEntities: false
        });
        const b = a.load(a.root().html(), {
          decodeEntities: false // HTML entities 코드를 자동으로 decoding하지 않도록 설정
        });
        
        b(view).html('\n' + resolvedData + '\n');
  
        const renderedHTML = '<!DOCTYPE html>\n' + a.html(a(b(view).parents('html')));
        const encodedEntitiesHTML = renderedHTML // HTML entities 코드를 이스케이핑해준다. 꺾쇠(< >)는 <div> 이런 태그에 들어가는 것까지 이스케이핑되므로 제외
          .replace(/·/g, '&middot;')
          .replace(/←/g, '&larr;')
          .replace(/↑/g, '&uarr;')
          .replace(/→/g, '&rarr;')
          .replace(/↓/g, '&darr;');
  
        // 서브레이아웃 + 콘텐츠 합친 파일을 특정 폴더(output)에 저장 (내 로컬에 저장된 합쳐진 파일을 공유폴더에 수동으로 옮겨 팀원들과 공유)
        fs.writeFile(path.resolve(__dirname, `${ctx}/${layout}/output/${HTMLfile}`), encodedEntitiesHTML, 'utf8', (error) => {
          if (error) throw error;
        });
          
        // 브라우저에 콘텐츠 출력
        response.end(renderedHTML, 'utf-8');
      });
    })
    .catch((e) => {
      throw e;
    });
  });
}

module.exports = router;

환경변수를 설정한 .env 파일의 내용은 아래와 같다.

1
2
3
4
5
HOST=localhost
PORT=8080
SUB_PAGE_ROOT="projectA"
SUB_PAGE_HTML_PATH="foo/bar"
SUB_PAGE_VIEW="#view"

예를 들어, http://localhost:8080/sub0101.html에 접속 시 서브레이아웃과 서브페이지 내용이 합쳐진 결과물이 브라우저에 출력된다.

후기

반복되는 레이아웃을 include 처리를 하지 않은 채 서브페이지 내용을 작업하다보니 불편함을 느끼기 일쑤였고, 결국 Node.js로 그럭저럭 쓸만한(?) 물건을 만들어서 실무에 테스트해보는 데까지 1달 정도 걸렸던 것 같다.

현재 진행 중인 프로젝트에 적용했고, 작업이 굉장히 간편해졌다. 지속적으로 코드 품질을 개선해나갈 예정이고, 후기도 업데이트할 것이다.

Hero image from CopyrightFreePictures