정적 웹 프레임워크(직지) 개발지

7월 14일, 2017


SW마에스트로 1단계 & 2단계 프로젝트 중 정적 웹을 사용하며 만들었던 “직지”에 대한 뒷 이야기와 당시 저의 고민, 그리고 해결방법에 대한 포스트입니다.

직지 레파지토리: github.com/Prev/jikji
관련 포스트: 정적 웹으로 서비스 구현하기 (목표와 방향에 관한 내용)

직지: “정적 웹 프레임워크”로, 무언가를 찍어낸다는 의미를 살려 세계최초의 금속활자인 직지심체요절의 앞 글자를 떼서 이름지었습니다.


1. 초기 버전 설계

1.1. 정적 웹을 왜 쓰게 되었나?

SW마에스트로 1단계 프로젝트를 계기로 정적 웹으로 서비스를 만들게 되었습니다. 이 프로젝트의 컨셉 자체가 Github Pages를 이용하여 무료료 서비스를 운영하는 것이었기에 정적 웹 사용은 전제로 깔린 조건이었고, 어떻게 이를 잘 활용하느냐가 문제였지 정적 웹을 사용할지 말지는 고민이 아니었습니다.

1.2. 왜 직지를 만들게 되었나?

처음에는 당연히 Jekyll 등의 기존 정적 웹 관련 라이브러리를 사용하려 했습니다. 하지만 이런 라이브러리는 주로 Markdown 파일을 HTML 파일로 변환해주는 기능이 주로, 애초에 블로그를 위해 만들어진 라이브러리가 대부분이었습니다.

프로젝트에서는 API 서버를 통해서 데이터를 가져오고, 신규 데이터에 대한 페이지를 생성하는 것이 목표였는데 기존 라이브러리로는 적합하지 않았고, 직접 라이브러리를 만들어야겠다고 생각했습니다.

1.3. 고려사항 1: Code-less

초기에는 정적 웹 개발자가 코드를 최대한 작성하지 않도록 설계를 했습니다. API 서버에서 정보를 가공해서 보내주기 때문에, 위의 데이터를 바탕으로 사이트 설정프론트엔드 코드만 작성하여 정적 웹을 운영할 수 있다면, 상당히 편리할 것이라 생각했습니다.

사이트 설정도 python 코드를 몰라도 쉽게 할 수 있게끔 만들고 싶었습니다. 이를 위해 json과 xml 등을 이용하여 사이트 설정과 관련된 rule을 정리했습니다.

그 중 특이한 것이 사이트의 페이지 목록을 정의하는 pages.xml 파일입니다.

pages.xml

<?xml version="1.0" encoding="UTF-8" ?>
<site>
	{% set all_docs = model.get('/_all_docs') %}
	<page>
		<url>/</url>
		<context>{{ all_docs }}</context>
		<template>home.html</template>
	</page>
	{% for id in all_docs.rows %}
	<page>
		<url>/doc/{{ id }}/</url>
		<context>{{ model.get('/doc/' + id) }}</context>
		<template>document.html</template>
	</page>
	{% endfor %}
</site>

(all_docs를 바탕으로 인덱스 페이지와, 각각의 doc당 하나의 페이지를 만드는 설정의 들어있는 pages.xml)

위 파일의 특이한 점은, 코드를 사용하지 않고 템플릿 문법을 사용해서 정의한다는 점입니다. 이를 통해 Code-less라는 특징을 이룰 수 있었고, 퍼블리셔 처럼 백엔드 코드를 잘 모르는 사람도 직지를 통해서 정적 웹을 만들 수 있도록 만들었습니다.

1.4. 고려사항 2: 개발 속도

다음으로 고려했던 부분은 개발속도입니다. HTML을 변경하면 변경 사항을 브라우저에서 바로 확인해야 했는데, Jekyll에서는 이가 매우 느렸습니다. Jekyll에서는 문서를 수정하면 자체적으로 reload를 해주는 serve라는 기능을 제공하는데, 페이지가 100개 정도만 되어도 5초가량의 시간이 걸리는 등 개발속도에 상당한 문제가 있었습니다.

jekyll-screenshot

(파일을 한번 수정할 때마다 5초의 시간을 기다려야 하는 Jekyll 어플리케이션의 모습)

문제를 우회해서 해결 할 수는 있긴 합니다. Jekyll에서는 가장 최근 글 n개만 재생성하게끔 하는 옵션을 제공하여, 그 옵션을 사용하면 reload 시간을 줄일 수 있습니다. 하지만 내가 보고자 하는 페이지를 볼 수 없다는 점은 문제를 완벽하게 해결하지 못해줍니다.

이 문제를 근본적으로 해결하기 위해 “전체 재생성”이 아닌, 개발자의 브라우저 접속에 기반한 reload 모드를 개발했습니다.

Listen 모드

“파일이 수정되었을 때 수정사항을 반영하여 사이트를 완벽하게 업데이트 하는 것”에 목표를 두지 않고, “개발자는 코드를 수정한 다음 어떤 행동을 할까“에 집중했습니다.

직지의 사용자는 아마 프론트엔드 개발자일 것이고, HTML이나 CSS, JS 코드를 수정하고 그 코드를 사용하는 대표적인 페이지를 새로고침하여 잘 반영되었는지 확인 할 것입니다. 사실 파일 변경트래킹을 할 필요가 없고, 어떤 페이지를 새로고침했을 때 그 페이지만 잘 보여주면 되는 것이죠.

Listen 모드에서는 Flask 서버를 활용하여 개발자가 어떤 URL로 접속했는지 우선 파악합니다. 이후 그 URL에 대응하는 페이지를 찾고 즉시 렌더링하여 보여줍니다. 사전에 렌더링을 하는 정적 웹의 특징과는 전혀 다르게, listen 모드에서는 일반적인 Dynamic web 방식으로 동작하게끔 구조를 설계 한 것이라고 볼 수 있습니다.

jikji-listen-mode

위 코드는 접속한 URL이 어플리케이션의 페이지 목록에 있으면 해당 페이지를 렌더링하여 출력하고, 그렇지 않고 asset 파일이면 asset을 출력해주고, 그마저 아니면 404를 출력하는 코드입니다. 정적 웹 라이브러리지만 동적 웹의 특징을 교묘하게 이용한 기능이라고 할 수 있죠.

2. 초기설계의 문제

처음 직지를 설계할 때에는, 목표로 했던 Code-less라는 특징과 개발속도의 향상을 이룰 수 있어 만족했습니다. 하지만 사이트가 초기보다 훨씬 커지면서, 생각치 못한 다양한 문제를 발견할 수 있었습니다.

2.1. pages.xml 문제

pages.xml은 템플릿 코드로 작성하는데, 내부적으로 당연히 이 파일을 우선 렌더링하고, 렌더링된 결과를 XML 파싱을 하며 페이지 리스트를 가져오게 됩니다. 이 파일에는 모델 데이터(context)들이 다수 들어가게 되는데 이 데이터로 인해 렌더링된 pages.xml의 용량이 너무 커지는 문제가 있었고, 이 때문에 생성 시간이 증가하거나 오류가 발생하는 문제가 생기게 되었습니다.

<?xml version="1.0" encoding="UTF-8" ?>
<site>
	<page>
		<url>/</url>
		<context>[{"id": 1, "data": "..."}, ...]</context>
		<template>home.html</template>
	</page>
	<page>
		<url>/doc/1/</url>
		<context>{"title": "...", "description": "...", "content": "..."}</context>
		<template>document.html</template>
	</page>
    <page>
		<url>/doc/2/</url>
		<context>{"title": "...", "description": "...", "content": "..."}</context>
		<template>document.html</template>
	</page>
     ...
</site>

(렌더링된 pages.xml)

2.2. Code-less 문제

Code-less는 분명 매력적인 특징이지만, 아무리 정적 웹이라 할 지라도 사이트가 커지면 코드 없이 개발 하는 것은 불가능하였습니다. 결국 코드를 선택적으로 사용할 수 있는 기능들을 추가했지만, 근본적으로 Design Pattern 없이 설계 된 라이브러리였고, 전체 코드가 매우 난잡해지는 문제가 있었습니다.

3. 버전 2.0

초기버전에 문제가 많음을 깨닫고, 2.0 버전은 처음부터 완전히 재설계하여 개발하게 되었습니다. 2.0 버전은 수십, 수백만 페이지에도 문제가 없는 구조로 설계하고, 코드의 품질도 높이기 위해 많은 고려를 하였습니다.

이를 위해 2.0 버전부터 직지는 단순 라이브러리 수준이 아닌, 정적 웹 어플리케이션을 위한 프레임워크 규모로 설계하게 되었습니다.

3.1. 고려사항 1: Design Pattern

MVC 패턴?

Code-less는 대규모 사이트에서 불가능하다고 판단하였고, 그럼 어떻게 코드를 작성시켜야 하는가에 대한 고민이 있었습니다. 동적 웹에서는 큰 고민 없이 MVC 패턴을 사용하면 되지만, 정적 웹에서도 이 디자인 패턴이 정말 적절한가를 우선 살펴보았습니다.

동적 웹에서의 일반적인 MVC 구성요소 다이어그램은 다음과 같습니다.

MVC Diagram

위의 다이어그램을 정적 웹의 관점에서 살펴보자면, 우선 정적 웹에서는 사용자가 완성된 HTML파일만 보기 때문에, 프로세스에 사용자가 없다고 볼 수 있습니다. 사용자가 없다고 생각하자면 Router도 필요가 없고, Controller의 역할도 축소되게 됩니다. 즉, 기존 MVC 패턴정적 웹에 사용하기에는 부적절하다고 판단했습니다.

Page-oriented Concept

그렇다면 정적 웹에서 중요하게 다루어야 할 것이 무엇인지를 생각 해 보았습니다. 동적 웹에서는 사용자 인터렉션이 매우 중요하며, 이에 중심해서 디자인 패턴을 설계하였습니다. 하지만 정적 웹사용자와 관련 없이 “생성 및 배포하는 페이지의 목록”을 갖고 시작하며, 그 페이지들을 생성하고 배포하기 위한 순차적인 프로세스가 중요하다고 생각했습니다.

정적 웹 작업을 진행하기 위해서는 우선 특정 페이지에 대한 데이터(모델)를 가져 와야 하고, 데이터를 바탕으로 페이지를 렌더링 해야하며, 렌더링된 결과를 스토리지에 업로드(퍼블리시) 하여야 하고, 마지막으로 페이지에 관련된 데이터(모델)는 업데이트 되었음을 API 서버에 알려야 합니다.

page-process

위 프로세스는 모두 특정 Page를 만들기 위한 순차적인 과정이라고 할 수 있으며, Page라는 단위 중심적으로 풀어 나간다면 보다 더 명확하게 정적 웹 프로세스를 설계할 수 있을 것이라 생각했습니다.

이에 따라 사용자 인터렉션을 배제하고 Page를 중심으로 한 컨셉을 직지의 주요한 특징으로 잡게 되었습니다.

Page-View Pattern

Page를 어플리케이션의 중심 개념으로 정하였지만, Page는 하나의 URL과 데이터, 렌더링 된 HTML 결과 등을 갖는 인스턴스지, 인터페이스는 아닙니다. 인스턴스 관리를 사용자(개발자)에게 맡길 수 없으므로, Page의 인터페이스 개념을 View라 정의하고 사용자 입장에서는 View 위주로 어플리케이션정의하게끔 직지를 설계하였습니다.

View에서는 데이터를 가지고 어떻게 결과를 내보낼지에 대한 함수, URL 규칙, 그리고 템플릿 파일의 주소가 정의되어 있고, 데이터(모델)와 함께 View를 실행하면 URLContent를 가진 Page 인스턴스를 만들게 됩니다.

하나의 View는 여러 개의 Pages를 만들게 되며, ViewPage가 가지고 있는 속성은 아래와 같습니다.

View
  • URL Rule: /event/{$id}
  • Template path: event_magazine.html
  • View function
Page
  • Model(Data)
  • URL: /event/23123
  • Content: <html>....</html>

ViewMVC패턴의 Controller와 유사합니다. 다만 큰 차이점은 MVC에서의 Controller는 브라우저의 URL을 기반으로 Model과 View의 통신을 거쳐 브라우저에 결과를 내보내지만, 직지에서의 View는 시스템에서 입력받은 데이터를 바탕으로 Page 들의 목록을 생성하는 데 있습니다.


(사용자가 정의한 index, comment View로, group_name와 people_name인자를 넣으면
이에 해당되는 Page 인스턴스를 만들 수 있음)

위 코드는 직지를 사용하는 샘플 정적 웹 어플리케이션의 View 코드입니다. 결과적으론 Page-View Pattern을 통해 코드의 가독성을 높이고, 내부 프로세스를 명료하게 구현할 수 있었습니다.

3.2. 고려 사항 2: 속도

직지 2.0에서는 수십, 수백만 규모의 페이지를 관리할 수 있도록 만든 정적 웹 프레임워크입니다. 그만큼 “속도” 문제는 상당히 중요했고, 속도를 증가시키기 위해 속도에 영향을 주는 요소를 분석 해 보았습니다.

정적 웹에서 속도에 영향을 주는 요소는 크게 아래의 2가지가 있습니다.

  1. 생성해야 할 페이지의 갯수
  2. 프로그램 동작 시간 자체

생성해야 할 페이지의 갯수(1)를 줄이기 위해 직지에서는 CSS, JS, 이미지 등의 스태틱 파일의 변경시간을 관리합니다. 또한 한번 생성 한 페이지는 다시 생성하지 않도록 View 단에서 다양한 Callback을 지원하여, 이를 통해 생성이 완료된 페이지를 API 서버에 쉽게 알릴 수 있습니다.

프로그램 동작 시간 자체(2)를 줄이기 위해 직지에서는 내부적으로 병렬처리를 사용합니다. 동시에 다수 페이지를 렌더링하면 속도를 상당히 끌어 올릴 수 있지만, 병렬처리를 위해 또 고려해야 할 많은 사항들이 있었습니다.

병렬처리 이슈

프레임워크에서 멀티프로세스로 페이지를 생성하는 것을 구현하는 작업은 그리 어려운 작업은 아닙니다. 실제로 Python multiprocessing.Pool 내장 라이브러리를 이용하여 간단하게 구현을 했구요.

다만 문제는 페이지 렌더링 과정이 멀티프로세싱으로 작동함을 사용자에게 학습시키고 State-less 하게 작성해야함을 알려야한다는 것과, 작업을 프로세스별로 잘 분배시키는 것에 있습니다.

이에 따라 프레임워크 설계 처음부터 병렬처리를 고려하였고, 렌더링 작업은 모두 View 함수 내부에서 작성하도록 디자인했습니다. 디자인 방식은 간단했는데, 일반적인 동적 웹에서 response 함수(혹은 Controller)는 멀티프로세스로 동작합니다. 같은 매개변수가 입력되었을 때, 보통 같은 동작을 하고 같은 결과를 반환하죠. View 함수는 이처럼 매개변수를 바탕으로 렌더링 결과를 반환하는 구조를 갖고 있기 때문에 사용자는 자연스럽게 state-less한 코드를 작성하게 될 것이며, 프레임워크에선 response함수만 호출하면 되므로 작업 분배도 쉬워집니다.


(병렬처리의 효과로 초당 250개 이상의 페이지를 생성하는 모습)

3.3. 고려사항 3: Error Control

정적 웹은 오류 제어에도 많은 신경을 써주어야 합니다. 우선 크게 고려했던 오류 제어로는 Fault Tolerance(일부 시스템에 문제가 있어도 부분적으로 정상 동작해야함)와 Atomic Issue(단위 작업 문제)가 있습니다.

Fault Tolerance

동적 웹에서는 보통 별도의 예외 처리 없이도 어떤 한 페이지에 문제가 있으면 그 페이지만 오류가 발생하지 전체 시스템이 마비되지는 않습니다. 하지만 정적 웹에서는 어떤 한 페이지에 문제가 생겼을 때, 자칫하면 전체 시스템이 마비될 수 있습니다. 즉, 프레임워크에서 Fault Tolerance를 고려해서 구현해야 한다는 소리죠.

직지에서는 페이지 생성 도중 오류가 발생할 경우 해당 페이지는 건너뛰는 방법을 통해 사이트 생성 전체가 마비되는 상황을 막고 있습니다. 또한 오류가 있는 페이지를 사용자가 처리할 수 있도록 페이지 생성 작업이 끝난 이후, 성공한 페이지의 목록오류가 발생한 페이지의 목록구분하여 제공합니다. 렌더링 결과물을 배포 할 때에는 성공한 페이지만 업로드하고, “배포 완료처리 API”에는 성공한 페이지의 목록만 보내기 때문에, 오류가 발생한 페이지는 다시 생성 작업을 진행하게끔 하는 내부 프로세스도 가지고 있습니다.


(오류가 발생해도 무시하고 진행하여 37개 페이지가 성공하고 6개 페이지에서 오류가 난 상태로 Generation을 끝낸 모습)

Atomic Issue

애초에 모든 페이지가 오류가 안날 수 없다는 가정도 하였고, Fault Tolerance로 인해 예외 처리도 하면서 생성, 배포 작업을 진행하게 될텐데, 이 때문에 Atomic 문제가 발생할 수 있습니다.

예를 들어 특정 인물에 대한 모듈에 “타임라인”, “요약”, “사진” 탭이 있는데, “요약” 탭에만 문제가 생겼을 경우, 이 인물에 페이지 생성이 성공적이었다고 API를 호출하면 안됩니다. 어떤 상황에서는 서비스의 자연스러움을 위해 “요약”탭만 건너뛰는게 아니라, “타임라인” 및 “사진” 탭 등 그 인물에 관련된 모든 페이지가 하나의 단위로 처리되어 생성되지 않는 것이 나을 수도 있습니다.

즉, 하나의 모델을 공유하는 페이지들은 하나의 단위 작업(Atomic)으로 취급되어야 하며, 이 문제를 해결하기 위해 PageGroup 이라는 개념을 내부적으로 추가 도입해서 사용하고 있습니다. (하나의 PageGroup은 하나의 모델과 여러개의 Page(View)를 가지고 있는 인스턴스입니다.)

직지에서는 기본적으로 PageGroup 단위로 업데이트 되었음을 API 서버에 알리며, 옵션에 따라 페이지 중 하나라도 문제가 있는 PageGroup은 배포가 안되게끔 설정할 수도 있습니다.


("57161"번 이벤트를 모델로 가지는 페이지그룹에서 5개 페이지 중 4개만 성공한 상황)

4. 결론

직지(Jikji)는 현재 2.1.4 버전까지 개발이 되었으며, 테스트 코드와 함께 관리되고 있습니다. (Github에서 보기)

jikji-readme

SW마에스트로에서 진행한 메멘토 프로젝트(memento.live)는 직지를 이용하여 정적 웹으로 운영되고 있습니다. 매일 새벽마다 자동으로 생성 및 배포 작업이 진행되고 있으며, 초당 100개 이상의 페이지를 생성할 만큼 매우 빠르게 동작합니다.

직지를 통해 만들어진 HTML 파일들은 Amazon S3 서버에 업로드되어 관리되고 있으며, 사용자가 늘어나도 운영 비용은 크게 늘어나지 않습니다. (S3는 EC2와 비교도 안될 정도로 매우 저렴합니다!)

memento-rp

정적 웹은 일반적으로 실제 서비스에서 잘 사용되지 않는 기술인 만큼 시행착오도 많았지만, 결과적으로는 정적 웹을 서비스 운영 수준까지 활용할 수 있는 능력을 기른 것 같고, 다양한 관점에서 설계를 하며 많은 것을 배울 수 있었습니다.

일반적인 경우에 정적 웹을 굳이 채택했다가 부대 비용이 더 나갈 수 있겠지만, 특정한 상황에는 정적 웹을 사용하는 것도 상당히 의미있는 경험이 될 수 있다고 생각합니다. 상황만 맞는다면 여러분도 정적 웹을 시도 해 보세요. 나름 흥미로운 경험이 될 것이라 생각합니다!