Web

블로그에 RSS 기능 추가하기

date
Jul 19, 2024
thumbnail
rss-feed-thumbnail.png
slug
blog-rss-feature-update
author
status
Public
tags
SEO
Next.js
Vercel
summary
동적, 정적 방식으로 블로그 콘텐츠에 대한 RSS 피드를 제공해보자
type
Post
category
Web

개요


블로그 글쓰기 스터디에 참여하게 되었는데, 디스코드 채널에 새 포스팅을 알려주는 디스코드 봇을 넣으면 어떻겠냐는 의견이 있었다. 알아보니 RSS를 통해 콘텐츠를 구독하면 모니터링 기능이 있는 봇을 통해 채널에 알람이 가는 방식이다. RSS 라이브러리들도 잘 되어 있겠다 얼른 추가해볼 생각에 덥석 시작했는데, 아니나 다를까 몇 가지 문제점들을 맞닥뜨려서 이를 공부한 과정을 공유하고자 한다. 스터디 할 일도 해결되고 🐶🍯이다.
 
 

RSS (Rich Site Summary, Really Simple Syndication)

notion image
  • 뉴스나 블로그 사이트에서 주로 사용하는 콘텐츠 표현 방식
  • 사이트의 콘텐츠를 구독하여 구독자들에게 업데이트한 콘텐츠를 제공할 수 있음
  • 해당 사이트를 방문하지 않고도 새로 업데이트한 콘텐츠를 받아볼 수 있음
  • 파일 확장자는 주로 .rss, .xml 이며 콘텐츠 타입은 application/rss+xml
  • 이보다 고도화된 포맷인 Atom 또는 XML 대신 JSON을 사용하는 JSON Feed 등의 Contents Syndication 등이 존재
 
 

콘텐츠 신디케이터 라이브러리

  • 전통의 웹 콘텐츠 신디케이터 RSS를 생성해주는 라이브러리
 
  • RSS 2.0, JSON Feed 1.0, Atom 1.0 피드를 생성해주는 라이브러리
  • XML을 사용하는 RSS 대신 JSON Feed를 사용하면 파싱이 쉽고 가벼움
 
 

RSS 라이브러리 사용하기


전통의 RSS 부터 추가해보자는 마음에 rss 라이브러리를 사용해보자. 사용이 직관적이어서 사실 예제 코드만 보면 사용법을 대부분 익힐 수 있다.
 
RSS 만들고 내보내는 과정
RSS 만들고 내보내는 과정
  1. 피드(feed)를 생성
  1. 피드의 내용을 설정
  1. 피드에 아이템(item) 넣기
  1. 피드를 파일로 만들어 내보내기
 

피드 생성 및 설정

const myFeed = new RSS(feedOptions);
const myFeed = new RSS({ title: //... description: //... feed_url: //... site_url: //... image_url: //... language: //... // 등등 여러 옵션들이 존재 });
  • 새 피드를 생성한다.
  • 피드의 내용(feedOptions)들을 설정한다.
    • 커스텀 요소들을 넣을 수도 있다.
 

피드에 아이템 넣기

feed.item(itemOptions);
feed.item({ title: //... description: //... url: //... author: //... date: //... // 등등 여러 옵션들이 존재 });
  • 피드에 아이템을 채워넣는다.
  • 아이템 내용(itemOptions)들을 설정한다.
    • 커스텀 요소들을 넣을 수도 있다.
 

피드 포장

const xml = feed.xml({ indent: true });
 

내보내기

완성된 xml을 어떻게 제공해줘야 괜찮을까? 크게 보면 정적으로 제공하거나 동적으로 제공하거나 두 가지 방식으로 나눌 수 있겠다.
 
정적으로 제공하기
정적으로 제공하기
✅ 서버 리소스를 사용하지 않으며, 캐싱 및 빠른 응답이 가능함
💦 최신 피드를 제공하려면 빌드가 필요함
 
 
동적으로 제공하기
동적으로 제공하기
✅ 항상 최신 피드를 제공할 수 있음
💦 매 요청마다 서버 리소스 사용하는 등 고려할 점 많음
 
 
해당 블로그(dongho-log)는Next.js 12.0.1 버전으로 개발된 블로그이고, Notion API를 통해 Notion에 글을 작성하고 글의 status 속성을 public으로 설정하면 자동으로 글을 업데이트 해주는 방식으로 되어 있다. 동적으로 제공하는 것이 더 편하겠다 싶어 /api/rss.xml에 접근하면 동적으로 피드를 생성하여 제공하는 방식으로 선택했다.
 
 

동적으로 API를 통해 제공하기


12버전의 Next.js (페이지 라우터)를 사용하고 있으므로 NextApiHandler를 사용해서 요청이 오면 피드 XML을 반환하도록 작성해보자.
 
pages/api/rss.xml.ts
import { getPosts } from "@/src/libs/apis" import { NextApiHandler } from "next" import RSS from "rss" const rss: NextApiHandler = async (req, res) => { // 피드 생성하고 설정하기 const feed = new RSS({ title: "dongho-log", description: "신동호 기술 블로그" site_url: "https://www.dongho.xyz", feed_url: "https://www.dongho.xyz/api/rss.xml", }) // 모든 글 긁어와서 공개된 글만 필터링 const allPosts = await getPosts() const publicPosts = allPosts.filter( (post) => post.status && post.status.includes("Public") ) // 피드에 아이템 넣기 publicPosts.map((post) => { feed.item({ title: post.title, url: `https://www.dongho.xyz/${post.slug}`, date: post.createdTime, description: post.summary ?? "", }) }) // 헤더 캐시 설정 res.setHeader("Content-Type", "text/xml") res.setHeader( "Cache-Control", "public, s-maxage=1200, statle-while-revalidate=600" ) // RSS 피드를 XML로 포맷팅하여 응답 res.write(feed.xml({ indent: true })) res.end() } export default rss
 
NextResponse API 등을 사용하는 미들웨어를 작성하고 리다이렉트를 시키든 하는 과정도 있지만 일단 주요 코드는 위와 같다.
 
로컬에서 빌드한 뒤 localhost:3000/api/rss.xml로 접근하면 아래처럼 RSS 피드가 잘 보인다.
notion image
 

배포 페이지에서 문제 발생

실제 Vercel로 배포된 페이지에서 .../api/rss.xml로 접근해보니 시간이 좀 지나다가 아래와 같은 에러 페이지가 떴다.
 
notion image
(연결도 정상이고, Vercel도 정상인디, 서버리스 함수가 타임아웃 되었다네요…)
 
Vercel을 통해 배포된 Next.js 앱은 서버리스 아키텍처를 사용하는데, 이 때 Next API 라우팅을 사용하면 해당 API 라우팅은 서버리스 함수로 실행된다. Vercel Hobby(무료) 플랜의 경우 서버리스 함수의 durations가 기본 값 10초로 제한되어 있다.
 
저 라우팅에 실행되는 함수가 10초가 넘는다는 소리다. 서버리스 함수의 콜드 스타트 문제일까? 코드가 복잡하지 않지만 블로그 글이 늘어갈 수록 더 많은 시간은 걸리겠지만 초 단위의 시간이 소모될 것 같지는 않다. 해결 방법을 찾아보자.
 

Maximum Duration 변경하기

vercel 공식 문서를 확인해보면 Hobby 플랜도 Serverless Function의 Duration을 따로 컨트롤할 수 있는 기능을 제공하고 있다.
 
notion image
 
vercel.json 파일을 만들어서 위와 같이 특정 함수에 대한 maxDuration을 지정해줄 수 있다. (Node.js, SvelteKit, Astro, Nuxt, Remix 그리고 Next.js 13.5 버전 이상부터는 함수에서 직접 maxDuration을 변수로 지정해서 내보내면 알아서 작동한다고 한다.)
 
하기와 같이 vercel.json 파일을 만들어서 재배포해보자.
 
// vercel.json { "functions": { "src/pages/api/rss.xml.ts": { "maxDuration": 30 } } }
  • 넉넉하게 30초로 해줬다.
 
notion image
 
시간은 조금 걸리지만 이제 동적으로 RSS 피드를 생성하여 제공된다.
 
콜드 스타트(Cold Start) 문제였을까?
콜드 스타트(Cold Start) 문제였을까?
서버리스 아키텍처(Serverless Architecture)에서 콜드 스타트(Cold Start)는 첫 실행 및 초기화에 한해 응답 지연이 발생하는 문제다. 아마도 Notion API를 통해 내 Notion 페이지의 하위 페이지들을 모두 읽어오는 시간 때문에 오래 걸리는 것일 수도 있겠지만 콜드 스타트로 인한 응답 지연 이슈도 더해지지 않았을까?
 
이후에는 설정해둔 응답 헤더의 캐시 컨트롤 덕분에 거의 즉답 수준의 속도로 응답할 수 있었다.
 
 

정적 생성도 제공하기


미리 생성한 피드를 정적으로 제공하는 방식도 넣어 놓자.
 
src/libs/apis 하위에 generateRssFeed.ts 파일을 만들고 아래와 같이 작성했다.
import { TPosts, TPost } from "@/src/types" import RSS from "rss" export async function generateRssFeed(allPosts: TPosts) { const feed = new RSS({ title: "dongho-log", site_url: "https://www.dongho.xyz", feed_url: "https://www.dongho.xyz/rss.xml", description: "신동호 기술 블로그", }) const publicPosts = allPosts.filter( (post: TPost) => post.status && post.status.includes("Public") ) publicPosts.map((post, _i) => { feed.item({ title: post.title, url: `https://www.dongho.xyz/${post.slug}`, date: post.createdTime, description: post.summary ?? "", }) }) const rss = feed.xml({ indent: true }) return rss }
 
src/pages/index.tsx의 getStaticProps에 RSS를 추가해서 정적으로 제공하도록 한다.
export async function getStaticProps() { const posts = await getPosts() //... 생략 const rssFilePath = path.join(process.cwd(), "public/rss.xml") try { await fs.access(rssFilePath) } catch (error) { const rss = await generateRssFeed(posts) await fs.writeFile(rssFilePath, rss) } //... 생략 }
  • rss.xml이 없는 경우에만 파일을 생성하도록 작성
 
위에서 작성했던 API를 통한 동적으로 RSS를 제공하는 코드도 하기와 같이 수정해준다.
import { getPosts, generateRssFeed } from "@/src/libs/apis" import { NextApiHandler } from "next" const rss: NextApiHandler = async (req, res) => { const allPosts = await getPosts() const rss = await generateRssFeed(allPosts) res.setHeader("Content-Type", "text/xml") res.setHeader( "Cache-Control", "public, s-maxage=1200, statle-while-revalidate=600" ) res.write(rss) res.end() } export default rss
  • generateRssFeed 함수를 재사용하도록 수정
 
이제 /rss.xml로 정적 파일에도 접근이 가능하고, /api/rss.xml 로 동적으로 rss를 생성하여 제공할 수도 있게 되었다. 굳이 두 방법을 모두 사용할 필요는 없고, 본인 프로젝트에 맞게 한 가지 방법을 선택해서 제공하면 될 것 같다. 내 경우 그냥 학습하는 의미로 두 방법을 모두 넣어두었다.
 
 

참고 문서