RSS: 새 글을 구독자에게 전해 주는 웹 피드

“RSS는 이제 아무도 안 쓰지 않나요?”라는 이야기를 종종 듣는데요. 2013년 Google Reader가 문을 닫으면서 RSS가 죽었다는 말이 한동안 유행했지만, 사실 RSS는 우리가 모르는 사이에 곳곳에서 일하고 있습니다. 팟캐스트 앱이 새 에피소드를 가져오는 것도, 뉴스레터 서비스가 블로그 새 글을 메일로 보내 주는 것도, 검색엔진이 최근 글을 빨리 찾아가는 것도 대부분 RSS 덕분입니다. 🤔
이 글에서는 웹 피드(web feed)가 무엇인지, RSS 2.0과 Atom, JSON Feed는 어떻게 다른지 살펴보겠습니다. 그리고 Bun으로 RSS 피드를 직접 만들어 보고, 피드 리더(feed reader)가 피드를 효율적으로 가져가도록 응답하는 방법과 자주 하는 실수까지 정리해 보겠습니다.
RSS란?
RSS는 웹사이트의 새 콘텐츠 목록을 정해진 XML 형식으로 내보내는 방법입니다. 이렇게 내보낸 파일을 피드라고 부르고, 피드를 주기적으로 읽어 새 글을 모아 보여 주는 프로그램을 피드 리더라고 합니다.
피드가 없던 시절에는 좋아하는 블로그에 새 글이 올라왔는지 확인하려면 사이트를 하나하나 직접 들어가 봐야 했습니다. 피드가 있으면 이 일을 피드 리더가 대신합니다. 구독한 사이트들의 피드를 돌아다니며 새 항목이 생겼는지 확인하고, 한곳에 모아 보여 주는 거죠. 사이트 입장에서는 방문자가 매번 찾아오지 않아도 새 글을 알릴 수 있는 통로가 생기는 셈입니다.
RSS라는 이름은 버전에 따라 뜻이 달라서 조금 헷갈립니다. 1999년 Netscape가 처음 내놓았을 때는 RDF Site Summary였고, 지금 가장 널리 쓰이는 RSS 2.0에서는 Really Simple Syndication의 약자입니다. RSS 2.0 명세는 지금 RSS Advisory Board가 관리하고 있으며, 마지막 개정은 2009년의 2.0.11입니다. 그 뒤로 사실상 바뀌지 않았으니 한 번 익혀 두면 오래 써먹을 수 있습니다.
RSS 2.0의 기본 형식
가장 단순한 RSS 2.0 피드는 이렇게 생겼습니다.
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>예제 블로그</title>
<link>https://example.com/</link>
<description>Bun으로 만든 예제 블로그입니다.</description>
<item>
<title>Bun.serve로 HTTP 서버 만들기</title>
<link>https://example.com/bun-serve/</link>
<guid>https://example.com/bun-serve/</guid>
<pubDate>Mon, 12 Oct 2026 00:00:00 GMT</pubDate>
</item>
</channel>
</rss>
루트인 <rss> 아래에 사이트 전체를 나타내는 <channel>이 하나 있고, 그 안에 글마다 <item>이 하나씩 들어가는 구조입니다.
<channel>에는 <title>, <link>, <description> 세 요소가 반드시 있어야 합니다. 사이트 이름과 주소, 한 줄 소개입니다. 그 밖에 <language>, <lastBuildDate>, 피드를 몇 분 동안 캐시해도 되는지 알려 주는 <ttl> 같은 선택 요소가 있습니다.
<item>은 조금 특이한데요. 명세상 모든 하위 요소가 선택이고, <title>과 <description> 중 하나만 있으면 됩니다. 하지만 실제로는 제목(<title>), 글 주소(<link>), 고유 식별자(<guid>), 발행일(<pubDate>), 요약이나 본문(<description>)을 모두 넣는 게 보통입니다. 팟캐스트처럼 파일을 첨부할 때는 url, length, type 세 속성을 가진 <enclosure>를 씁니다.
날짜와 guid가 중요한 이유
<item>의 요소 중 실수가 가장 잦은 곳이 <pubDate>와 <guid>입니다. 둘 다 피드 리더가 새 글을 판단하는 데 직접 쓰기 때문입니다.
<pubDate>는 RFC 822 형식으로 써야 합니다. Mon, 12 Oct 2026 00:00:00 GMT처럼 요일, 일, 월, 연도, 시각, 시간대 순서입니다. 우리에게 익숙한 2026-10-12 같은 ISO 8601 형식을 넣으면 일부 피드 리더는 날짜를 읽지 못해 글을 엉뚱한 순서로 보여 주거나 발행일을 수집한 시각으로 바꿔 버립니다. JavaScript라면 Date의 toUTCString()이 바로 이 형식을 만들어 주니 직접 문자열을 조립할 필요가 없습니다.
<guid>는 글마다 절대 바뀌지 않는 고유한 값이어야 합니다. 피드 리더는 이 값으로 이미 본 글인지를 가립니다. 그래서 글 제목의 오타를 고쳤다고 <guid>가 바뀌면 같은 글이 새 글로 한 번 더 뜹니다. 사이트 주소를 옮기거나 URL 규칙을 바꿨을 때 구독자의 피드 리더에 옛 글이 수십 개씩 한꺼번에 쏟아지는 사고가 대부분 이 때문입니다.
<guid>에는 isPermaLink 속성이 있고 기본값은 true입니다. 값이 그 글의 영구 주소라는 뜻이죠. 글 주소를 <guid>로 쓴다면 그대로 두면 되고, 데이터베이스 ID 같은 URL이 아닌 값을 쓴다면 isPermaLink="false"를 붙여야 합니다. 어느 쪽이든 한 번 정한 값은 바꾸지 않는 게 원칙입니다.
RSS와 Atom, JSON Feed 비교
RSS 2.0 말고도 널리 쓰이는 피드 형식이 두 가지 더 있습니다.
Atom은 RSS의 모호한 명세를 정리하려고 만든 형식으로, 2005년 RFC 4287로 표준이 되었습니다. RSS보다 규칙이 엄격합니다. 피드와 각 항목(<entry>)에는 <id>, <title>, <updated>가 반드시 있어야 하고, 작성자(<author>)도 피드나 항목 중 한 곳에는 꼭 적어야 합니다. 날짜는 2026-10-12T00:00:00Z 같은 RFC 3339 형식을 쓰고, 본문이 HTML인지 일반 텍스트인지 type 속성으로 명시합니다.
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<id>https://example.com/</id>
<title>예제 블로그</title>
<updated>2026-10-12T00:00:00Z</updated>
<author><name>Dale</name></author>
<link rel="self" href="https://example.com/atom.xml" />
<entry>
<id>https://example.com/bun-serve/</id>
<title>Bun.serve로 HTTP 서버 만들기</title>
<updated>2026-10-12T00:00:00Z</updated>
<link href="https://example.com/bun-serve/" />
<summary type="text">Bun 내장 서버로 라우팅과 정적 파일 서빙을 해 봅니다.</summary>
</entry>
</feed>
JSON Feed는 XML 대신 JSON으로 피드를 표현하는 형식입니다. 2017년에 처음 나왔고 2020년에 1.1 버전이 나왔습니다. XML을 다루기 번거로운 환경에서 만들고 읽기 편하다는 게 장점입니다.
세 형식을 비교하면 다음과 같습니다.
| RSS 2.0 | Atom 1.0 | JSON Feed 1.1 | |
|---|---|---|---|
| 데이터 형식 | XML | XML | JSON |
| 표준화 | RSS Advisory Board | IETF RFC 4287 | 커뮤니티 명세 |
| 날짜 형식 | RFC 822 | RFC 3339 | RFC 3339 |
| 미디어 타입 | application/rss+xml | application/atom+xml | application/feed+json |
| 지원 범위 | 거의 모든 도구 | 거의 모든 도구 | 일부 피드 리더 |
어느 형식을 고를지 고민된다면 RSS 2.0을 추천합니다. 지원하지 않는 도구가 사실상 없고, 팟캐스트 생태계도 RSS 2.0을 기준으로 돌아갑니다. Atom은 명세가 깔끔해서 처음부터 직접 만든다면 좋은 선택이지만, 요즘 피드 리더는 둘 다 잘 읽으니 큰 차이는 없습니다. 둘 다 내보내는 사이트도 많습니다.
Bun으로 RSS 피드 만들기
이제 직접 만들어 보겠습니다. 글 목록을 받아 RSS 2.0 피드를 만들고 Bun.serve로 /rss.xml에서 서빙해 보겠습니다.
const SITE = "https://example.com";
const posts = [
{
slug: "bun-serve",
title: "Bun.serve로 HTTP 서버 만들기",
summary: "Bun 내장 서버로 라우팅과 정적 파일 서빙을 해 봅니다.",
publishedAt: new Date("2026-10-12T09:00:00+09:00"),
},
{
slug: "xml-escape",
title: "XML에서 <, >, & 이스케이프하기",
summary: "특수 문자가 들어간 제목도 피드에서 깨지지 않게 합니다.",
publishedAt: new Date("2026-09-28T09:00:00+09:00"),
},
];
function escapeXml(value: string) {
return value
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function buildRss() {
const items = posts
.map((post) => {
const url = `${SITE}/${post.slug}/`;
return ` <item>
<title>${escapeXml(post.title)}</title>
<link>${url}</link>
<guid isPermaLink="true">${url}</guid>
<description>${escapeXml(post.summary)}</description>
<pubDate>${post.publishedAt.toUTCString()}</pubDate>
</item>`;
})
.join("\n");
return `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>예제 블로그</title>
<link>${SITE}/</link>
<description>Bun으로 만든 예제 블로그입니다.</description>
<language>ko</language>
<atom:link href="${SITE}/rss.xml" rel="self" type="application/rss+xml" />
${items}
</channel>
</rss>
`;
}
const server = Bun.serve({
port: 3000,
routes: {
"/rss.xml": () =>
new Response(buildRss(), {
headers: { "Content-Type": "application/rss+xml; charset=utf-8" },
}),
},
});
console.log(`Listening on ${server.url}`);
몇 가지 짚고 넘어가겠습니다. 우선 글 제목과 요약은 escapeXml()로 이스케이프합니다. 제목에 <나 &가 들어가는 순간 XML이 깨지고, 피드 리더는 피드 전체를 읽지 못합니다. 또한 <pubDate>는 toUTCString()으로 RFC 822 형식을 만듭니다. 마지막으로 <channel> 안에 atom:link로 피드 자신의 주소를 적었습니다. RSS 2.0 명세에는 없는 요소지만, 피드 검증기가 권장하는 관례라 Atom 네임스페이스를 빌려 넣는 경우가 많습니다.
서버를 띄우고 요청을 보내 보겠습니다.
bun run server.ts
curl -i http://localhost:3000/rss.xml
HTTP/1.1 200 OK
Content-Type: application/rss+xml; charset=utf-8
Date: Sat, 17 Oct 2026 00:25:39 GMT
Content-Length: 1140
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>예제 블로그</title>
<link>https://example.com/</link>
<description>Bun으로 만든 예제 블로그입니다.</description>
<language>ko</language>
<atom:link href="https://example.com/rss.xml" rel="self" type="application/rss+xml" />
<item>
<title>Bun.serve로 HTTP 서버 만들기</title>
<link>https://example.com/bun-serve/</link>
<guid isPermaLink="true">https://example.com/bun-serve/</guid>
<description>Bun 내장 서버로 라우팅과 정적 파일 서빙을 해 봅니다.</description>
<pubDate>Mon, 12 Oct 2026 00:00:00 GMT</pubDate>
</item>
<item>
<title>XML에서 <, >, & 이스케이프하기</title>
<link>https://example.com/xml-escape/</link>
<guid isPermaLink="true">https://example.com/xml-escape/</guid>
<description>특수 문자가 들어간 제목도 피드에서 깨지지 않게 합니다.</description>
<pubDate>Mon, 28 Sep 2026 00:00:00 GMT</pubDate>
</item>
</channel>
</rss>
한국 시간 오전 9시로 넣은 발행일이 UTC 기준 자정으로 바뀌어 RFC 822 형식으로 나왔고, 두 번째 글 제목의 특수 문자도 모두 이스케이프되었습니다.
Content-Type은 application/rss+xml로 보냈는데요. 이 미디어 타입은 사실 IANA에 정식 등록된 적이 없지만 관례로 굳어져 거의 모든 도구가 알아듣습니다. 다만 일부 브라우저는 이 타입을 받으면 화면에 보여 주지 않고 파일로 내려받기 때문에, 브라우저에서 바로 열리길 원해 application/xml로 보내는 사이트도 있습니다. 어느 쪽이든 피드 리더는 문제없이 읽습니다.
304로 응답해 트래픽 줄이기
피드 리더는 구독한 피드를 몇 분에서 몇 시간 간격으로 계속 다시 가져갑니다. 구독자가 많으면 바뀐 것도 없는 같은 XML을 하루에 수천 번씩 보내게 되죠. 이럴 때 HTTP의 조건부 요청(conditional request)을 지원하면 트래픽을 크게 줄일 수 있습니다.
방법은 간단합니다. 응답에 Last-Modified 헤더로 피드가 마지막으로 바뀐 시각을 알려 주면, 잘 만든 피드 리더는 다음 요청부터 그 값을 If-Modified-Since 헤더에 담아 보냅니다. 서버는 그동안 바뀐 게 없으면 본문 없이 304 상태 코드만 돌려주면 됩니다. 앞의 라우트를 이렇게 바꿔 보겠습니다.
"/rss.xml": (req) => {
const lastModified = new Date(
Math.max(...posts.map((post) => post.publishedAt.getTime())),
).toUTCString();
if (req.headers.get("If-Modified-Since") === lastModified) {
return new Response(null, { status: 304 });
}
return new Response(buildRss(), {
headers: {
"Content-Type": "application/rss+xml; charset=utf-8",
"Last-Modified": lastModified,
},
});
},
가장 최근 글의 발행일을 피드의 마지막 수정 시각으로 쓰고 있습니다. 첫 요청과 If-Modified-Since를 담은 두 번째 요청을 차례로 보내 보겠습니다.
curl -I http://localhost:3000/rss.xml
curl -i -H "If-Modified-Since: Mon, 12 Oct 2026 00:00:00 GMT" http://localhost:3000/rss.xml
HTTP/1.1 200 OK
Content-Type: application/rss+xml; charset=utf-8
Last-Modified: Mon, 12 Oct 2026 00:00:00 GMT
content-length: 1140
HTTP/1.1 304 Not Modified
Date: Sat, 17 Oct 2026 00:25:48 GMT
두 번째 요청에는 본문 없이 304만 돌아갔습니다. 실제 서비스에서는 글을 수정했을 때도 시각이 바뀌도록 수정일까지 함께 따지거나, 피드 내용의 해시로 ETag를 만들어 If-None-Match와 비교하는 방법을 많이 씁니다. 정적 파일로 피드를 배포한다면 호스팅 플랫폼이 이 헤더들을 알아서 처리해 주는 경우가 대부분이고, 캐시 기간은 Cache-Control 헤더로 함께 조절할 수 있습니다.
피드 자동 발견
피드를 만들었다면 사람과 프로그램이 그 위치를 찾을 수 있게 해야 합니다. 페이지의 head 요소에 다음과 같은 <link> 태그를 넣어 두면 됩니다.
<link
rel="alternate"
type="application/rss+xml"
title="예제 블로그"
href="https://example.com/rss.xml"
/>
이를 피드 자동 발견(feed autodiscovery)이라고 하는데요. 피드 리더에 피드 주소가 아니라 블로그 주소만 넣어도 구독이 되는 건 이 태그 덕분입니다. 피드 리더가 페이지를 받아서 이 태그를 찾아 피드 주소를 알아내는 거죠. Atom 피드라면 type을 application/atom+xml로 바꾸고, 여러 형식을 제공한다면 태그를 여러 개 두면 됩니다.
자동 발견 태그가 없는 사이트를 위해 피드 리더들은 /feed, /rss.xml, /atom.xml, /feed.xml 같은 흔한 경로를 차례로 찔러 보기도 합니다. 그래서 피드를 한 경로에 두고 다른 흔한 경로에서도 같은 내용을 내보내는 사이트도 꽤 있습니다.
사이트맵과 함께 쓰기
RSS는 검색엔진에도 쓸모가 있습니다. Google은 RSS 2.0과 Atom 1.0 피드를 사이트맵처럼 제출받습니다. Search Console의 사이트맵 메뉴에 피드 주소를 넣으면 됩니다.
다만 피드는 사이트맵을 대신하지 못합니다. 피드에는 보통 최근 글 10~20개만 들어가기 때문에, 피드만 제출하면 그보다 오래된 글은 이 경로로 발견되지 않습니다. 반대로 사이트맵은 모든 페이지를 담는 대신 수만 개의 URL 사이에서 새 글이 묻히기 쉽습니다. Google도 2014년 검색 센터 블로그에서 둘을 함께 쓰라고 권했습니다. 전체 목록은 XML 사이트맵으로, 최근 변경은 피드로 알리는 식입니다.
새 글을 더 빨리 알리고 싶다면 WebSub도 있습니다. 예전에는 PubSubHubbub이라고 불렀고 2018년에 W3C 권고안이 되었습니다. 피드 리더가 주기적으로 찾아오길 기다리지 않고, 피드가 바뀌면 허브(hub)에 알려 구독자에게 바로 밀어 주는 방식입니다. 피드에 허브 주소를 <atom:link rel="hub">로 적어 두고 새 글을 올릴 때 허브에 핑을 보내면 됩니다.
자주 하는 실수
마지막으로 피드를 운영하며 흔히 겪는 실수를 정리해 보겠습니다.
우선 본문 속 상대 경로입니다. <description>에 글 본문 HTML을 통째로 넣을 때 <img src="/images/a.png">처럼 상대 경로가 섞여 있으면, 피드 리더는 이 경로를 어디 기준으로 풀어야 할지 몰라 이미지가 깨집니다. 피드에 들어가는 링크와 이미지 주소는 모두 절대 URL로 바꿔서 넣습니다. 본문을 넣을 때는 HTML을 이스케이프하거나 <![CDATA[ ... ]]>로 감싸는 것도 잊지 말아야 합니다.
또한 미래 날짜입니다. 예약 발행용으로 미래 날짜를 잡아 둔 글이 피드에 그대로 나가면, 피드 리더에 따라 그 글이 목록 맨 위에 계속 고정되거나 아예 무시됩니다. 피드를 만들 때 아직 발행 시각이 오지 않은 글은 빼야 합니다.
그리고 앞에서 말한 <guid> 변경입니다. 사이트 주소를 옮기면서 <guid>까지 새 주소로 바뀌면 구독자의 피드 리더에 옛 글이 새 글처럼 한꺼번에 쏟아집니다. 주소를 옮기더라도 <guid>는 옛 값을 그대로 유지하고, 옛 피드 주소에서 새 주소로 301 리디렉션을 걸어 두면 구독자를 잃지 않고 옮길 수 있습니다.
마지막으로 피드를 고친 뒤에는 W3C Feed Validation Service로 검사해 보는 걸 추천합니다. 날짜 형식이나 이스케이프 실수처럼 눈으로는 놓치기 쉬운 문제를 잡아 줍니다.
마치며
RSS는 1999년에 나온 오래된 기술이지만, 지금도 팟캐스트와 뉴스레터, 검색엔진, 피드 리더 사이에서 콘텐츠를 실어 나르는 기본 통로입니다. 형식은 단순해도 <guid>를 바꾸지 않고, <pubDate>를 RFC 822 형식으로 쓰고, 특수 문자를 이스케이프하는 기본기만 지키면 구독자가 불편 없이 새 글을 받아 볼 수 있습니다.
피드를 이미 운영하고 있다면 지금 피드 주소를 검증기에 한번 넣어 보세요. 생각보다 많은 경고가 숨어 있을지도 모릅니다. 😉
더 자세한 내용은 RSS 2.0 명세를 참고하세요.
This work is licensed under CC BY 4.0