어쩌다보니 블로그 업데이트를 연례 행사처럼 치루고 있다.

그동안 Gatsby를 잘 사용하고 있었지만 걷어내기로 했다. netlify 인수 이후엔 업데이트 자체도 상당히 정체된 상황이다. 새 버전에 대한 이야기가 있긴 하지만 수많은 플러그인이 동시에 관리되고 업데이트 해왔던 그간의 방향성을 봤을 때 조금 의구심이 들 수 밖에 없는 상황이다. 거기에다 Gatsby Cloud도 접은 것 보면 사용자로서는 좀 김이 빠진다. 정말 좋아하는 프로젝트고 프로덕트였는데 프로덕트의 성숙도와는 달리 순식간에 불투명해져버린 상황이 많이 아쉽다.

react 기반의 정적 사이트 생성기를 계속 사용할까 찾아봤다. Astro가 요즘 유행이기도 하고 Vercel 제품도 사용해보고 싶긴 하지만... 1) 웹브라우저의 기본적인 사이클과 잘 맞지 않는 기분이 종종 들었고 (아무래도 웹사이트니까 SPA같은 느낌이 드는게 묘하게 어색한 그런 기분), 2) 마크다운에 마음대로 인라인 스크립트나 스타일을 넣는 일도 좀 불편한 기분이고, 3) 간단한 html을 넣으려고 이런 저런 컴포넌트를 오가야 하는 일도 조금 번거로웠다. 물론 react 문제 아니고 내가 구성한 방식의 문제겠지만서도.

다른 정적 생성기를 사용할까 찾아보다가 그냥 간단하게 만들기로 했다. 페이지네이션 없이 그냥 목록만 제공할 거니까, 복잡하게 생각하지 말고 그냥 생각 가는데로 간단하게 만들기로 했다.

그동안 remark.js를 gatsby 통해서 잘 사용해왔으니까 이젠 직접 사용하기만 하면 될 것 같았다. 이미 라우팅은 각각 마크다운 파일에 정의된 frontmatter를 사용해서 만들어내고 있었기 때문에 frontmatter만 읽어 처리할 수 있으면 되는 상황이었다. remark.js과 여러 플러그인 통해서 별 문제 없이 구현할 수 있었다. 사실 gatsby 대부분 마크다운과 관련된 플러그인은 remark.js의 플러그인을 랩핑한 것이라서, 좀 더 날 것의 형태로 사용할 수 있다는 것은 오히려 장점이었다.

템플릿도 별도 엔진을 사용할까 하다가도 새로운 템플릿 문법을 보고 짜는 것도 번거롭고 이미 리터럴을 잘 쓰고 있으니 간단하게 템플릿 리터럴로 구현했다. 페이지 컨텐츠는 라우팅과 분리했지만 템플릿은 라우팅을 기준으로 불러오게 해서 페이지마다 스타일을 바꾸기 쉽게 만들었다. 그 외에도 sitemap.xml, RSS 피드, 리다이렉션, 이미지 처리 등 필요한 요소도 추가했다.

아직 별도의 캐시를 넣은 것도 아닌데 2~3분 걸리던 빌드가 30초대로 내려왔다. 빌드 로그를 보면 빌드 자체는 금방인데 필드 환경을 불러오는 시간이 꽤 길었다. netlify에서 cloudflare pages로 변경하고 싶은데 cloudflare pages는 빌드 시간 제한이 아니라 횟수 제한이라서 사이트가 좀 더 정리되면 그때 옮길 생각이다.

블로그를 변경하면서 간단한 템플릿 엔진이 필요했다. 템플릿 엔진을 사용하려고 살펴보니 새 문법을 배우는 것도 번거롭고 정말 작은 부분 때문에 의존성을 추가하는 것도 별로 맘에 들지 않았다. 그동안 잘 사용해온 템플릿 리터럴을 그냥 사용할 방법은 없을까 찾아보다가 Function 생성자를 사용해서 다음처럼 작성할 수 있었다.

"use strict";

function template(html, params = {}) {
  const keys = Object.keys(params);
  const ps = keys.map(k => params[k]);
  return new Function(
    ...keys,
    'return (`' + html + '`)')
  (...ps);
}

Function 생성자는 파라미터 목록과 함수 내에 들어갈 내용을 문자열로 받고 그 함수를 반환한다. 위에서 보면 전달한 params에서 키 값을 수집해 함수를 생성하는 일에 사용하고 또 키 이름 순서에 맞게 ps 배열을 만들어 함수에 전달했다.

이제 html로 문자열을 전달하면 템플릿 리터럴로 이용할 수 있다.

const welcomePage = '<h1>${name}님 환영합니다.</h1>'
template(welcomePage, {name: '거북이'})
// "<h1>거북이님 환영합니다.</h1>"

다만 "`" 문자 사용에 주의해야 한다.

const welcomePage = '<h1>\\`${name}\\`님 환영합니다.</h1>'
template(welcomePage, {name: '거북이'})
// "<h1>`거북이`님 환영합니다.</h1>"

다른 도움 함수

부분함수도 손쉽게 만들 수 있다.

function partial(html) {
  return function render(params) {
    return template(html, params);
  }
}
const accountPage = '<button>${name}님의 계정 정보</button>'
const accountPartial = partial(accountPage)

const me = accountPartial({name: '나'})
// "<button>나님의 계정 정보</button>"
const koala = accountPartial({name: '코알라'})
// "<button>코알라님의 계정 정보</button>"

템플릿 함수가 필요하다면 다음처럼 params에 함께 전달할 수 있다.

const balance = () => 3000 // 또는 좀 더 복잡한 코드

const balanceInfo = template('<strong>잔고: ${balance()}</strong>')({balance})

nodejs에서 사용하고 있기 때문에 nodejs의 모듈을 사용해 html을 불러오는 함수를 다음처럼 작성했다. 또한 템플릿 내부에서 별도의 파일을 불러올 수 있도록 load 함수를 경로 맥락과 함께 템플릿 내로 전달했다.

import fs from 'fs'
import path from 'path'

function load(filename, basePath = '.') {
  const dirname = path.dirname(filename);
  const html = fs.readFileSync(path.join(basePath, filename));
  const partialHtml = partial(html);

  return function loadPartial(params) {
    return partialHtml({
      ...params,
      load: function loadFromSubDir(filename) {
        return load(filename, dirname);
      }
    })
  }
}
<!-- ./templates/index.html -->
<h1>안녕하세요!</h1>

${load('./partials/info.html')({user})}
<!-- ./templates/partials/info.html -->
<p>${user.name}님의 계정 정보</p>
load('./template/index.html')({user: {name: '당근'}})
// "<h1>안녕하세요!</h1>\n\n<p>당근님의 계정 정보</p>"

템플릿 내에서 다른 유틸리티 함수가 더 필요하다면 위 함수와 같이 더 추가하면 되겠다.

보안 고려하기

사용자 입력을 직접 템플릿에 사용하는 것은 당연히 위험하다! 상황에 맞게 적절한 예비 조치가 필요하다.

function sanitize(text) {
  return text.replace(/[^ㄱ-ㅎ|가-힣|a-z|A-Z|0-9| ]+/gi, "")
}

template(
  "<p>${sanitize(name)}님 안녕하세요!</p>", {
    name: "<strong>헤헤</strong>",
    sanitize,
  })
// "<p>strong헤헤strong님 안녕하세요!</p>"

이렇게 작은 템플릿 함수를 작성해봤다. 템플릿 리터럴을 활용하면 몇 줄 안되는 코드로도 템플릿 함수를 구현할 수 있었다. 간단한 수준에서라면 이 함수로도 충분히 활용 가능하겠지만 이 접근 방식의 한계(Function 내에서 바깥 스코프에 접근할 수 있는 등)로 보안 문제가 발생할 수 있다. 이런게 가능하다 정도로만 이해하고 제대로 된 라이브러리를 활용하는 것이 더 바람직하다.

(📝🚌 @write_bus)

가능한 한 자주 글을 써라. 그게 출판될 거라는 생각으로가 아니라, 악기 연주를 배운다는 생각으로.

Write as often as possible, not with the idea at once of getting into print, but as if you were learning an instrument.

-- J.B. priestley

Google Chrome를 주로 사용하고 있는데 언제부터인지 브라우저 내 PDF 뷰어가 엄청 느려졌다. Mozilla의 pdf.js가 Chromium 확장을 제공하고 있어서 설치해봤는데 만족스럽다. Manifest V2로 작성된 확장이라서 크롬 웹 스토어에 있는 버전은 더이상 관리되지 않는 것 같다. 그래도 직접 빌드를 한 경우엔 아직까지도 문제 없이 설치할 수 있다.

$ git clone https://github.com/mozilla/pdf.js.git && cd pdf.js
$ npm install --global glup-cli
$ npm install
$ glup chromium

Google Chrome에서 chrome://extensions를 연 후 좌측 상단에 Load unpacked 버튼을 클릭, 그리고 pdf.js 폴더 내에 build/chromium을 선택하면 확장을 설치할 수 있다. Firefox에 내장되어 있는 pdf 뷰어와 동일하게 동작한다.

웹사이트 설정

웹페이지 색상을 선택하세요

Darkreader 플러그인으로 선택한 색상이 제대로 표시되지 않을 수 있습니다.