소유한 도메인(gitsam.com)을 사용하여 Google Drive 공유 file URL link (long) 등을 단축 URL link (short)로 변환하여 제공하는 서비스를, 거의 무료 수준의 FaaS(서버리스) 기반 도구를 사용해 만들기.

FaaS(서버리스) 플랫폼을 활용하여, 소유한 도메인으로 Google Drive 공유 링크와 같은 긴 URL을 짧게 줄여주는 URL 단축기를 만드는 실습 과정을 안내합니다.

https://developers.google.com/program/plans-and-pricing

프로젝트 개요

구현 목표

  • 커스텀 도메인 사용: gitsam.com과 같은 개인 도메인으로 서비스 운영
  • URL 변환: Google Drive 공유 링크 등 긴 URL을 짧은 URL로 변환
  • 저비용 운영: 가능하면 무료 등급(Free Tier) 내에서 모든 기능 구현
  • 서버리스 아키텍처: FaaS 기반으로 인프라 관리 부담 최소화
  • 보안 고려: 예측 불가능한 키 생성 및 생성 권한 관리

보안 고려사항

  • 예측 불가능한 키: 단축 URL에 사용될 키는 UUID 또는 해시(Hash) 기반의 랜덤 문자열로 생성하여 추측을 방지합니다.
  • 원본 링크 권한 유지: 단축 URL을 통해 공유하더라도 원본 링크가 비공개라면 접근할 수 없습니다. 서비스는 리다이렉트만 수행할 뿐, 원본의 접근 권한을 변경하지 않습니다.
  • 접근 로그 (선택): 각 플랫폼의 로깅 기능을 활용하여 단축 URL 접근 기록을 모니터링할 수 있습니다.

핵심 구성 요소

  1. 도메인: 단축 URL의 기반이 될 사용자 소유의 도메인 (gitsam.com)
  2. URL 단축 로직: 짧은 키를 생성하고, 이 키와 원본 URL을 매핑하여 저장한 뒤, 요청 시 원본 URL로 리다이렉트하는 기능
  3. 데이터 저장소: 생성된 짧은 키와 원본 URL의 매핑 정보를 저장할 데이터베이스 (DB)
  4. 서버리스 백엔드: URL 단축 로직을 실행할 FaaS 플랫폼 (예: Cloudflare Workers, Firebase Functions)
  5. 도메인 연결: 도메인의 DNS 설정을 통해 FaaS 플랫폼으로 요청을 전달하는 구성

FaaS 플랫폼 선택 및 비교

초기 비용 없이 시작할 수 있고 운영이 간편한 FaaS 플랫폼은 다음과 같습니다.

플랫폼 주요 장점 무료 제공 범위
Cloudflare Workers 압도적인 속도, 글로벌 CDN 기본 제공, 사용하기 쉬운 KV 저장소 일일 요청 10만 건
Vercel Functions 간편한 배포, Next.js 등 프론트엔드 프레임워크와 호환성 우수 월 100GB 대역폭 등
Firebase (Functions) Google 생태계와의 강력한 연동 (인증, DB, 호스팅) 월 함수 호출 200만 건 등
AWS Lambda 가장 높은 유연성과 확장성, 다양한 서비스와 연동 가능 월 요청 100만 건
📌 프로젝트 추천 플랫폼: Cloudflare Workers 또는 Firebase

구현 방법 1: Cloudflare Workers 활용

Cloudflare Workers는 도메인 연결이 매우 쉽고, 간단한 Key-Value 저장소인 KV를 무료로 제공하여 DB 없이도 빠르게 URL 단축기를 만들 수 있습니다.

비용 구조

항목 예상 비용
Cloudflare Workers 무료 (일일 요청 10만 건)
Cloudflare KV 저장소 무료 (읽기 10만/일, 쓰기 1,000/일, 1GB 데이터)
도메인 및 CDN/SSL 무료 (도메인은 기소유)

Workers 코드 예시

Wrangler CLI 또는 Cloudflare 대시보드에서 Worker를 생성하고 아래 코드를 배포합니다.

index.js

javascript
export default {
  async fetch(request, env) {
    // 요청 URL에서 경로(path) 부분을 추출합니다. 예: https://gitsam.com/abc123 -> /abc123
    const url = new URL(request.url);
    const path = url.pathname.slice(1);

    // 경로가 존재할 경우 (단축 URL 접속 시)
    if (path) {
      // KV 저장소(LINKS)에서 경로(path)를 키(key)로 사용하여 원본 URL을 조회합니다.
      const originalURL = await env.LINKS.get(path);

      if (originalURL) {
        // 원본 URL이 존재하면 302 리다이렉트 응답을 보냅니다.
        return Response.redirect(originalURL, 302);
      }
      // 원본 URL이 없으면 404 Not Found 응답을 보냅니다.
      return new Response("URL Not Found", { status: 404 });
    }

    // 경로가 없을 경우 (메인 도메인 접속 시)
    return new Response("gitsam.com URL Shortener", { status: 200 });
  }
};

KV 저장소 설정 및 데이터 등록

  1. Cloudflare 대시보드에서 Workers & Pages > KV 메뉴로 이동합니다.
  2. LINKS라는 이름의 KV Namespace를 생성합니다.
  3. Worker 설정에서 KV Namespace Bindings를 추가하여 LINKS 변수와 방금 생성한 KV를 연결합니다.
  4. LINKS KV에 직접 데이터를 추가합니다.

도메인 연결

  1. Cloudflare에 등록된 gitsam.com 도메인의 DNS 설정으로 이동합니다.
  2. Workers Routes 탭에서 gitsam.com/* 경로의 요청을 위에서 생성한 Worker로 라우팅하도록 설정합니다. SSL/TLS는 자동으로 적용됩니다.

구현 방법 2: Firebase 활용

FirebaseHosting(호스팅), Cloud Functions(클라우드 함수), Firestore(DB) 를 조합하여 보다 체계적인 URL 단축기를 만들 수 있습니다. 특히 관리자만 링크를 생성하도록 **인증(Authentication)**을 붙이기에 용이합니다.

Firebase 서비스 구성

  • Firebase Hosting: link.gitsam.com과 같은 커스텀 도메인을 연결하고, 특정 경로의 요청을 Cloud Functions로 전달합니다.
  • Cloud Functions: URL 리다이렉트 로직과 단축 URL 생성 API를 처리합니다.
  • Cloud Firestore: 단축 키와 원본 URL의 매핑 정보, 클릭 횟수 등을 저장합니다.
  • Firebase Auth: (선택) 관리자 계정만 URL을 생성할 수 있도록 보안을 강화합니다.

Firestore 데이터 스키마

  • Collection: links
  • Document ID: hpd34133 (단축 키, slug)
  • Fields:

Cloud Functions 코드 (TypeScript)

functions/src/index.ts

typescript
import { onRequest } from "firebase-functions/v2/https";
import { setGlobalOptions } from "firebase-functions/v2";
import { initializeApp } from "firebase-admin/app";
import { getFirestore, FieldValue } from "firebase-admin/firestore";
import * as crypto from "crypto";

// 전역 옵션 설정: 서울(asia-northeast3) 리전, 256MB 메모리
setGlobalOptions({ region: "asia-northeast3", memory: "256MiB" });

initializeApp();
const db = getFirestore();

// 랜덤 키 생성을 위한 Base62 인코딩 함수
function generateSlug(len = 6): string {
  const ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
  const bytes = crypto.randomBytes(8);
  let num = BigInt("0x" + bytes.toString("hex"));
  const base = BigInt(62);
  let slug = "";
  while (slug.length < len) {
    slug = ALPHABET[Number(num % base)] + slug;
    num /= base;
  }
  return slug.slice(0, len);
}

// GET /:slug 요청 시 원본 URL로 리다이렉트
export const go = onRequest(async (req, res) => {
  const slug = req.path.replace(/^\/+/, "");
  if (!slug) {
    return res.status(400).send("Short URL key is missing.");
  }

  const linkDoc = db.collection("links").doc(slug);
  const snap = await linkDoc.get();

  if (!snap.exists) {
    return res.status(404).send("URL Not Found");
  }

  const data = snap.data();
  if (!data?.active || !data?.url) {
    return res.status(404).send("This link is inactive or invalid.");
  }
  
  // 비동기적으로 클릭 카운트 업데이트
  await linkDoc.update({
    count: FieldValue.increment(1),
    lastHitAt: FieldValue.serverTimestamp(),
  });
  
  return res.redirect(302, data.url);
});

// POST /api/shorten 요청 시 단축 URL 생성
export const shorten = onRequest(async (req, res) => {
  if (req.method !== "POST") {
    return res.status(405).send("Method Not Allowed. Please use POST.");
  }
  
  // 간단한 API 키 인증 (실제 서비스에서는 Firebase Auth 사용 권장)
  const apiKey = process.env.ADMIN_API_KEY;
  if (!apiKey || req.get("x-api-key") !== apiKey) {
    return res.status(401).send("Unauthorized");
  }

  const { url, slug: customSlug } = req.body;
  if (typeof url !== "string" || !url.startsWith("http")) {
    return res.status(400).send("Invalid URL provided.");
  }

  let slug = customSlug || generateSlug(6);
  const docRef = db.collection("links").doc(slug);
  const doc = await docRef.get();

  if (doc.exists) {
    return res.status(409).send(`'${slug}' already exists.`);
  }

  await docRef.set({
    url,
    active: true,
    createdAt: FieldValue.serverTimestamp(),
    createdBy: "api",
    count: 0,
  });

  const host = req.get("host");
  res.status(201).json({ short: `https://${host}/${slug}`, slug });
});

Hosting 라우팅 설정

firebase.json 파일에서 Hosting으로 들어온 요청을 Cloud Functions로 전달하도록 설정합니다.

json
{
  "hosting": {
    "public": "public",
    "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
    "rewrites": [
      {
        "source": "/api/shorten",
        "function": "shorten"
      },
      {
        "source": "/:slug",
        "function": "go"
      }
    ]
  }
}

배포

프로젝트 설정이 완료되면 아래 명령어로 Firebase에 배포합니다.

Bash

firebase deploy --only functions,hosting

구현 방법 3: Google App Engine 활용

Google App Engine (GAE)PaaS (Platform-as-a-Service) 로, Node.js Express나 Python Django와 같은 웹 프레임워크로 작성된 애플리케이션을 그대로 배포할 수 있어 자유도가 높습니다.

Firebase와 App Engine의 차이점

  • Firebase: BaaS (Backend-as-a-Service). Hosting, Functions 등 독립된 모듈을 조합하는 방식. 빠른 개발에 유리.
  • App Engine: PaaS. 하나의 완성된 웹 애플리케이션을 배포하는 방식. 복잡한 서버 구조와 높은 자유도가 필요할 때 유리.

App Engine 코드 예시 (Node.js + Express)

app.js

javascript
const express = require("express");
const { Firestore } = require("@google-cloud/firestore");

const app = express();
const db = new Firestore();

app.use(express.json());

// 리다이렉트 엔드포인트
app.get("/:slug", async (req, res) => {
  const { slug } = req.params;
  const docRef = db.collection("links").doc(slug);
  const doc = await docRef.get();

  if (!doc.exists) {
    return res.status(404).send("Not Found");
  }
  res.redirect(302, doc.data().url);
});

// 단축 URL 생성 API
app.post("/api/shorten", async (req, res) => {
  const { url } = req.body;
  if (!url) {
    return res.status(400).send("URL is required.");
  }
  
  const slug = Math.random().toString(36).substring(2, 8);
  await db.collection("links").doc(slug).set({ url });

  const domain = process.env.SERVICE_DOMAIN || req.get('host');
  res.status(201).json({ short: `https://${domain}/${slug}` });
});

const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
  console.log(`Server listening on port ${PORT}`);
});

App Engine 배포 설정

app.yaml 파일에 런타임 환경과 인스턴스 클래스 등을 정의합니다.

yaml
runtime: nodejs20
instance_class: F1
env_variables:
  SERVICE_DOMAIN: "gitsam.com"
  • runtime: nodejs20: Node.js 20 런타임을 사용합니다.
  • instance_class: F1: 가장 저렴한 기본 인스턴스 유형으로, 무료 등급에 해당합니다.

배포

gcloud CLI를 사용하여 App Engine에 배포합니다.

Bash

gcloud app deploy