들어가며
“매일 특정 시간에, RSS로 블로그 데이터를 받아와서, 등록된 사용자에게 이메일을 전송하고 싶다.”라는 요구사항이 있다. 오늘은 이 요구사항을 충족시키는 개발을 해보자.
요구사항 분석
- 매일 특정 시간에
매일 특정 시간에 실행시키는 기술에는 대표적으로 Cron이 있다. Cron은 Unix/Linux 시스템에서 작업을 특정 시간에 자동으로 실행하도록 예약하는 시스템이다.
전통적인 Cron작업은 아래와 같은 명령어를 입력해 crontab(리눅스의 크론)을 수정할 수 있다.
crontab -e
0 0 * * * /home/user/backup.sh
위와 같이 입력하면 매일 밤 12시에 backup이라는 쉘 파일을 실행한다.
그 외에도 AWS CloudWatch Events, Google Cloud Scheduler, Azure Logic Apps, GitHub Actions, Vercel Cron Jobs 등의 기술을 활용해서 Cron 작업을 실행할 수도 있다.
현재 서비스는 Vercel로 배포를 하고 있었고, Vercel Cron Jobs를 활용하면 간편하게 Cron 작업을 실행할 수 있을 거라 생각했다. 아래와 같이 코드 몇 줄로 Cron Jobs를 등록할 수 있다.
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"crons": [
{
"path": "/api/hello",
"schedule": "0 5 * * *"
}
]
}
- RSS로 블로그 데이터를 받아와서
RSS(Really Simple Syndication/Rich Site Summary)는 웹사이트의 새 콘텐츠를 쉽게 받아볼 수 있도록 하는 기술이다. 블로그나 뉴스 사이트처럼 업데이트가 잦은 곳에서 주로 쓰고, RSS 리더로 새로운 소식을 한 곳에 모아 볼 수 있다.
toss.tech같은 기술 블로그는 toss.tech/rss.xml과 같이 RSS를 제공해서 모든 글을 볼 수 있게 해준다. 이 URL을 통해 누구나 해당 블로그의 최신 글을 받아볼 수 있으며, RSS 리더나 스크립트를 통해 자동으로 새 콘텐츠를 가져올 수 있다.
RSS를 쉽게 가져올 수 있는 라이브러리로는 Node.js 환경에서 사용할 수 있는 rss-parser가 있다. 이 라이브러리는 RSS 피드를 JSON 형식으로 파싱해줘서 데이터를 다루기 쉽고, 여러 형식의 RSS(RSS 1.0, RSS 2.0, Atom)를 지원해서 대부분의 블로그나 뉴스 사이트 피드를 가져올 수 있다. 그 외에는 Python 기반의 feedparser 등이 있다. 호스팅 서비스에는 Feedly, Inoreader 등이 있다.
이 중에서 나는 Node.js 기반의 개발을 할 것이기에 rss-parser를 사용할 것이다.
- 등록된 사용자에게 이메일을 전송하고 싶다.
이메일을 전송할 수 있는 Node.js 기반 기술에는 NodeMailer, emailjs, sendgrid-nodejs 등이 있고 클라우드 SaaS인 Resend 등이 있다.
이 중에서 나는 초기 설정이 간편하고, 다양한 개발자 도구를 제공하는 Resend를 선택했다.
개발
우리가 개발할 요구사항의 가장 핵심은 기능은 “RSS로 블로그 데이터를 받아오는”것이다. 이 핵심 기능을 가장 먼저 구현해보도록 하자.
1. RSS로 블로그 데이터 받아오기
npm install rss-parser- RSS 패칭
import Parser from "rss-parser";
const parser = new Parser();
const feed = await parser.parseURL('https://toss.tech/rss.xml');
위와 같이 Parser 인스턴스를 만들고, parseUrl메서드에 rss URL을 전달하면 된다.
이 때, 이 작업을 서버 환경에서 실행해야한다. 브라우저 환경에서 실행할 경우 아래와 같이 CORS에러가 발생한다.
이는 브라우저의 보안 정책인 동일 출처 정책(SOP, Same Origin Policy)에 의한 것이다. 다른 도메인의 리소스에 접근하려면 해당 서버에서 CORS(Cross Origin Resource Sharing)를 허용해야 한다. RSS 피드를 제공하는 서버가 브라우저의 요청에 대해 CORS를 허용하지 않으므로, 서버 사이드에서 요청을 보내는 방식으로 우회해야 한다.
Next.js 프레임워크를 사용한다면 Next.js 서버가 Node.js 서버 환경이고, API Routes를 활용한다면 이 문제를 쉽게 해결할 수 있다. 만약 Next.js 프레임워크를 사용하지 않는다면 Node.js 서버를 직접 구축하거나, Netlify Functions, AWS Lambda와 같은 서버리스 환경을 활용할 수 있다.
서버 환경에서 데이터를 패칭하면 아래와 같이 데이터가 온다.
{
// ..
feedUrl: "https://toss.tech/rss.xml",
paginationLinks: { self: "https://toss.tech/rss.xml" },
title: "토스테크",
description: "토스의 개발과 디자인에 대한 이야기를 다룹니다.",
generator: "토스팀",
link: "https://toss.tech",
language: "ko",
copyright: "Viva Republica, All rights reserved.",
lastBuildDate: "Mon, 14 Jul 2025 00:01:24 GMT",
docs: "https://validator.w3.org/feed/docs/rss2.html"
}
위 데이터는 우리가 전달한 RSS 페이지인 https://toss.tech/rss.xml의 각 내용임을 확인할 수 있다.
rss-parser는 위와 같은 XML형식을 JSON 형식으로 변환해서 전달을 하는 것이다. 이제 우리는 이 데이터를 원하는대로 활용하면 된다.
새로 올라온 글만 골라내기
RSS를 받아오면 최신 글 여러 개가 통째로 온다. 그런데 매일 이걸 그대로 메일로 보내면, 어제 이미 보낸 글을 오늘 또 보내는 참사가 벌어진다. 그래서 '지난번에 보낸 시점 이후에 올라온 글'만 골라내야 한다.
가장 간단한 방법은 각 글의 pubDate(발행 시각)를 기준으로 거르는 것이다. 마지막으로 메일을 보낸 시각을 어딘가에 저장해두고, 그 시각보다 나중에 올라온 글만 남기면 된다.
const lastSentAt = await getLastSentAt(); // 지난번 발송 시각 (DB나 KV에 저장해둔 값)
const newItems = feed.items.filter((item) => {
if (!item.pubDate) {
return false;
}
return new Date(item.pubDate) > lastSentAt;
});
솔직히 고백하면, 지금 서비스는 이 부분이 아직 제대로 안 잡혀 있다. pubDate 포맷이 블로그마다 제각각이라(어떤 곳은 GMT, 어떤 곳은 KST, 아예 없는 곳도 있다) 생각보다 신경 쓸 게 많았다. 그래서 지금은 단순하게 처리해두고, 중복 제거를 제대로 붙이는 건 다음 숙제로 남겨뒀다.
2. 등록된 사용자에게 이메일을 전송하기
- Resend 회원가입
- 도메인 구매
Resend를 이용해서 이메일을 전송하기 위해서는 도메인이 있어야한다. 이 도메인은 이메일을 전송한 사람의 이메일 주소가 된다.
위와 같이 tech-mail.shop이 도메인이다.
도메인은 가비아, squarespace, GoDaddy, Ionos 등 수많은 도메인 구매 사이트에서 구매하면 된다. 나는 한국의 도메인 구매 서비스인 가비아에서 가장 저렴한 tech-mail.shop을 구매했다.
- 도메인 인증
- 도메인 구매 사이트에서 DNS 설정에 들어간다.
DNS 설정에는 레코드를 추가할 수 있는 버튼이 있을 것이다. 이 버튼을 클릭해보자.
💡 DNS(Domain Name System)
: 인터넷의 전화번호부같은 이름
- 사람이 기억하기 쉬운 도메인 이름(e.g. google.com)을 컴퓨터가 이해할 수 있는 IP 주소(e.g. 172.217.175.78)로 변환해준다.
- DNS 레코드 타입
- A : 도메인을 IPv4 주소로 연결
- AAAA : 도메인을 IPv6 주소로 연결
- CName : 도메인을 다른 도메인으로 연결(별칭)
- MX : 메일 서버 지정
- TXT : 텍스트 정보 저장 (SPF, DKIM)
2. Resend에서 추가해야할 레코드를 확인해보자.
Resend의 Domain탭에서 Add Domain으로 도메인을 추가해보자. 2번에서 구매한 도메인을 입력하면 아래와 같은 화면을 볼 수 있다.
현재 나는 인증을 마친 상태라 Status가 Verified이지만, 인증을 하지 않았다면 아무 것도 없을 것이다. 여기서 Required라 적혀있는 DKIM and SPF를 도메인 구매 사이트에서 레코드로 등록해서 인증을 하면 된다.
3. 1번의 레코드에 2번의 정보들을 기입한다.
여기서 TTL은 네트워크 패킷의 수명으로, 최대 기한인 86400으로 설정하면 된다.
💡 TTL(Time To Live)
: DNS 레코드가 캐시에 저장되어 있을 수 있는 최대 시간
- 초 단위로 설정
- DNS 서버가 레코드를 조회할 때 TTL도 함께 받으며, TTL 시간이 지나면 캐시에서 삭제하며, 다음 요청 시 다시 원본 DNS 서버에 질의한다.
모든 DNS 레코드를 추가하고 조금만 기다리면 Verified 상태의 도메인이 될 것이다.
- 이메일 전송하기
export async function sendEmail({ to, subject, react, from }: SendEmailParams) {
const resend = new Resend(process.env.RESEND_API_KEY);
return resend.emails.send({
from: 'tech-mail <noreply@tech-mail.shop>',
to,
subject,
react,
});
}
Resend Api키를 Resend 객체에 전달해서 인스턴스를 생성한 후, email.send 메서드로 이메일을 전송하면 된다. 이 때, from 필드에는 우리가 만든 도메인의 이메일 주소를 입력하면 된다.
한 가지 주의할 점이 있다. API 키에는 절대 NEXT_PUBLIC_ 접두사를 붙이면 안 된다. Next.js는 NEXT_PUBLIC_이 붙은 환경 변수를 클라이언트 번들에 그대로 넣어버리기 때문에, 이렇게 하면 API 키가 브라우저로 노출된다. 이메일 전송은 서버(API Routes)에서만 일어나니까, 접두사 없는 RESEND_API_KEY로 두고 서버에서만 읽으면 된다.
3. 매일 특정 시간에 이메일 전송하기
Vercel Cron Jobs 설정은 매우 간단하다. 아래와 같이 언제 실행할 건지, 어떤 경로의 API Routes를 실행할 것인지 명시만 해주면 된다.
// vercel.json
{
"crons": [
{
"path": "/api/cron/check-rss",
"schedule": "0 23 * * *"
}
]
}
💡 Cron 시간이 왜 이상하게 밀려 있지?
: 위 스케줄이
0 23인 게 눈에 띌 거다. 아침에 받고 싶다면서 왜 밤 11시일까?
- Vercel Cron은 UTC 기준으로 동작한다. 한국(KST)은 UTC+9라, 한국 시간 아침 8시에 받으려면 UTC로는 전날 23시(
0 23 * * *)로 적어야 한다. 이 9시간 시차를 빼먹으면 엉뚱한 시간에 메일이 날아간다.- 그리고 Hobby(무료) 플랜에서는 Cron이 하루 1회로 제한된다. "아침 저녁 두 번 보내야지" 했다가 막히는 수가 있으니, 여러 번 돌려야 한다면 요금제부터 확인하자.
결과
매일 같은 시간에 구독한 블로그의 아티클을 모아서 보내주는 것을 확인할 수 있다.
서비스 이용하러가기
맺으며
이렇게 "매일 특정 시간에, RSS로 블로그 데이터를 받아와서, 등록된 사용자에게 이메일을 전송하고 싶다."라는 요구사항을 모두 구현해보았다.
돌아보면 rss-parser, Resend, Vercel Cron 세 가지 조합으로 반나절 만에 믈대가 나왔다. 제일 애먹은 건 의외로 CORS였다. 브라우저에서 바로 RSS를 가져올 수 있을 줄 알았는데, 결국 API Routes로 서버를 한 번 거쳐야 했다.
또한 이메일 전송을 위해서는 도메인이 필요했는데, 도메인 구매부터 DNS 설정, Resend 도메인 인증까지의 과정이 생각보다 복잡했다. 한 번 잡아두니 그다음부턴 편해서 할 만했다.
이메일 전송으로 실시간 피드백을 받을 수 있고, 내가 겪던 불편함을 해결하는 서비스를 만들다 보니 시간 가는 줄 모를 정도로 몰입할 수 있었다. 프론트엔드 개발자를 선택한 것이 정말 잘한 결정이라는 생각이 계속해서 들었다.
앞으로 이 서비스를 실제로 운영하려면 개선할 점이 많다. 중복된 RSS 항목 필터링, 이메일 템플릿 개선 및 개인화, 사용자별 구독 설정 등이 필요하다. 이것들도 하나씩 붙여나갈 생각을 하니 벌써 재밌다. 앞으로도 실생활의 문제를 개발로 풀어가고 싶다. 정말 재밌다.