dev.to의 대시보드가 자신의 게시물 수를 제대로 세지 못하는 문제
요약
오픈 소스 플랫폼 Forem(dev.to의 기반)에서 대시보드의 게시물 카운트가 실제 표시되는 게시물 수와 일치하지 않는 버그를 분석하고 수정하는 과정을 다룹니다. 캐시된 카운터와 실제 필터링된 뷰 사이의 불일치 원인을 파악하여 해결합니다.
핵심 포인트
- 캐시된 카운터(counter_culture)와 실제 렌더링되는 데이터 간의 불일치 문제 분석
- 게시물 유형(full_post, status 등)에 따른 필터링 누락 확인
- 데이터 무결성을 위해 캐시와 뷰의 로직을 일치시키는 중요성 강조
이 글은 Sentry가 지원하는 DEV's Summer Bug Smash: Clear the Lineup에 제출된 글입니다.
프로젝트 개요 (Project Overview)
forem은 dev.to 자체를 뒷받침하는 오픈 소스 플랫폼입니다. 저는 몇 달 동안 이 프로젝트를 Star(즐겨찾기)하고 Clone(복제)해 두었지만, 코드베이스를 한 번도 열어본 적이 없었습니다. Rails 기반인데, 저는 Ruby를 작성하지 않기 때문입니다. Jess의 포스트가 마침내 그 상황을 바꾸게 된 계기가 되었습니다.
github.com/forem/forem
버그 수정 또는 성능 개선 (Bug Fix or Performance Improvement)
#23687은 단 한 줄로 된 보고서였습니다: 한 사용자가 정확히 하나의 게시물을 발행했는데, 대시보드의 "Posts" 카운터에는 2라고 표시되었습니다.
Ruby를 작성하지 않는다는 것은 느낌만으로 수정 방법을 추측할 수 없음을 의미했습니다. 저는 실제로 추적해야 했습니다. 버그의 형태가 추측이 아닌 부정할 수 없는 사실이 될 때까지 DashboardsController, 사이드바 partials(부분 템플릿), 그리고 Article 모델을 읽어야 했습니다.
대시보드 사이드바의 "Posts" 배지는 @user.articles_count를 렌더링합니다. 이는 해당 사용자에게 속한 모든 Article 행에 대해 증가하는 User 모델의 counter_culture 캐시일 뿐입니다. 유형에 대한 필터도, 상태에 대한 필터도 없습니다:
# app/models/article.rb
counter_culture :user
하지만 그 배지가 위치한 링크는 항상 매개변수(params)가 없는 동일한 기본 뷰인 DashboardsController#show를 엽니다. 해당 뷰는 **아카이브되지 않은, full-post-type(전체 게시물 유형)**의 기사만 목록에 표시합니다:
# app/controllers/dashboards_controller.rb
@articles = target.articles.from_subforem.includes(:organization)
@articles = params[:state] == "status" ? @articles.statuses : @articles.full_posts
...
Forem에는 full_post, status(짧은 "Boost" 업데이트), fullscreen_embed라는 세 가지 기사 유형이 있습니다. 하지만 카운터는 이 유형들을 구분하지 않으며, 아카이브된 것과 활성화된 것도 구분하지 않습니다. 배지는 모든 것을 카운트합니다. 반면 그 아래의 목록은 엄격한 부분 집합(subset)만을 보여줍니다. 상태 업데이트(status update)를 게시했거나 게시물을 아카이브한 적이 있는 사람이라면 누구나, 실제로 클릭해서 볼 수 있는 내용과 일치하지 않는 숫자를 보게 됩니다. 이것이 바로 #23687에서 보고된 내용입니다.
Ruby에서는 이를 검증할 수 없었지만, 구조가 펼쳐지자마자 즉시 그 형태를 알아챌 수 있었습니다. 필터링된 뷰(filtered view)가 실제로 렌더링하는 내용과 캐시된 카운트(cached count)가 어긋나고 있는 모습이었죠. 저 또한 JavaScript에서 정확히 똑같은 버그를 배포한 적이 있습니다. 문법만 다를 뿐 실패 방식은 동일했습니다.
코드 (Code)
github.com/forem/forem/pull/23690
이번 수정 사항은 공유된 articles_count 카운터를 건드리지 않습니다. 해당 캐시는 배지(badge)나 스팸 휴리스틱(spam heuristics)을 위해 다른 곳에서 읽히는데, 그곳에서는 "이 사용자가 작성한 모든 게시물"이라는 의미가 정확하기 때문입니다. 대신, DashboardsController에 Posts 탭이 실제로 렌더링하는 내용과 일치하도록 범위가 지정된(scoped) 헬퍼(helper)를 할당하였으며, 전체 페이지와 AJAX 사이드바 액션 모두 가공되지 않은 캐시 대신 이 헬퍼를 사용하도록 했습니다.
# "Posts" 네비게이션 항목은 항상 사용자의 대시보드 기본 뷰
# (아카이브되지 않은, 전체 게시물만 포함된 뷰)로 연결되므로,
# 해당 인디케이터(indicator)는 사용자의 가공되지 않은
# articles_count가 아니라 동일한 범위를 반영해야 합니다.
...
나의 개선 사항 (My Improvements)
눈으로 직접 확인하고 신뢰할 수 있는 방법이 없었습니다. 저는 Ruby를 그 정도로 잘 읽지 못하며, 작업 중이던 컴퓨터에는 Ruby나 Postgres가 설치되어 있지 않아 로컬에서 스펙 테스트(spec suite)를 실행할 수도 없었습니다. 검증은 다른 곳에서 이루어져야 했습니다. 저는 하나의 전체 게시물, 하나의 상태(status) 게시물, 그리고 하나의 아카이브된 게시물을 가진 사용자가 정확히 1이라는 카운트를 보아야 한다는 것을 단언하는 회귀 테스트(regression specs)를 작성했습니다. 그 후 브랜치를 푸시하고, 제 확신 대신 Forem의 자체 CI(지속적 통합)가 판단하도록 맡겼습니다.
CI는 첫 번째 실행에서 실제 문제를 잡아냈습니다. 수정 사항의 문제가 아니라, 제 테스트의 문제였습니다. create(:article, type_of: "status")가 자체 모델 유효성 검사(model validation)에서 실패했는데, Forem에서 상태(status) 타입의 게시물은 본문 마크다운(body markdown)을 가질 수 없지만 팩토리(factory)의 기본값은 이를 포함하고 있었기 때문입니다. 저는 이미 테스트 스위트의 다른 곳에서 사용 중인 패턴(body_markdown: "", main_image: nil)을 찾아내어 두 개의 스펙을 수정하고 다시 푸시했습니다.
이 실패야말로 이것이 수정 사항으로 포장된 추측이 아니라는 실제 증거입니다. 만약 로컬에서 스펙을 실행할 수 있었다면 푸시하기 전에 잡아낼 수 있었겠지만, 대신 프로젝트 자체의 CI가 로컬 실행이 했을 법한 역할을 수행해 주었습니다.
제 다른 두 게시물에서도 계속해서 마주쳤던 것과 동일한 교훈입니다: 완벽하게 작동했지만 두 번이나 실패한 Cloudflare Worker와 모니터링 도구의 데모를 촬영하고 있었다. 그런데 모니터가 모니터링을 하고 있지 않았다. — "컴파일되었다"와 "정확하다"는 서로 다른 주장이며, 그중 하나만이 신뢰할 가치가 있습니다.
이제 모든 것이 초록색(성공)입니다. dashboard_spec.rb를 실행하는 샤드(shard)를 포함하여 19번의 성공적인 체크, 1번의 건너뜀(skipped), 0번의 실패가 기록되었습니다. PR(Pull Request)은 forem/forem을 대상으로 열려 있으며, 서드파티 포크(third-party fork) PR은 병합(merge) 전에 유지 관리자의 검토가 필요하기 때문에 검토를 기다리고 있습니다. 이 글을 쓰는 시점 기준으로 아직 병합되지는 않았습니다 — 다른 의도를 암시하기보다는 차라리 솔직하게 말하는 편이 낫겠습니다.
제 다른 두 게시물과 한 가지 다른 점이 있다면, 저는 Ruby를 작성하지 않는다는 것입니다. Claude가 버그를 찾아내고 수정 코드를 작성했습니다. 저는 문제를 선정하고 제 컴퓨터를 떠나는 모든 것—포크(fork), 푸시(push), PR, CLA—을 통제했습니다. 코드에 대해서는 전적으로 위임했지만, 그것이 배포(ship)될지 여부에 대해서는 위임하지 않았습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기