그간 바쁨을 핑계로 멈춰왔던 기록을 다시금 시작하고자 마음먹었습니다. 그 시작으로서, 제가 원하는 포맷으로 생각을 제대로 담아낼 수 있도록 웹사이트를 개편하는 작업을 진행하고 있습니다.
이 웹사이트는 2021년 구매한 Jekyll 기반의 블로그 템플릿에서부터 시작되었습니다. 구입한 지 오래된 만큼 시대에 맞지 않는 기능도 있고, 개발 블로그 템플릿이 아니었던 만큼 개발 블로그의 특성에 맞추어 바꾸어야 하는 부분도 많았는데요. 가장 눈에 띄었던 것은 템플릿의 기본 방문 데이터 수집 툴이었던 Google Analytics였습니다.
저는 A Cypherpunk’s Manifesto에 영향을 받아 데이터 주권과 프라이버시에 관심을 갖게 되었습니다. 그리고, Google Analytics는 여러 가지 면에서 그것과는 거리가 멀었습니다. 방문자에게 데이터 수집을 거부할 권리가 없고, Google에 데이터가 귀속되며, Google에 내 데이터를 사용할 권한이 주어지고, 쿠키 기반의 추적을 통한 섀도우 프로파일링이 가능해집니다. 또한, 무거운 스크립트가 사용자 경험을 저해하기도 하죠.
그 대안으로서 오픈 소스 분석 툴인 Umami를 Self-hosting하여 사용하게 되었습니다.
Umami란?

Umami는 트래픽 분석의 프라이버시 문제를 해결하기 위해 개발된 오픈 소스 분석 플랫폼으로, Google Analytics의 복잡한 추적 구조를 대체하며 쿠키 없이 방문자 데이터를 수집해 프라이버시를 보호하는 경량화 Self-hosting 툴입니다. Next.js 프론트 + PostgreSQL 데이터베이스로 구성된 단일 경량 앱 구조로 개발과 운영이 단순하고 편리합니다.
프라이버시를 우선하는 철학과 오픈 소스인 점, 배포와 운영이 쉽다는 점이 제 요구 사항과 일치하여 Umami를 선택하게 되었습니다.
Umami Analytics 적용하기
이제 실제로 Umami를 구축하여 블로그에 도입한 과정을 설명하겠습니다. 간편하고 빠른 구축을 위하여 Supabase를 데이터베이스로 사용하였고, Vercel로 호스팅하였습니다. 무료로 사용할 수 있는 점은 덤입니다.
Supabase 프로비저닝
먼저, Supabase에 접속하여 프로젝트를 생성합니다.

이후, 검색창에 ‘Connection String’을 검색하여 Umami에서 Supabase의 PostgreSQL에 접근할 수 있게 하는 데이터베이스 연결 문자열을 확인합니다.
이때 연결 포트는 반드시 5432여야 합니다. 6543 포트는 트랜잭션 모드 전용 포트인데, 트랜잭션 모드로 연결할 경우 최초 배포 시 Umami에서 내부적으로 테이블을 생성하는 데 사용되는 Prisma ORM이 정상적으로 동작하지 않아 배포가 실패하게 됩니다.

Umami Vercel 배포
Vercel에서는 내 GitHub에 있는 Repository를 바로 불러와서 빠르게 배포할 수 있습니다. 우리는 이 기능을 이용해서 Umami를 간단하게 배포해 보겠습니다.
먼저 GitHub에서 Umami Repository를 Fork 해야 합니다.
이후, Vercel에서 프로젝트를 생성하고, Overview 페이지 우측 상단의 Add New -> Project를 클릭하여 Import Git Repository 메뉴에서 Umami를 Import합니다.

Project 설정 화면에서 다른 것들은 그대로 두고, Environment Variables에 DATABASE_URL, HASH_SALT 환경변수를 추가합니다.
| 환경 변수 | 설명 | 값 |
|---|---|---|
DATABASE_URL |
데이터베이스 연결 URL | 아까 Supabase 데이터베이스에서 확인한 Connection String 입력 |
HASH_SALT |
데이터베이스 암호화에 사용되는 무작위 문자열 | 원하는 무작위 문자열 입력 |

Deploy 버튼을 누르고 배포가 완료되면 아래와 같이 Supabase 테이블이 자동 생성됩니다.

배포된 Umami 웹사이트에 접속하여 초기 계정(admin / umami)으로 로그인하고, 통계를 분석할 웹사이트의 도메인을 미리 등록해줍니다.

이후, 웹사이트 우측의 Edit 버튼을 눌러 추적 스크립트를 확인합니다. 실제 통계 수집은 발급되는 추적 스크립트를 웹사이트에 심은 이후부터 시작됩니다.
![]()
웹사이트에 Tracking code 심기
HTML 편집으로 적용하는 방법과 Jekyll 웹사이트에서 적용하는 경우 두 가지를 다루겠습니다.
HTML 편집
HTML의 <head> 태그 안에 아래와 같이 Umami Script를 추가하면 됩니다.
<head>
<meta charset="utf-8">
<meta name="viewport">
<title>Website Title</title>
<script defer src="https://your-umami-domain.vercel.app/script.js" data-website-id="your-website-id"></script>
</head>
Jekyll 웹사이트 적용
Umami의 태그 정보를 _config.yml(또는 다른 환경 변수 등록 경로)에 추가합니다.
# Umami Analytics
umami:
script-url: "https://your-umami-domain.vercel.app/script.js"
website-id: "your-website-id"
이후, includes/umami.html 파일을 생성하여 아래와 같이 Umami 연결 스크립트를 추가합니다.
<script async src="https://umami-theta-lemon.vercel.app/script.js" data-website-id="35f2b041-3ac1-4099-abe3-fa26327bf048"></script>
이후, _layouts/default.html(또는 기본 HTML 템플릿 파일)에 Umami 파일 include 스크립트를 추가합니다.
<body>
{% if site.data.settings.umami and site.data.settings.umami.website-id %}
{% include umami.html %}
{\% endif %}
{% include header.html %}
...
</body>
프로젝트를 재실행하면, 방문 데이터 수집이 정상적으로 진행되고 있는 것을 확인할 수 있습니다.

Opt-out 기능 추가
Umami로 마이그레이션한 목적이 프라이버시 존중이었던 만큼, 방문자에게도 데이터 수집을 거부할 권리가 주어져야 합니다. 이를 구현하기 위하여, 데이터 수집을 거부할 수 있는 Opt-out 버튼을 추가하였습니다.
Umami는 사용자 브라우저의 localStorage에 umami.disabled 변수가 true로 설정된 경우 데이터 수집을 중지합니다. 이를 이용하여 umami.disabled 변수의 값을 변경하는 간단한 스위치 버튼을 추가하였습니다.
{% if site.data.settings.umami and site.data.settings.umami.website-id %}
<span class="footer__opt-out-wrap">
<span class="footer__opt-out-label">Disable analytics</span>
<button type="button" class="footer__opt-out-switch" data-umami-opt-out role="switch" aria-checked="false" aria-label="Disable analytics" title="Analytics on">
<span class="footer__opt-out-track">
<span class="footer__opt-out-thumb"></span>
</span>
</button>
</span>
{% endif %}
var optOutSwitch = document.querySelector("[data-umami-opt-out]");
if (optOutSwitch) {
var umamiDisabledKey = "umami.disabled";
var wrap = optOutSwitch.closest(".footer__opt-out-wrap");
var labelEl = wrap ? wrap.querySelector(".footer__opt-out-label") : null;
var isOptOut = !!localStorage.getItem(umamiDisabledKey);
optOutSwitch.setAttribute("aria-checked", isOptOut ? "true" : "false");
optOutSwitch.title = isOptOut ? "Analytics off" : "Analytics on";
optOutSwitch.setAttribute("aria-label", isOptOut ? "Enable analytics" : "Disable analytics");
if (labelEl) {
labelEl.textContent = isOptOut ? "Enable analytics" : "Disable analytics";
}
optOutSwitch.addEventListener("click", function () {
if (localStorage.getItem(umamiDisabledKey)) {
localStorage.removeItem(umamiDisabledKey);
} else {
localStorage.setItem(umamiDisabledKey, "true");
}
location.reload();
});
}
본 웹페이지의 Footer에서 해당 버튼을 클릭하여 데이터 수집을 차단하실 수 있습니다.
마치며
하나씩 기능들을 만들고 동작에 대해 공부하며 이 웹사이트가 정말 내 웹사이트다라는 감각을 얻고 있습니다. 앞으로 꾸준히 발전시켜 나가며 경험을 공유하겠습니다.