728x90

장애 대응 중에 구글에서 에러 메시지 검색했는데, 상위 3개가 전부 "이 기사를 계속 읽으려면 구독하세요"였던 경험. 다들 한 번쯤 있을 거다. 나는 새벽 3시에 Kafka 컨슈머 랙 관련 아티클 찾다가 미디엄 페이월에 세 번 연속 막히고 나서 그냥 소스 코드를 읽기 시작했다.

2026년 8월 21일 Kagi 체인지로그에 짧게 한 줄이 올라왔다. "we've added a setting for removing paywalled links from search results automatically." Stocks 위젯 개편 소식에 딸려 나온 부록 같은 문장인데, HN에서 반응이 꽤 컸다. 왜 이 한 줄이 검색 품질 논의의 핵심인지, 그리고 인프라 하는 사람 입장에서 뭘 배울 수 있는지 정리해본다.

왜 지금 이게 화제인가

검색 결과에 페이월이 섞이는 건 단순한 불편이 아니라 정보 비용의 문제다. 구독 안 한 사이트가 상위에 뜨면, 사용자는 클릭 → 로딩 → 오버레이 확인 → 뒤로가기라는 4단계를 낭비한다. 트러블슈팅 중이면 이 왕복이 체감상 훨씬 크다.

그런데 왜 구글은 이걸 안 해줄까. 구조적인 이유가 있다.

  • 광고 기반 검색엔진은 퍼블리셔가 곧 생태계 파트너다. 유료 언론사 링크를 자동으로 빼는 건 그 관계를 정면으로 건드린다.
  • 구글은 오히려 반대 방향으로 갔다. NewsArticle 스키마의 isAccessibleForFree 필드는 페이월 콘텐츠를 배제하기 위한 게 아니라, 클로킹으로 오해받지 않게 정상 색인시키기 위한 장치에 가깝다.
  • Kagi는 구독료로 굴러간다. 사용자가 유일한 고객이니까 "이거 빼주세요"를 그냥 들어줄 수 있다. 인센티브 구조가 다르다.

이건 기술 난이도의 문제가 아니라 비즈니스 모델의 문제였다는 뜻이다. Kagi가 AI 기능 전체 off 토글(settings/ai)을 넣은 것도 같은 맥락이다. 체인지로그에서 직접 "we want to stay true to putting you in control of your search engine"이라고 썼다.

페이월을 어떻게 판별하나 — 동작 원리

Kagi가 정확히 어떤 방식을 쓰는지는 공식적으로 공개된 게 없다. 체인지로그 한 줄이 전부다. 다만 이 문제를 푸는 방법은 업계적으로 몇 가지로 수렴한다. 내가 사내 문서 크롤러 만들 때 겪은 것과 겹치는 부분이 많다.

1) 구조화 데이터 (가장 싸고 가장 안 믿음직함)

schema.org의 isAccessibleForFree: false. 퍼블리셔가 스스로 붙이는 값이라 신뢰도는 자진신고 수준이다.

curl -s https://example-news.com/article/123 \
  | grep -o '"isAccessibleForFree":[^,}]*'

# 출력 예시
"isAccessibleForFree":false

붙어 있으면 확실하지만, 안 붙어 있다고 무료라는 보장이 전혀 없다. 재현율(recall)이 형편없다.

2) 도메인 리스트 (실용적, 유지보수 지옥)

NYT, WSJ, Bloomberg, Medium... 알려진 페이월 도메인을 리스트로 관리한다. 단순하지만 두 가지가 깨진다. 하나는 같은 도메인 안에서 무료 기사와 유료 기사가 섞이는 경우(NYT 오피니언 일부, Medium의 non-member 스토리), 다른 하나는 미터링 페이월이다. "이번 달 3개까지 무료"는 서버가 아니라 브라우저 로컬스토리지 상태에 달려 있어서 크롤러 입장에서 판정 자체가 불가능하다.

3) 콘텐츠 시그널 (제일 정확, 제일 비쌈)

본문 텍스트에서 패턴을 잡는다. 실제로 돌려볼 수 있는 최소 버전:

#!/usr/bin/env bash
# paywall-probe.sh — 페이월 의심 시그널 간이 탐지
URL="$1"
BODY=$(curl -sL -A "Mozilla/5.0" --max-time 10 "$URL")

echo "== body length: $(echo "$BODY" | wc -c) bytes"
echo "$BODY" | grep -o '"isAccessibleForFree":[^,}]*' | head -1
echo "$BODY" | grep -io -E 'subscribe to (continue|read)|구독하고 계속|paywall|메타 회원 전용' \
  | sort -u | head -5
$ ./paywall-probe.sh https://some-news.example/a/1
== body length: 48213 bytes
"isAccessibleForFree":false
Subscribe to continue
paywall

실제 검색엔진 규모에서는 이걸 색인 시점에 미리 계산해서 플래그로 저장해둔다. 쿼리 시점에 페이지를 다시 긁을 수는 없으니까. 그래서 이 기능은 "필터"라기보다 "이미 있던 메타데이터를 노출하는 토글"에 가깝다고 보는 게 맞다. Kagi는 이전부터 검색 결과에 페이월 배지를 붙여왔는데, 그 판정 결과를 재활용해 아예 결과에서 빼는 스위치를 추가한 것으로 보인다.

비유하자면

Nginx의 deny 리스트를 쿼리마다 실시간 평가하느냐, 아니면 빌드 타임에 map 파일로 구워두느냐의 차이다. 후자가 검색엔진이 택할 수밖에 없는 방식이고, 대신 플래그가 낡을 수 있다는 부작용이 따라온다. 어제까지 무료였던 기사가 오늘 유료로 바뀌어도 다음 크롤까지는 그대로다. 반대도 마찬가지고.

실무 관점 — 도입 판단과 흔한 함정

켜기 전에 생각할 것

나는 켜놓고 쓰는 쪽인데, 무조건 추천하진 않는다. 정리하면 이렇다.

  • 켜면 좋은 경우: 기술 트러블슈팅, 문서 검색, 스택오버플로/깃허브/공식 docs 위주 탐색. 페이월 소스가 거의 없는 영역이라 부작용이 적다.
  • 끄는 게 나은 경우: 특정 사건의 1차 보도를 찾을 때. 국내외 주요 언론 단독 기사는 대부분 유료다. 필터를 켜면 원본이 사라지고 그걸 받아쓴 어그리게이터만 남는다. 이게 제일 무섭다. 신뢰도 낮은 재탕 소스로 결과가 채워지는 역효과.
  • 구독 중인 사이트가 있으면: 필터는 내 구독 상태를 모른다. WSJ 구독 중인데 WSJ이 통째로 사라지는 상황이 생긴다. 이럴 땐 전역 필터 대신 Kagi의 도메인별 랭킹 조정(Raise/Lower/Block)을 쓰는 게 낫다.

흔한 함정 1: 필터를 켜면 결과가 아예 안 나온다

페이월 필터 + 다른 필터(렌즈, 블록 도메인)를 겹치면 결과 0건이 뜬다. 특히 좁은 쿼리에서 자주 본다. 게다가 체인지로그를 보면 관련 버그가 실제로 있었다.

Blocked domains are used as sources in Quick Answer  #11257 @bausauce

차단 도메인이 일반 결과에선 빠지는데 Quick Answer의 소스로는 들어가던 이슈다. 필터링 계층이 여러 군데(랭킹, 결과 렌더링, AI 요약 파이프라인)에 흩어져 있으면 이런 누락이 반드시 생긴다. 우리가 만드는 시스템도 똑같다. WAF에서 막았는데 내부 서비스 메시 경유 트래픽은 안 막히는 것과 구조적으로 동일한 실수다.

흔한 함정 2: 크롤러 관점에서 페이월 판정 자체가 실패

직접 파이프라인을 만들어본 사람이면 이 에러를 만난다. Kagi도 체인지로그에 같은 문구를 버그로 올려놨다.

Upstream connect error or disconnect/reset before headers. reset reason: connection termination  #10680 @TheToby

Envoy 기반 프록시에서 나오는 전형적인 메시지다. 크롤러가 타임아웃/리셋을 맞으면 그 페이지는 "본문 없음"으로 처리되고, 본문 길이가 짧다는 이유로 페이월로 오판될 수 있다. 실제로 같은 체인지로그에 이런 것도 있다.

Extract API returns empty data for an entire batch when one page times out  #11176 @fredcy

배치 하나가 타임아웃 나면 배치 전체가 빈 데이터로 돌아오는 버그. 이게 페이월 판정 파이프라인에 물려 있으면 무고한 도메인 수백 개가 한꺼번에 페이월로 낙인찍힌다. 교훈은 명확하다. "데이터 없음"과 "차단됨"을 절대 같은 값으로 취급하지 말 것. 3-state로 가야 한다.

# 나쁜 설계
paywalled = (len(body_text) < 500)

# 최소한 이렇게
if fetch_status != 200:
    state = "UNKNOWN"      # 필터에서 제외하지 않음
elif has_paywall_signal(body):
    state = "PAYWALLED"
else:
    state = "FREE"

운영에서 이 원칙을 어겨서 사고 난 적이 있다. 헬스체크 엔드포인트가 타임아웃 났는데 그걸 "unhealthy"로 처리해서 정상 파드를 전부 내려버린 케이스. 도메인은 다르지만 실수의 모양은 똑같다.

함정 3: 필터가 켜졌는지 본인이 잊는다

제일 흔하고 제일 짜증난다. "왜 이 검색어에 결과가 없지?" 하고 30분 삽질했는데 알고 보니 몇 달 전에 켜둔 토글 때문. 이건 Kagi만의 문제가 아니라 모든 사용자 설정형 필터의 숙명이다. 대응책은 하나다. 계정 설정을 주기적으로 덤프해서 어딘가에 기록해두는 것.

# 필터 없는 결과를 확인할 때는 렌즈를 명시적으로 지정
# (2026-08-21 체인지로그: 렌즈 URL이 숫자 → 이름으로 바뀜)
https://kagi.com/search?q=kafka+consumer+lag&lens=forums

# 포럼 렌즈는 애초에 페이월 소스가 거의 없다 →
# 페이월 필터를 전역으로 켜는 대신 렌즈로 좁히는 게 부작용이 적다

개인적으로는 이 방식을 더 선호한다. 전역 토글은 "빼기"고, 렌즈는 "고르기"다. 빼기는 뭐가 빠졌는지 안 보이지만 고르기는 내가 뭘 골랐는지 URL에 남는다. 디버깅 가능성 측면에서 후자가 훨씬 낫다.

대안 정리

  • 브라우저 확장: uBlock 필터로 오버레이만 제거. 검색 결과는 그대로라 정보 손실이 없지만, 서버사이드 페이월(본문 자체를 안 내려주는 경우)엔 무력하다.
  • archive.today / Wayback: 링크는 보이되 우회. 저작권 이슈는 각자 판단.
  • Kagi 도메인 랭킹 조정: 완전 배제 대신 순위만 낮추기. 가장 균형 잡힌 선택이라고 본다.
  • SearXNG 자체 호스팅: 필터 로직을 직접 짜고 싶으면 이쪽. 대신 결과 품질과 레이트리밋 관리가 온전히 내 몫이 된다.

정리

한 줄 요약: 페이월 필터는 기술적 돌파구가 아니라 "사용자가 고객인 검색엔진"만 만들 수 있는 기능이다.

  • 기술 검색 위주인 사람 → 켜라. 부작용 거의 없고 체감 개선이 크다.
  • 뉴스·리서치를 검색으로 하는 사람 → 켜지 마라. 1차 소스가 통째로 사라지고 재탕 기사만 남는다.
  • 언론사 구독 중 → 전역 토글 대신 도메인별 랭킹 조정으로.
  • 비슷한 필터를 직접 만들 사람 → 판정 실패와 차단을 분리해라. UNKNOWN 상태 없이 만들면 크롤 실패가 그대로 오탐이 된다. 이게 이 글에서 가져갈 유일한 설계 교훈이다.

덧붙이면, Kagi 체인지로그는 사용자 피드백 번호(#11296 같은)를 항목마다 달아둔다. 요청 → 이슈 번호 → 릴리즈 노트가 한 줄로 이어지는 구조인데, 사내 플랫폼팀 운영해본 사람이면 이게 얼마나 유지하기 어려운지 안다. 기능보다 이 운영 방식이 더 인상적이었다.

참고 자료

728x90

+ Recent posts