RSS 주소를 고쳤는데, 구독자에게는 새 글이 될 수 있습니다

한글이 들어간 글 주소를 퍼센트 인코딩으로 바꾸는 일은 작은 정리처럼 보입니다. 브라우저에서 같은 글이 열리고 제목과 본문도 그대로라면 더 그렇습니다. 그런데 RSS의 <guid>까지 새 주소로 바꾸면 구독 도구는 이미 읽은 글을 처음 보는 항목으로 판단할 수 있습니다.

주소를 올바르게 만드는 작업과 이미 배포한 항목의 식별자를 보존하는 작업은 따로 확인해야 합니다. RSS를 생성하는 코드를 수정한다면 새 XML이 유효한지 보기 전에 기존 글의 GUID가 그대로인지부터 비교하는 편이 좋습니다.

같은 페이지로 열려도 같은 식별자는 아닙니다

RSS에는 글을 여는 <link>와 항목을 식별하는 <guid>가 있습니다. RSS 2.0 명세는 GUID를 항목을 유일하게 식별하는 문자열로 정의하며 수집기가 새 항목 여부를 판단하는 데 사용할 수 있다고 설명합니다. URL로 해석해서 같은 페이지가 나오는지를 먼저 비교하라는 규칙은 아닙니다. (RSS 2.0의 guid 정의)

다음은 실제 구독 서비스 대신 Python의 집합으로 식별자 비교만 보여 주는 예제입니다. 주소는 설명용입니다.

from urllib.parse import quote, unquote

old_guid = "https://example.com/posts/한글"
new_link = quote(old_guid, safe=":/")
seen = {old_guid}

print(unquote(new_link) == old_guid)
print(new_link in seen)
print(old_guid in seen)

실행 결과는 차례로 True, False, True입니다. 인코딩을 풀면 같은 주소지만 저장해 둔 문자열과 인코딩한 문자열은 다릅니다. 이 예제가 입증하는 범위는 문자열 식별의 차이까지입니다. 어떤 RSS 리더가 무엇을 다시 표시하거나 발송하는지는 해당 서비스의 규칙을 더 읽어야 합니다.

Buttondown은 이 부분을 명시하고 있습니다. guid 또는 Atom의 id가 있으면 이를 고유 식별자로 쓰고 없으면 link를 사용합니다. 같은 링크라도 GUID가 다르면 서로 다른 항목으로 취급합니다. 다만 실제 이메일 발송은 자동화 설정과 일정 등의 조건도 영향을 주므로 GUID 변경만 보고 “구독자 전원에게 중복 메일이 갔다”고 결론 낼 수는 없습니다. (Buttondown의 항목 식별 규칙)

기존 글에는 이미 배포한 값이 있습니다

개인 포트폴리오의 RSS 구현에는 IRI_GUID_SLUGS라는 작은 목록이 있습니다. 한글 경로를 그대로 담은 GUID로 먼저 발행한 다섯 글을 구분하는 목록입니다. 이 글들의 링크는 인코딩된 주소를 사용하지만 GUID는 기존 형식을 유지합니다. 나머지 글에는 인코딩된 형식을 사용합니다.

이 분기에서 보존하는 것은 예전 코드의 취향이 아닙니다. 구독 도구가 이미 항목을 기억할 때 사용했을 수 있는 값입니다. 모든 항목을 새 규칙으로 통일하면 코드는 간단해지지만 밖으로 내보낸 식별자도 함께 바뀝니다.

다른 구현에 적용한다면 다음처럼 역할을 분리할 수 있습니다. 프로젝트의 실제 코드를 복사한 것은 아니며 이전에 발행한 식별자를 별도로 보관한다는 설계 예시입니다.

const link = buildCurrentUrl(post);
const guid = publishedGuidById.get(post.id) ?? initialGuidFor(post);

여기서 publishedGuidById는 매 빌드마다 현재 URL로 다시 만드는 임시 맵이어서는 안 됩니다. 안정적인 내부 글 ID와 실제로 배포한 GUID의 대응을 남겨야 보존이라는 목적을 달성합니다. 기존 피드에 내부 ID가 없었다면 먼저 어떤 항목이 어느 글인지 대응시킨 뒤 그 결과를 검토해야 합니다. 제목은 수정될 수 있으니 제목만으로 자동 연결하는 것도 충분하지 않습니다.

새 피드라면 URL과 무관한 식별자를 설계할 수도 있습니다. 그 경우 RSS 명세의 isPermaLink="false" 의미를 확인해야 합니다. 이 속성을 생략하면 기본값은 true입니다. 이미 배포한 피드에 새 ID 체계를 소급 적용하는 일은 별도의 이관이며 단순한 URL 정리로 취급할 수 없습니다.

예약 글이 발행되면 비교 기준도 달라집니다

예약 발행이 있다면 브랜치를 검토하는 동안에도 피드가 바뀔 수 있습니다. 검토를 시작할 때는 없던 글이 배포 직전에 공개되면 그 글에는 이미 이전 코드가 만든 GUID가 생겼을 수 있습니다.

이 프로젝트의 원자료에도 예약 글이 먼저 공개된 뒤 GUID 형식이 달라진 기록이 있습니다. 이후 값을 과거 형식으로 되돌리면 변경을 없애는 대신 또 한 번 식별자를 바꿀 수 있습니다. 어느 구독자에게 전달됐는지는 확인되지 않아 여기서는 발송 사고의 사례로 다루지 않습니다.

그래서 마이그레이션 검사는 한 번 저장한 피드로 끝내기 어렵습니다. 실제 공개 시점을 포함해 다음 순서로 확인할 수 있습니다.

  1. 현재 공개 피드를 저장하고 발행된 각 글의 GUID와 링크를 기록합니다.
  2. 새 코드가 만든 피드에서 기존 항목의 GUID가 그대로인지 비교합니다.
  3. 예약 발행이 있었거나 배포가 늦어졌다면 공개 피드를 다시 읽고 새로 등장한 항목을 대조합니다.
  4. 배포 뒤 피드를 다시 확인합니다. 이메일 영향은 피드 비교와 분리해 발송 기록으로 판단합니다.

새 글은 목록에 추가될 수 있습니다. 검사의 핵심은 이전 항목이 빠지거나 다른 GUID로 바뀐 경우를 잡아내는 것입니다. 문제가 있는 샘플의 GUID를 일부러 바꿨을 때도 통과한다면 그 검사는 식별자 보존을 확인하지 못하고 있습니다.

URL 정리는 앞으로 글을 잘 찾게 만드는 일입니다. GUID 보존은 이미 그 글을 받아 본 시스템과의 약속을 유지하는 일입니다. RSS 생성 코드를 고칠 때 두 값을 따로 비교하면 주소가 같아 보인다는 이유로 지나치기 쉬운 변경을 발견할 수 있습니다.