Disqus에서 Giscus로
관련 파일: _config.yml, _includes/comments-providers/giscus.html, scripts/comments/fetch_recent_comments.py
이 블로그의 댓글 시스템은 오랫동안 Disqus였다 — 2022년에 댓글 기능 추가 (1) 두 편이 그 셋업을 적었고, 그 후로 손이 갈 일이 거의 없었다. 손이 갈 일이 거의 없었다는 것은 보통 잘 굴러간다는 뜻이지만, 동시에 애초에 쓰일 일이 거의 없다는 뜻이기도 하다. 한 달에 댓글이 한두 건 도착하는 블로그의 운영자는 그 한두 건을 어디에 살게 할지를 큰 결정으로 만들기가 곤란한 상황에 놓인다. 어쨌든 한두 건이라도 살 자리는 있어야 하니까.
옮기게 된 직접적인 동기는 결정적이지 않은 일들의 합이었다. 우선 Disqus 무료 plan은 댓글 영역 위/아래로 광고를 끼워 보여주는데, 그 광고들이 정확히 누구를 향한 것인지는 우주의 미스터리 중 하나다 — 수학 글 아래에 어디서 가져온 건지 모를 옷 광고가 떠 있는 풍경은 별로 단정하지 않다. 그 다음으로, Disqus 측 GDPR 트래커 동의 배너가 페이지를 처음 열 때 잠깐 시야를 가린다. 그것도 한 번이면 괜찮은데, 시크릿 창에서 페이지를 열 때마다 다시 마주치게 된다.
그리고 무엇보다 — 최근 댓글들을 블로그 사이드바에 띄우고 싶었는데, Disqus의 그쪽 API가 만만찮았다. 인증 흐름이 OAuth이고 발급된 API key의 rate가 제법 빡빡하고, 결과 JSON에서 댓글 본문과 글의 매핑을 다시 풀어야 한다. 그 노력의 끝에 띄우게 되는 것이 한 달에 한두 건의 댓글이라는 점이, 작업의 ROI를 묘하게 만든다.
댓글이 사는 자리
Giscus는 댓글을 GitHub Discussions의 한 thread로 저장한다. 즉 우리 블로그의 한 글에 달리는 댓글은 math-jh/math-jh.github.io 저장소의 Discussions에 그 글의 URL을 제목으로 갖는 thread로 들어간다. 이게 핵심적인 차이인데, 우리가 이미 GitHub의 데이터를 들여다볼 수 있는 자격을 갖고 있기 때문이다 — gh auth token이 그 자격증이다. 별도의 API key를 발급받고 OAuth를 거치고 rate limit을 협상할 필요가 없다.
설정은 _config.yml에 다음 6줄이 들어가는 것이다.
comments:
provider: "giscus"
giscus:
repo_id: "R_kgDOHqXFrA"
category_name: "Giscus Comments"
category_id: "DIC_kwDOHqXFrM4C9YLq"
discussion_term: "pathname"
pathname을 mapping으로 쓰면 /ko/math/.../groups라는 URL이 곧 그 글의 discussion 제목이 된다. 그래서 /ko/와 /en/은 자동으로 별개의 thread를 갖는다 — 한국어 글에 단 댓글이 영어판 페이지에 새지 않는다.
repo_id와 category_id는 GitHub Discussions에서 카테고리를 하나 만든 뒤 (Giscus Comments라는 이름을 쓴 것은 그저 자명성이 우선이라는 이유에서) giscus.app의 위저드가 알려준다. 위저드를 다시 열어볼 필요가 없도록 그 두 값은 _config.yml에 적어두는 셈이다.
_includes/comments-providers/giscus.html은 minimal-mistakes 테마가 이미 들고 있어서 손볼 일이 거의 없었다. 한 가지만 살짝 손봤다 — Giscus의 UI 언어가 페이지 언어를 따라가도록 URL prefix로부터 ko/en을 뽑아내 data-lang에 넘기는 로직.
한국어 페이지 아래에는 한국어 UI의 Giscus 댓글창이 떠야지, 영어 UI가 떠 있으면 약간 어색하다. 어색함이 댓글 작성을 방해하는지는 모르겠지만, 적어도 한 번은 누가 어색하게 느꼈을 것이다.
## 최근 댓글 사이드바
여기까지가 1차 동기 — Disqus 광고와 GDPR 배너를 떨궈내는 일 — 의 종착지이다. 2차 동기는 *최근 댓글들을 블로그 사이드바에 띄우기*였고, 이 부분이 Giscus로 옮긴 진짜 이유였다. [최근 글과 최근 댓글을 사이드바에 띄우는 시도](/ko/llm_workshop/marvin_recents_sidebar)는 이 마이그레이션을 전제로 한 작업이고, 그 작업이 깔끔할 수 있었던 것은 Giscus의 데이터가 사실은 우리 저장소의 데이터이기 때문이었다.
`scripts/comments/fetch_recent_comments.py`는 15분에 한 번 GitHub GraphQL API에 가서 최근에 업데이트된 discussion 30개를 가져오고, 각 discussion에서 가장 최근 댓글 한 건을 추려 `_data/recent_comments.yml`에 쓴다. 인증은 `gh auth token`을 빌려쓴다 — 사용자는 이미 GitHub CLI로 로그인되어 있으므로 그 토큰이 항상 손에 있다.
쿼리는 짧다.
```graphql
query($owner: String!, $name: String!, $first: Int!) {
repository(owner: $owner, name: $name) {
discussions(first: $first, orderBy: {field: UPDATED_AT, direction: DESC}) {
nodes {
title
url
updatedAt
comments(last: 1) {
nodes { bodyText url createdAt author { login } }
}
}
}
}
}
언어 분리는 discussion 제목 (즉 글의 URL) 앞 세 글자로 한다 — /ko/로 시작하면 한국어, /en/로 시작하면 영어. 사이드바의 Liquid 쪽도 같은 규칙으로 필터링하는데, 이건 단순히 안전망이다. 데이터 단계에서 한 번, 렌더링 단계에서 한 번 거르는 셈인데, 어느 한쪽이 빠져도 다른 쪽이 잡아주니까. 안전망이 두 개 걸려있는 자리는 보통 과거에 누가 한 번은 떨어진 적이 있는 자리이긴 하다.
쿼리가 실패하면 (rate limit, 토큰 부재, 네트워크 끊김) 기존 _data/recent_comments.yml을 그대로 두고 조용히 끝난다. 사이드바에 어제의 댓글이 잠깐 더 떠있는 편이, 빈 목록이 떠있는 것보다는 덜 슬프니까. 덜 슬프다는 것이 정량화가 어려운 평가 기준이라는 점은 인정한다.
옛 Disqus 댓글들의 행방
옮기는 김에 옛 댓글들도 옮겨야 하나 잠시 고민했다. Disqus는 댓글 export를 XML로 내준다. Giscus 쪽에 import하는 도구는 있는데, 매칭이 perfect하지 않아서 손이 더 가는 일이 된다. 결국 옮기지 않기로 했다 — Disqus 시절 댓글의 총합이 그 작업의 비용보다 작다고 판단했다. 그 판단이 옳았는지 잘못됐는지는 영영 검증되지 않을 가능성이 높다. Disqus 측에서 그 댓글들은 여전히 존재하는데, 우리 블로그에서는 더 이상 호출되지 않으니, 어디로 갔다고 말하기 애매한 상태가 됐다.
정리
새 댓글은 GitHub Discussions로 들어오고, 사용자는 자기 저장소의 노티에서 그것을 본다. 사이드바에는 매 15분마다 한 번 fresh한 최근 댓글 목록이 뜬다. 광고는 없고, GDPR 배너도 없고, OAuth로 발급받은 키를 한 자리에 깨끗하게 정리해둘 필요도 없다. 한 달에 한두 건의 댓글을 위한 인프라치고는 살짝 과한 셋업이라고도 할 수 있지만, 과한 셋업의 부수효과 중 하나는 다음 한두 건의 댓글이 도착했을 때 그것을 발견하는 것이 즐거워진다는 점이다 — 즐거움이라는 단어를 이 자리에서 쓰는 게 다소 과장스럽다는 것은 인정한다.
다음에 또 옮길 일이 있을지는 알 수 없다. 댓글 시스템을 옮기는 일은 블로그 운영의 통과의례 같은 면이 있어서, 충분히 오래 운영하면 결국 한 번은 더 만나게 될 것이다. 그 때가 오면 그 때의 도구가 그 때의 자리에서 더 단정한 모양일 것이라고 기대하기로 한다. 큰 기대는 아니지만, 작은 기대 정도라면 가능하다.
댓글남기기