동적 웹 서버

Flickr에 올라온 사진들을 보여주는 동적인 웹 서버를 만듭니다. 동적인 데이터와 뷰를 분리하기 위해 템플릿이라는 개념에 대해 알아봅니다.
웹 서버, 템플릿, 모델과 뷰


동적 웹 서버

정적/동적 웹 서버의 구조정적/동적 웹 서버의 구조

이전 챕터에서 Request에 따라서 정적인 페이지를 Response로 주는 웹 서버를 만들었습니다. 이제 구글, 페이스북, 쇼핑몰 등의 웹 서비스처럼 서버의 데이터에 따라 Response가 동적으로 변하는 웹 서버를 만들어 보겠습니다.

Flickr API 모듈

날씨 API날씨 API

온라인 갤러리를 만듭니다. 이때 사진 데이터는 플리커(Flickr)라는 웹 서비스에서 제공하는 API를 이용해서 실시간으로 가져옵니다. 여기서 API란 자사의 서비스를 타 서비스의 개발에 이용 할 수 있도록 제공한 기능을 말합니다. Google API, Facebook API, Twitter API, Naver API 등 큰 규모의 서비스 회사들은 생태계를 확장하기 위해서 자사의 API를 부분적으로 공개해서 제공하는 경우가 많습니다. 또 정부기관 및 비영리 단체 등에서 공개 API를 제공하기도 합니다.

Google API의 API KeyGoogle API의 API Key

HTTP 프르토콜을 사용하는 API 서버는 당연히 일반적인 웹 서버와 동일한 구조를 지닙니다. 주요한 차이점은 HTML 문서를 주고 받기보다는 클라이언트와 가벼운 포맷으로, 즉 JSON, XML 등의 Content-Type으로 응답하는 경우가 많습니다. 또한 로그인 등의 인증 절차를 대신하면서 API 이용 권한을 관리하고, 이용 현황 등을 모니터링하기 위해서, API 키를 생성해주고 API 이용시 Request에 API 키를 첨부하도록하는 경우가 많습니다.

flickr-api 모듈의 스펙

  • flickr API의 flickr.photos.getRecent API 를 이용합니다.
  • API의 엔드포인트(URL)로 https 프로토콜로 GET Request를 보내서 사진 정보를 받습니다.
  • 사진 정보들을 가공해서 {title:'각 사진의 제목', small:'작은 사진 URL', medium:'중간 사진 URL'} 꼴의 객체의 배열로 만듭니다.
  • 생성된 배열을 callback의 인자로 넘기면서 callback을 실행합니다.

flickr-api.js

const https = require('https');
const API_URL = "https://api.flickr.com/services/rest/?method=flickr.photos.getRecent&api_key=46a247f7ad0611a92fa3bf67a931c5db&format=json&nojsoncallback=1";

module.exports = {
    getRecentPhotos: getRecentPhotos
};

function getRecentPhotos(callback){
    https.get(API_URL, res => {

        let data = "";
        res.on('data', d => {
          data += d;
        });

      res.on('end', () => {
            let rawPhotos = JSON.parse(data).photos.photo;
            var photos = [];

        rawPhotos.forEach(photo => 
            photos.push({
                title: photo.title,
                small : `https://farm${photo.farm}.staticflickr.com/${photo.server}/${photo.id}_${photo.secret}_m.jpg`,
                medium: `https://farm${photo.farm}.staticflickr.com/${photo.server}/${photo.id}_${photo.secret}_z.jpg`
            })
        );

        callback(photos);
      });
    });
}

Flickr 갤러리

Flickr 갤러리Flickr 갤러리

위에서 만들어둔 flickr-api 모듈을 이용해 실시간으로 Flickr 사진들을 보여주는 웹 서버를 만듭니다.
데모: http://photos-flickr.herokuapp.com

Flickr 갤러리 스펙

  • 일단은 라우팅 없이 모든 Request에 대해서 같은 Response를 줍니다.
  • Flickr에서 받아온 사진 100개를 포함한 HTML로 문서를 Reponse로 줍니다.
  • Bootstrap Flat-UI로 문서를 꾸미고, Bootstrap 그리드 시스템을 이용해 한 행에 4개의 사진이 있도록 합니다.
  • JavaScript를 이용해서 사진을 클릭하면 사진이 확대되고, 큰 사진을 다시 클릭하면 사라지도록 합니다.

flickr-app.js

const flickr = require('./flickr-api');
const http = require('http');
const fs = require('fs');

const htmlHeader = fs.readFileSync('./html/header.html'),
      htmlFooter = fs.readFileSync('./html/footer.html');

http.createServer((req, res) => {

  flickr.getRecentPhotos(photos => {

    let html = htmlHeader + `<div>`;

    for (let i in photos) {
      let photo = photos[i];

      if (i % 4 == 0) html += `</div><div class='row'>`;

      html += `
        <div class='col-xs-3' style="margin-top:25px">
          <img class="img-rounded img-responsive" src="${photo.small}" medium-src="${photo.medium}" style="cursor:pointer" />
          <img src="${photo.medium}" style="display:none" />
          <p style="text-overflow:ellipsis;overflow:hidden;white-space:nowrap;"><small>${photo.title}</small></p>
        </div>`;
    }

    html += `</div>` + htmlFooter;

    res.writeHeader(200, {'Content-Type': 'text/html'});
    res.write(html);
    res.end();

  });

}).listen(8888);

JavaScript for(var i in object|array) { … }
ES6 In Depth: let and const
ES6 Template literals
JavaScript에서 문자열(String)을 표현하는 방법은 "string", 'string’, string 세가지 방식이 있습니다. ES6에서 도입된 백틱()을 이용한 표기법을 템플릿 스트링이라고 합니다. 템플릿 스트링(Template String)을 이용하면 문자열을 여러줄로 쓰거나, 문자열 사이에 변수나 표현식을 간편하게 삽입 할 수 있습니다.Hello my name is ${expression}…` 이라고 쓰면 expression의 표현식이 계산되어 문자열 안에 삽입이 됩니다.

템플릿

위 웹 서버는 데이터에 따라서 HTML 조각을 만들어 하나의 HTML 문서로 조합 한 후 Response를 주고 있습니다. 즉, 작동은 하지만 뷰와 데이터가 뒤섞여 있습니다. 이런 방식은 데이터의 구조나 웹 페이지의 디자인이 바뀔 때마다 많은 수정 사항을 동반합니다. 뷰와 데이터를 분리해 확장성과 유지보수의 편리함을 꾀하기 위해서 템플릿(Template)이라는 개념을 도입합니다.

  • HTML 파일(template.html)을 읽습니다. 이때 데이터가 들어갈 부분에 특수한 표시자({photos})를 만들어둡니다.
  • 표시자가 있는 모든 부분을 데이터와 관련된 태그들로 치환합니다.
  • 이때 {photos}란 표식은 임의로 정한 것이고 어떤 표식이라도 html 태그와 구분만 된다면 좋습니다.

flickr-template.js

const flickr = require('./flickr-api');
const http = require('http');
const fs = require('fs');

const htmlTemplate = fs.readFileSync('./html/template.html').toString();

http.createServer((req, res) => {

  flickr.getRecentPhotos(photos => {

    let photosHtml = renderPhotosHTML(photos);
    let response = htmlTemplate.replace("{photos}", photosHtml);

    res.writeHeader(200, {'Content-Type': 'text/html'});
    res.write(response);
    res.end();
  });

}).listen(8888);

function renderPhotosHTML(photos){
    var html = `<div>`;
    for (let i in photos) {
      let photo = photos[i];

      if (i % 4 == 0) html += `</div><div class='row'>`;

      html += `
        <div class='col-xs-3' style="margin-top:25px">
          <img class="img-rounded img-responsive" src="${photo.small}" medium-src="${photo.medium}" style="cursor:pointer" />
          <img src="${photo.medium}" style="display:none" />
          <p style="text-overflow:ellipsis;overflow:hidden;white-space:nowrap;"><small>${photo.title}</small></p>
        </div>`;
    }

    html += `</div>`;
    return html;
}

전체 소스코드

뷰와 데이터가 분리되면 웹 디자이너는 HTML 파일만으로 작업을 하면서 데이터와 관련된 부분에 특수한 표시자를 작성하면되고, 개발자는 데이터와 관련된 로직에만 신경을 쓰면되기 때문에 분업에도 유리합니다. 하지만 실상 위 코드도 renderPhotosHTML라는 함수로 템플릿이란 개념을 흉내만 냈을 뿐, 데이터와 뷰가 전혀 분리되지 않았습니다.
실제 템플릿 엔진들은 for문과 같은 제어 로직을 지원하여 데이터와 뷰를 완전히 분리 할 수 있도록 돕습니다. 추후에 상용 템플릿 엔진에 대해서 알아보도록 하겠습니다.

목차
4. 웹 백엔드
5. 데이터베이스
저자

김동욱

개발 경력 약 10년, 전문 분야는 웹 및 각종 응용 소프트웨어 플랫폼입니다. Codeflow를 운영하고 있습니다.

2018년 04월 10일 업데이트

지원되지 않는 웹 브라우저거나 예기치 않은 오류가 발생했습니다.