Computer >> 컴퓨터 >  >> 프로그래밍 >> Redis

Astro, Upstash, GitHub으로 무료 오픈소스 LinkTree 대안 서비스 만들기

이 글에서는 LinkTree의 오픈소스 대안인 itsmy.fyi를 Astro, Upstash, GitHub, Edgio를 활용해 어떻게 구축했는지 소개합니다. Upstash는 모든 사용자 데이터를 관리(CRUD)할 수 있게 해주었고, GitHub API보다 훨씬 넉넉한 요청 제한을 제공했으며, 세밀한 단위의 레이트 리밋(rate limiting) 구현도 가능하게 해주었습니다.

Astro, Upstash, GitHub으로 무료 오픈소스 LinkTree 대안 서비스 만들기

사용하는 기술 스택

  • Astro — 프론트엔드 및 백엔드
  • Upstash — 레이트 리밋 & CRUD 작업 처리
  • GitHub 이슈 & 웹훅(Webhooks) — 사용자 프로필을 관리하는 공개 CMS
  • Tailwind CSS — 스타일링
  • Edgio — 배포

준비물

  • GitHub 계정
  • 데이터베이스 생성용 Upstash 계정

Upstash Redis 설정하기

Upstash 계정을 생성하고 로그인했다면, Redis 탭으로 이동해 데이터베이스를 생성합니다.

Astro, Upstash, GitHub으로 무료 오픈소스 LinkTree 대안 서비스 만들기

Astro, Upstash, GitHub으로 무료 오픈소스 LinkTree 대안 서비스 만들기

데이터베이스 생성이 완료되면 Details(상세 정보) 탭으로 이동합니다. 아래로 스크롤하여 Connect your database 섹션을 찾은 후, 내용을 복사해 안전한 곳에 저장해 둡니다.

Astro, Upstash, GitHub으로 무료 오픈소스 LinkTree 대안 서비스 만들기

이어서 REST API 섹션까지 스크롤한 뒤 .env 버튼을 선택하고, 마찬가지로 내용을 복사해 안전한 곳에 보관합니다.

Astro, Upstash, GitHub으로 무료 오픈소스 LinkTree 대안 서비스 만들기

프로젝트 설정하기

앱 저장소를 클론한 뒤, 이 튜토리얼을 따라 프로젝트의 모든 내용을 익혀보세요. 프로젝트를 포크하려면 다음 명령어를 실행합니다:

git clone https://github.com/rishi-raj-jain/itsmy.fyi
cd itsmy.fyi
yarn install

저장소를 클론한 후에는 .env 파일을 생성하고, 앞서 저장해 둔 값들을 추가합니다. 완성된 파일은 대략 다음과 같습니다:

# GitHub 저장소에서 발급받은 값
GITHUB_API_TOKEN="to_create_and_update_github_comments"
GITHUB_WEBHOOK_SECRET="if_you_are_matching_github_webhooks_sha256"
 
# 위 단계에서 얻은 값
UPSTASH_DB="your_upstash_redis_from_above"
UPSTASH_REDIS_REST_URL="your_upstash_redis_rest__url_from_above"
UPSTASH_REDIS_REST_TOKEN="your_upstash_redis_rest__token_from_above"

여기까지 완료했다면, 다음 명령어로 로컬 환경을 실행할 수 있습니다:

yarn run edgio:dev

저장소 구조

다음은 프로젝트의 주요 폴더 구조입니다. 빨간색으로 표시된 파일들은 이 글에서 다룰 CRUD 작업과 레이트 리밋 관련 파일들, 그리고 해당 기능들이 참조되는 위치입니다.

Astro, Upstash, GitHub으로 무료 오픈소스 LinkTree 대안 서비스 만들기

데이터 흐름 개요

다음은 데이터가 흐르는 방식에 대한 개략적인 다이어그램입니다.

  • 사용자가 itsmy.fyi/me/slug 페이지를 방문했을 때, 해당 페이지의 응답이 캐시되어 있지 않거나 재검증 중이라면 getUserInfo 함수가 호출되고, 이 함수는 Upstash DB에서 사용자 JSON을 가져옵니다.
  • 사용자가 GitHub 이슈를 생성·수정·삭제하면 GitHub이 웹훅(Webhook)을 트리거하여 특정 엔드포인트로 POST 요청을 보냅니다. 해당 엔드포인트에서는 먼저 Upstash 레이트 리밋으로 요청된 변경이 허용 가능한지 판단한 후, Upstash를 통해 사용자 JSON을 생성·수정·삭제합니다.

Astro, Upstash, GitHub으로 무료 오픈소스 LinkTree 대안 서비스 만들기

Upstash Redis를 활용한 사용자 프로필 CRUD 작업

이 섹션에서는 사용자 프로필의 조회, 수정, 삭제가 실제로 어떻게 처리되는지 자세히 살펴보겠습니다. 데이터를 가져오고 화면에 표시하기 위해 ioredis를 통해 Upstash를 지속적으로 활용합니다.

CRUD 작업의 기반을 GitHub에서 Upstash로 옮긴 이유

처음에는 GitHub을 데이터 관리의 중심으로 삼았습니다. GitHub 이슈를 데이터 입력 폼으로 사용하고, GitHub 웹훅으로 저장소 내 사용자 JSON을 CRUD하는 방식이었습니다. 하지만 GitHub REST API의 제한인 저장소당 시간당 1,000회 요청은 플랫폼의 의도된 사용 방식을 오히려 저해했습니다.

반면 Upstash는 무료 플랜에서 하루 10K 커맨드를 제공할 정도로 넉넉했고, 사용량이 늘어나도 비용 증가폭이 매우 미미했습니다. 덕분에 거의 무료에 가까운 비용으로 더 많은 사용자를 확보할 수 있었고, 데이터베이스 확장과 비용 관리를 걱정하지 않고도 빠르게 반복 개발(iteration)을 진행할 수 있었습니다.

getUserInfo: 사용자 프로필 조회 함수

getUserInfo 함수는 ioredis의 hget을 사용하며, 고유한 slug를 키로 삼아 Upstash에 API 요청을 보내 해당 사용자의 프로필 페이지 데이터를 가져옵니다. 해당 프로필이 존재하지 않거나 오류가 발생하면 함수는 { code: 0 } 객체를 반환하도록 설계되어 있으며, 이 경우 Astro의 동적 라우트에서 자동으로 404 페이지로 리디렉션됩니다.

// File: lib/Upstash/users/get.js
// Read User Profile Code
 
import redis from "../setup";
 
export async function getUserInfo(slug) {
 try {
 const userData = await redis.hget("profiles", slug);
 const parsedData = JSON.parse(userData);
 if (parsedData.slug === slug) {
 return { ...parsedData, code: 1 };
 }
 return {
 code: 0,
 error: `slug doesn't match for the user.`,
 };
 } catch (e) {
 const error = e.message || e.toString();
 console.log(error);
 return {
 code: 0,
 error,
 };
 }
}

나머지 CRUD 작업 코드는 다음과 같습니다:

import redis from "../setup";
 
// File: @/lib/Upstash/users/delete.js
// Delete User Profile Code
export async function deleteUserInfo(slug) {
 try {
 await redis.hdel("profiles", slug);
 return { code: 1 };
 } catch (e) {
 console.log(e.message || e.toString());
 return {
 code: 0,
 };
 }
}
 
// File: @/lib/Upstash/users/post.js
// Create/Update User Profile Code
export async function postUserInfo(data) {
 try {
 await redis.hset("profiles", data.slug, JSON.stringify(data));
 return { code: 1 };
 } catch (e) {
 const error = e.message || e.toString();
 console.log(error);
 return {
 code: 0,
 error,
 };
 }
}

레이트 리밋(Rate Limiting)

Edgio 환경의 서버리스에서 레이트 리밋을 구현하기 위해 Upstash Redis 데이터베이스 클라이언트와 @upstash/ratelimit 라이브러리를 사용합니다.

// Reference Function to ratelimiting
 
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
 
import { getENV } from "@/lib/env";
 
const url = getENV("UPSTASH_REDIS_REST_URL");
const token = getENV("UPSTASH_REDIS_REST_TOKEN");
 
export const ratelimit = (number, time) => {
 if (url && token) {
 return new Ratelimit({
 redis: new Redis({
 url,
 token,
 }),
 limiter: Ratelimit.fixedWindow(number, time),
 });
 }
 return;
};

레이트 리밋을 적용함으로써 다음 두 가지 목표를 달성할 수 있었습니다.

A. 서비스를 완전 무료 · 무제한으로 제공

레이트 리밋 덕분에 프로필 생성 API를 공개적으로 노출할 수 있었습니다! 이를 통해 GUI만으로 손쉽게 프로필을 설정할 수 있다는 시스템의 장점을 보여줄 수 있었습니다. 누구나 웹사이트(itsmy.fyi)에서 일주일에 3개의 프로필을 생성할 수 있고, 프로필 편집이나 무제한 프로필 생성 같은 기능을 원한다면 GitHub 방식으로 전환하면 됩니다. 여기서는 IP 주소를 키로 삼아 주당 3개 프로필이라는 제한을 적용했습니다.

// Rate limit 3 profiles in a week via the web for a user
const ratelimitUser = ratelimit(3, 7 * 24 * 60 * 60 + " s");
 
if (rateLimiter) {
 // Look at the x-0-client-ip set by Edgio in serverless
 const result = await rateLimiter.limit("x-0-client-ip");
 limit = result.limit;
 remaining = result.remaining;
 if (!result.success) {
 // Return a message
 }
}

B. 사용자별 수정 횟수에 대한 세분화된 조절

또한 레이트 리밋을 활용해 사용자의 GitHub 사용자명을 기준으로 분당 수정 횟수를 조절할 수 있습니다. 현재는 1분에 최대 3번의 변경을 허용하며, 이를 통해 예상치 못한 스팸을 줄일 수 있습니다.

const rateLimiter = ratelimit(3, "60 s");
 
if (rateLimiter) {
 const result = await rateLimiter.limit(context.sender.login);
 limit = result.limit;
 remaining = result.remaining;
 if (!result.success) {
 return {
 headers: {
 "X-RateLimit-Limit": limit,
 "X-RateLimit-Remaining": remaining,
 },
 body: JSON.stringify({
 message:
 "Too many updates in 1 minute. Please try again in a few minutes.",
 }),
 };
 }
}

엣지에서 Stale-While-Revalidate로 모든 사용자 프로필 캐싱하기

다음 코드는 Stale While Revalidate 개념을 활용해 캐시 히트율을 높이는 방법을 보여줍니다. routes.js의 코드에서 router.match 함수를 사용해 /me/로 시작하는 모든 사용자 프로필 경로를 매칭합니다. cache 메서드 내부에서는 브라우저 캐싱을 차단하고 엣지 캐싱만 활성화하여, 항상 빠른 속도와 최신 콘텐츠를 사용자에게 제공합니다.

edge 옵션은 maxAgeSeconds: 1로 설정해 데이터를 단 1초 동안만 캐시하도록 하고, staleWhileRevalidateSeconds 옵션은 1년으로 설정해 캐시가 갱신되는 동안에도 기존 캐시 데이터를 직접 제공할 수 있게 합니다.

// User path(s)
router.match("/me/:path", ({ cache, removeUpstreamResponseHeader }) => {
 // Remove the cache-control header from Astro's standalone server
 removeUpstreamResponseHeader("cache-control");
 // Disable in browser caching, and use Edgio's edge to use SWR
 cache({
 edge: {
 maxAgeSeconds: 1,
 staleWhileRevalidateSeconds: 60 * 60 * 24 * 365,
 },
 browser: false,
 });
});

Stale While Revalidate를 활용하면 서버 부하를 줄이고 사용자에게 더 빠른 응답을 제공함으로써 앱 성능을 크게 향상시킬 수 있습니다.

동적 사용자 프로필 즉석 생성하기

Astro는 동적 라우트를 설정하기 매우 쉽습니다. 앱에서 src/pages/me/[slug].astro 파일을 찾을 수 있는데, 이는 /me/로 시작하는 페이지들을 매핑합니다. 예를 들어 /me/rishi-raj-jain, /me/some-other-user 같은 경로가 있습니다.

사용자 프로필 가져오기

Astro params에서 추출한 slug 쿼리 파라미터를 활용해 현재 사용자의 데이터를 가져오며, 앞서 설명한 getUserInfo 함수를 호출해 관련된 모든 사용자 데이터를 얻습니다. 데이터를 찾지 못하거나 오류가 발생하면 방문자를 404 페이지로 리디렉션합니다.

import { getUserInfo } from "@/lib/Upstash/users";
 
// Extract slug query
const { slug } = Astro.params;
 
// Get data from Upstash using the getUserInfo function
const {
 name: userName,
 image: userImage,
 links = [],
 socials = [],
 about = "",
 og = {},
 background = {},
 code = 1,
} = await getUserInfo(userSlug);
 
// In case the code: 0 is recevied, redirect to a 404
if (code === 0) {
 return Astro.redirect("/404");
}

CLI로 배포하기

다음 명령어로 프로덕션 빌드를 수행하고 로컬에서 테스트할 수 있습니다:

yarn run edgio:build && yarn run edgio:production

배포를 위해서는 Edgio 계정이 필요합니다. 무료로 회원가입할 수 있으니 여기서 가입하세요. 계정 생성 후 프로젝트 루트 폴더에서 다음 명령어를 실행하면 Edgio에 배포됩니다:

yarn run edgio:deploy

이제 배포가 끝났습니다! 네, 이게 전부입니다.

마무리

결론적으로, 이 프로젝트를 통해 세분화된 레이트 리밋 구현, 서버리스 환경에서의 CRUD 데이터 작업, GitHub 이슈를 CMS로 활용하는 방법, 그리고 필요에 맞게 확장되는 서비스(즉, Upstash)를 선택해 MVP를 출시하는 현명한 의사결정까지 값진 경험을 얻을 수 있었습니다.