내가 직접 만든 Laravel 분석 패키지 (생산 환경을 거의 망칠 뻔)
구글 애널리틱스를 쓰지 않는 이유 (또는 왜 나는 휠을 다시 만들고 싶어하는가)
솔직히 말하면, 이 선택은 쉬운 것이 아니었습니다. 개발을 꽤 오래 해오면서 구글 애널리틱스(GA)에 익숙해졌거든요. 얼마나 간단한지: 사이트에 ID가 포함된 스크립트를 삽입하면 데이터가 애널리틱스 콘솔로 흐르기 시작합니다. 남은 건 트래픽이 들어오길 기다린 뒤 국가, 성별, 연령대 등으로 분석하는 일뿐이죠.
예전엔 그게 전부였지만, 오늘날 현실에서는 이 개념을 재고할 객관적인 이유가 있습니다. 현실을 직시해 봅시다: 2026년에 구글 애널리틱스를 연결하는 것은 앞문에 거대한 데드볼트를 설치하고 열쇠는 이웃에게 맡겨두는 것과 같습니다. 모든 것이 통제되는 듯 보이지만, 이웃은 당신이 언제 집에 돌아왔는지, 무엇을 샀는지, 왜 얼굴이 우울한지까지 정확히 알고 있습니다. 그리고 누군가가 당신을 방문해 익명을 원한다면, 이웃은 열쇠를 주지 않을 것이고, 당신에게도 그 사실을 알려주지 않을 겁니다. 이웃의 대답은 아마 이렇게 될 겁니다:
“아무도 안 왔어, 난 절대 잠들지 않아, 모든 게 통제돼 있어…”.
무슨 말이냐면요…
일반 데이터 보호 규정(GDPR)을 기억하시나요?
애널리틱스를 사용하려면 쿠키 동의 배너를 표시하고 사용자의 허가를 받아야 합니다. 여기서 문제가 발생합니다. 사용자의 90 %가 모든 쿠키를 허용하지 않고, 필수 쿠키만 허용합니다. 결국 구글 애널리틱스는 물거품이 됩니다. 게다가 사람들은 이런 배너에 지긋지긋합니다. 법적으로 배너를 없앨 방법이 있다면, 왜 안 할까요?
(데이터 보호법과 관련된 법적 문제를 피하는 방법은 GDPR Without the Headache: A Guide for Web Developers in Germany 를 참고하세요.)
GA를 포기하기로 결정했습니다. “제거” 버튼을 눌러 구글 생태계에서 벗어나면서 논리적인 질문이 떠올랐습니다: 그 빈자리를 누가 메워줄까? 숫자는 여전히 필요했으니까요.
검토한 후보들
| 도구 | 장점 | 단점 |
|---|---|---|
| Matomo (구 Piwik) | 올인원, 강력함 | 별도 PHP 서버, MySQL, 지속적인 관리 필요; 작은 프로젝트엔 과도함 |
| Plausible / Fathom | 세련되고 현대적이며 프라이버시 친화적 | 유료 구독이 필요하거나, VPS RAM을 많이 잡아먹는 Docker 셀프 호스팅 필요 |
“어제 독일 세금에 관한 내 글을 50명이 읽었다는 걸 알기 위해 전체 인프라를 구축해야 할까?” 라는 생각이 들었습니다. 그때 깨달았습니다: 나는 ‘콤바인 하베스터’가 아니라, 라라벨 애플리케이션 안에 바로 들어갈 수 있는 작은 정밀 메스가 필요하다는 것을.
내가 원하는 것은 가벼우면서도 아침 에스프레소 한 잔 같은, GDPR에 대해 귀찮게 묻지 않는 도구였습니다. 게다가 oleant.net 에서 누가 내 보안 도구를 두드리는지(사람인지 봇인지) 궁금했거든요. 이 블로그에도 같은 도구가 필요했으니, Composer로 쉽게 설치하고 Packagist에 공개할 수 있는 독립 패키지를 만들기로 했습니다.
코드를 직접 살펴보고 싶은 분들을 위해, 패키지 이름은 **oleant/laravel-visit-analytics**이며 Laravel 10/11/12와 호환됩니다. GitHub 링크:
시스템의 핵심: 미들웨어와 뒷맛의 마법
어떤 아키텍처를 사용할까? 구현 방법은 여러 가지가 있습니다. 다행히 라라벨은 훌륭한 미들웨어 메커니즘을 제공하죠. 저는 이를 고수하기로 했지만, 사용자에게 지연을 주지는 않을까 걱정되었습니다. 사용자는 내 오버헤드에 신경 쓸 필요가 없습니다. 데이터베이스가 다운되거나 뭔가 오류가 나면 어떻게 할까요? 500 오류 페이지가 사이트의 얼굴이 되는 건 원치 않으니까요.
그래서 중요한 결정을 내렸습니다 — terminate() 메소드를 활용하기로 한 겁니다. 라라벨 미들웨어에서 이것은 예의 바른 웨이터와 같습니다: 청구서를 건네고 미소 짓는 (handle()) 순간, 손님이 이미 떠난 뒤에 테이블을 닦고 팁을 기록합니다 (terminate()).
사용자는 이미 페이지를 받아 만족하고, 서버는 백그라운드에서 조용히 로그를 DB에 기록합니다. 무언가 잘못돼도 클라이언트는 만족한 채 떠나고, 웨이터는… 이번엔 팁을 못 받는 겁니다. (농담입니다.) 실제 동작 예시는 다음과 같습니다:
PHP
public function terminate(Request $request, Response $response): void
{
try {
// 관리자 등 비대상 클라이언트를 제외하는 로직 …
// …
// 이제 방문자를 DB에 기록합니다
$this->logVisit($request);
} catch (\Throwable $e) {
// DB가 잠시 쉬어도 500 오류로 사용자에게 알리지 않음
\Log::error(
"Analytics failed, but we're keeping our cool: " . $e->getMessage()
);
}
}
생활 팁: 모든 애널리틱스 코드는
try‑catch로 감싸야 합니다. 로거가 긴 User‑Agent 혹은 예상치 못한 케이스를 처리하지 못해 전체 프로젝트가 다운되는 일은 정말 어처구니없습니다.
GDPR on the Fly: How Not to Become Public Enemy No. 1
계속됩니다… (원문은 여기서 끊기지만 핵심 개념은 이미 전달되었습니다.)
패키지를 자유롭게 탐색하고, 자신의 프로젝트에 적용해 보시고, 의견을 알려 주세요!
화면에 “우리를 감시하고 있어, 형제” 라는 문구가 뜨는 경우
IP 익명화를 구현했습니다. 주소의 마지막 부분을 데이터베이스에 저장되기 전에 잘라버립니다. 이렇게 하면 사용자를 익명화하면서도 어느 국가·데이터센터 등에서 방문했는지는 알 수 있습니다. 데이터센터 봇에 대한 이야기는 더 많은 데이터가 쌓인 뒤에 다시 다루겠습니다.
IP 익명화 복귀
PHP
// Before: 192.168.1.154 -> After: 192.168.1.0
마스크를 쓴 군중을 보는 것과 같습니다: 5명이 왔다는 건 알지만, 그 중 누가 이웃인지는 전혀 모릅니다. 법도 만족하고, 양심도 편안합니다.
페이로드: 좋은 것만 모으기
처음엔 URL에 들어오는 모든 것을 DB에 저장하려 했습니다. 그런데 Livewire 로그를 보니 파라미터에 행성 반 정도의 상태가 넘쳐났고, DB가 폭발할 것이라는 걸 깨달았습니다. 그래서 array_intersect_key 를 이용해 config에 허용된 파라미터만 기록하도록 필터링을 구현했습니다. 이제 config에서 직접
