엣지 컴퓨팅 플랫폼 비교 분석 2025

26 min read
compareedge-computingcloudflareawsfastlyvercelcdnserverless

Cloudflare Workers vs AWS Lambda@Edge vs Fastly Compute@Edge - 엣지 컴퓨팅 선택 가이드

🌐 엣지 컴퓨팅 플랫폼 비교 분석 2025

글로벌 저지연 애플리케이션을 위한 엣지 컴퓨팅 플랫폼 선택 가이드


📊 개요

비교 대상

  • Cloudflare Workers: 가장 큰 엣지 네트워크
  • AWS Lambda@Edge: AWS 통합 엣지 컴퓨팅
  • Fastly Compute@Edge: 고성능 엣지 플랫폼
  • Vercel Edge Functions: Next.js 최적화
  • Deno Deploy: 모던 JavaScript 런타임
  • Netlify Edge Functions: JAMstack 친화적

평가 기준

  • 글로벌 커버리지
  • 성능 (콜드 스타트, 응답 시간)
  • 지원 언어/런타임
  • 가격
  • 기능 제한
  • 개발자 경험

📈 상세 비교표

핵심 특성 비교

| 특성 | Cloudflare Workers | Lambda@Edge | Fastly Compute | Vercel Edge | | ------------------ | ------------------ | ----------- | -------------- | ----------- | | PoP 수 | 280+ | 400+ | 60+ | 전 세계 | | 콜드 스타트 | <10ms | 100-300ms | <50ms | <50ms | | 최대 실행 시간 | 30초 | 30초 | 60초 | 25초 | | 메모리 | 128MB | 128MB | 128MB | 128MB | | CPU 시간 | 10ms-50ms | 제한 없음 | 제한 없음 | 제한 있음 | | 언어 지원 | JS/Wasm | JS/Python | Rust/JS/Go | JS/TS |

기술적 제한사항

| 항목 | Cloudflare | Lambda@Edge | Fastly | Vercel | | ------------- | ---------- | ----------- | --------- | ------ | | 번들 크기 | 1MB | 1MB | 100MB | 1MB | | 환경 변수 | 1MB | 4KB | 제한 없음 | 64KB | | 요청 크기 | 100MB | 40KB | 8MB | 4.5MB | | 응답 크기 | 무제한 | 1MB | 무제한 | 4.5MB | | WebSocket | ✅ | ❌ | ✅ | 제한적 | | 스토리지 | KV/R2/D1 | ❌ | KV Store | KV |

가격 비교 (월 기준)

| 사용량 | Cloudflare | Lambda@Edge | Fastly | Vercel | | ------------- | ---------- | ----------- | ------ | ------ | | 10M 요청 | $5 | $6 | $50 | $20 | | 100M 요청 | $50 | $60 | $500 | $200 | | 1B 요청 | $500 | $600 | $5,000 | $2,000 | | 무료 티어 | 100K/일 | 제한적 | 없음 | 제한적 |


💼 사용 사례별 구현

🌍 글로벌 A/B 테스팅

추천: Cloudflare Workers

// Cloudflare Workers - A/B 테스팅 구현
export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);

    // 사용자 정보 추출
    const country = request.cf?.country || "US";
    const city = request.cf?.city || "Unknown";
    const colo = request.cf?.colo || "Unknown";

    // 사용자 ID 또는 세션 기반 일관된 할당
    const userId =
      request.headers.get("x-user-id") || (await generateUserId(request));

    // A/B 그룹 결정 (일관성 보장)
    const abGroup = await getABGroup(userId, env.AB_TEST);

    // KV에서 테스트 설정 로드
    const testConfig = await env.CONFIG_KV.get("ab-test-config", "json");
    const variant = testConfig.variants[abGroup];

    // 요청 라우팅
    let response;
    if (variant.type === "redirect") {
      // 다른 오리진으로 리다이렉트
      response = await fetch(variant.origin + url.pathname, request);
    } else if (variant.type === "rewrite") {
      // HTML 수정
      response = await fetch(request);
      response = await modifyHTML(response, variant.modifications);
    } else {
      // 프록시
      response = await fetch(request);
    }

    // 메트릭 수집 (비동기)
    ctx.waitUntil(
      collectMetrics(
        {
          testId: testConfig.id,
          variant: abGroup,
          userId,
          country,
          city,
          timestamp: Date.now(),
          responseTime: Date.now() - startTime,
        },
        env.ANALYTICS,
      ),
    );

    // 응답에 헤더 추가
    response = new Response(response.body, response);
    response.headers.set("x-ab-variant", abGroup);
    response.headers.set("x-edge-location", colo);

    return response;
  },
};

// HTML 수정 함수
async function modifyHTML(response, modifications) {
  const rewriter = new HTMLRewriter();

  modifications.forEach((mod) => {
    rewriter.on(mod.selector, {
      element(element) {
        if (mod.action === "replace") {
          element.setInnerContent(mod.content);
        } else if (mod.action === "setAttribute") {
          element.setAttribute(mod.attribute, mod.value);
        } else if (mod.action === "addClass") {
          element.setAttribute(
            "class",
            element.getAttribute("class") + " " + mod.className,
          );
        }
      },
    });
  });

  return rewriter.transform(response);
}

// 메트릭 수집
async function collectMetrics(data, analytics) {
  // Analytics Engine으로 전송
  await analytics.writeDataPoint({
    indexes: [data.testId, data.variant],
    blobs: [data.country, data.city],
    doubles: [data.responseTime],
    timestamp: data.timestamp,
  });

  // 실시간 집계를 위한 Durable Object 업데이트
  const id = analytics.idFromName(`test-${data.testId}`);
  const stub = analytics.get(id);
  await stub.fetch("https://internal/increment", {
    method: "POST",
    body: JSON.stringify(data),
  });
}

// Durable Object for 실시간 집계
export class ABTestAnalytics {
  constructor(state, env) {
    this.state = state;
    this.env = env;
  }

  async fetch(request) {
    const url = new URL(request.url);

    if (url.pathname === "/increment") {
      const data = await request.json();
      await this.increment(data);
      return new Response("ok");
    }

    if (url.pathname === "/stats") {
      const stats = await this.getStats();
      return new Response(JSON.stringify(stats));
    }
  }

  async increment(data) {
    const key = `${data.variant}:${data.country}`;
    const current = (await this.state.storage.get(key)) || {
      impressions: 0,
      totalTime: 0,
    };

    current.impressions++;
    current.totalTime += data.responseTime;

    await this.state.storage.put(key, current);
  }
}

🔒 보안 게이트웨이

추천: AWS Lambda@Edge

// Lambda@Edge - 보안 검증 및 필터링
exports.handler = async (event, context) => {
  const request = event.Records[0].cf.request;
  const headers = request.headers;

  // 1. IP 기반 지역 차단
  const clientIp = headers["cloudfront-viewer-address"][0].value;
  const country = headers["cloudfront-viewer-country"][0].value;

  if (await isBlockedCountry(country)) {
    return {
      status: "403",
      statusDescription: "Forbidden",
      body: "Access denied from your region",
    };
  }

  // 2. DDoS 방어 - Rate Limiting
  const rateLimitKey = `ratelimit:${clientIp}`;
  const requests = await incrementAndGetCount(rateLimitKey);

  if (requests > 100) {
    // 분당 100 요청 제한
    return {
      status: "429",
      statusDescription: "Too Many Requests",
      headers: {
        "retry-after": [{ key: "Retry-After", value: "60" }],
      },
    };
  }

  // 3. WAF 규칙 실행
  const wafResult = await runWAFRules(request);
  if (wafResult.blocked) {
    await logSecurityEvent({
      type: "waf_block",
      rule: wafResult.rule,
      ip: clientIp,
      uri: request.uri,
      headers: headers,
    });

    return {
      status: "403",
      statusDescription: "Forbidden",
      body: "Request blocked by security rules",
    };
  }

  // 4. JWT 토큰 검증 (API 보호)
  if (request.uri.startsWith("/api/")) {
    const authHeader = headers.authorization?.[0]?.value;

    if (!authHeader || !authHeader.startsWith("Bearer ")) {
      return {
        status: "401",
        statusDescription: "Unauthorized",
        headers: {
          "www-authenticate": [
            {
              key: "WWW-Authenticate",
              value: 'Bearer realm="api"',
            },
          ],
        },
      };
    }

    const token = authHeader.substring(7);
    try {
      const payload = await verifyJWT(token);

      // 토큰 정보를 헤더에 추가하여 오리진에 전달
      request.headers["x-user-id"] = [
        {
          key: "X-User-Id",
          value: payload.sub,
        },
      ];
      request.headers["x-user-role"] = [
        {
          key: "X-User-Role",
          value: payload.role,
        },
      ];
    } catch (error) {
      return {
        status: "401",
        statusDescription: "Unauthorized",
        body: "Invalid token",
      };
    }
  }

  // 5. 요청 변환 및 보안 헤더 추가
  request.headers["x-real-ip"] = [
    {
      key: "X-Real-IP",
      value: clientIp,
    },
  ];
  request.headers["x-forwarded-country"] = [
    {
      key: "X-Forwarded-Country",
      value: country,
    },
  ];

  // 민감한 헤더 제거
  delete request.headers["x-amzn-trace-id"];
  delete request.headers["x-amz-cf-id"];

  return request;
};

// WAF 규칙 실행
async function runWAFRules(request) {
  const rules = [
    // SQL Injection 탐지
    {
      name: "sql_injection",
      pattern:
        /((union|select|insert|update|delete|drop|create)\s+.*\s+(from|into|where|table))|(-{2})|(\/\*|\*\/)/i,
      target: ["uri", "querystring", "body"],
    },
    // XSS 탐지
    {
      name: "xss",
      pattern:
        /(<script[^>]*>|javascript:|onerror=|onload=|alert\(|document\.|window\.|eval\()/i,
      target: ["querystring", "body"],
    },
    // Path Traversal
    {
      name: "path_traversal",
      pattern: /(\.\.\/|\.\.\\/i,
      target: ["uri", "querystring"],
    },
  ];

  for (const rule of rules) {
    for (const target of rule.target) {
      let value = "";

      if (target === "uri") {
        value = request.uri;
      } else if (target === "querystring") {
        value = request.querystring;
      } else if (target === "body" && request.body) {
        value = Buffer.from(request.body.data, "base64").toString();
      }

      if (rule.pattern.test(value)) {
        return { blocked: true, rule: rule.name };
      }
    }
  }

  return { blocked: false };
}

⚡ 실시간 개인화

추천: Fastly Compute@Edge

// Fastly Compute@Edge - Rust로 구현한 실시간 개인화
use fastly::{Error, Request, Response};
use fastly::http::{header, Method, StatusCode};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Serialize, Deserialize)]
struct UserProfile {
    id: String,
    preferences: HashMap<String, String>,
    history: Vec<String>,
    segment: String,
}

#[derive(Serialize, Deserialize)]
struct PersonalizationRule {
    segment: String,
    modifications: Vec<Modification>,
}

#[derive(Serialize, Deserialize)]
struct Modification {
    selector: String,
    action: String,
    value: String,
}

#[fastly::main]
fn main(req: Request) -> Result<Response, Error> {
    // 사용자 식별
    let user_id = get_user_id(&req);

    // 사용자 프로필 로드 (엣지 KV에서)
    let profile = match load_user_profile(&user_id) {
        Some(p) => p,
        None => create_default_profile(&user_id),
    };

    // 개인화 규칙 로드
    let rules = load_personalization_rules();
    let applicable_rules: Vec<_> = rules.iter()
        .filter(|r| r.segment == profile.segment)
        .collect();

    // 오리진 요청
    let mut beresp = req.clone_without_body()
        .send("origin_0")?;

    // HTML 응답인 경우 개인화 적용
    if is_html_response(&beresp) {
        let body = beresp.take_body_str();
        let personalized_body = apply_personalizations(
            &body,
            &applicable_rules,
            &profile
        );

        beresp.set_body(personalized_body);
    }

    // 추천 헤더 추가
    if req.get_path().starts_with("/api/recommendations") {
        let recommendations = generate_recommendations(&profile);
        beresp.set_header(
            "X-Recommendations",
            serde_json::to_string(&recommendations)?
        );
    }

    // 개인화 메트릭 수집 (비동기)
    log_personalization_event(PersonalizationEvent {
        user_id: user_id.clone(),
        segment: profile.segment.clone(),
        path: req.get_path().to_string(),
        timestamp: std::time::SystemTime::now(),
        applied_rules: applicable_rules.len(),
    });

    Ok(beresp)
}

fn apply_personalizations(
    html: &str,
    rules: &[&PersonalizationRule],
    profile: &UserProfile
) -> String {
    let mut result = html.to_string();

    for rule in rules {
        for modification in &rule.modifications {
            result = match modification.action.as_str() {
                "replace" => {
                    // CSS 선택자 기반 교체
                    replace_by_selector(
                        &result,
                        &modification.selector,
                        &modification.value
                    )
                },
                "inject" => {
                    // 동적 콘텐츠 주입
                    inject_content(
                        &result,
                        &modification.selector,
                        &personalize_content(&modification.value, profile)
                    )
                },
                "remove" => {
                    // 요소 제거
                    remove_by_selector(&result, &modification.selector)
                },
                _ => result,
            };
        }
    }

    result
}

fn generate_recommendations(profile: &UserProfile) -> Vec<String> {
    // 사용자 히스토리 기반 추천 생성
    let mut recommendations = Vec::new();

    // 협업 필터링 로직
    let similar_users = find_similar_users(&profile.id);
    for user in similar_users {
        let user_profile = load_user_profile(&user).unwrap();
        for item in user_profile.history {
            if !profile.history.contains(&item) {
                recommendations.push(item);
            }
        }
    }

    // 콘텐츠 기반 필터링
    for item in &profile.history {
        let similar_items = find_similar_items(item);
        recommendations.extend(similar_items);
    }

    // 중복 제거 및 상위 N개 반환
    recommendations.dedup();
    recommendations.truncate(10);

    recommendations
}

// WebAssembly 모듈 호출 (복잡한 ML 모델)
#[link(wasm_import_module = "ml_model")]
extern "C" {
    fn predict_user_segment(features: *const f32, len: usize) -> i32;
}

fn update_user_segment(profile: &mut UserProfile) {
    // 특징 추출
    let features = extract_features(profile);

    // WASM ML 모델 호출
    let segment_id = unsafe {
        predict_user_segment(features.as_ptr(), features.len())
    };

    profile.segment = match segment_id {
        0 => "casual".to_string(),
        1 => "power_user".to_string(),
        2 => "enterprise".to_string(),
        _ => "default".to_string(),
    };
}

🎯 Next.js 앱 최적화

추천: Vercel Edge Functions

// Vercel Edge Functions - Next.js 미들웨어
import { NextRequest, NextResponse } from "next/server";
import { geolocation, ipAddress } from "@vercel/edge";

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};

export async function middleware(request: NextRequest) {
  // 지역 정보 추출
  const { country, city, region } = geolocation(request);
  const ip = ipAddress(request);

  // 1. 지역별 리다이렉트
  if (request.nextUrl.pathname === "/") {
    const locale = getLocaleByCountry(country);
    if (locale && locale !== "en") {
      return NextResponse.redirect(new URL(`/${locale}`, request.url));
    }
  }

  // 2. 기능 플래그 (엣지에서 결정)
  const features = await getFeatureFlags({
    userId: request.cookies.get("userId")?.value,
    country,
    ip,
  });

  // 3. 실험 그룹 할당
  const experiments = assignExperiments(request, features);

  // 4. 성능 최적화 - 이미지 포맷 협상
  if (request.nextUrl.pathname.match(/\.(jpg|jpeg|png|gif)$/)) {
    const accept = request.headers.get("accept") || "";

    if (accept.includes("image/avif")) {
      return NextResponse.rewrite(
        new URL(
          request.nextUrl.pathname.replace(/\.[^.]+$/, ".avif"),
          request.url,
        ),
      );
    } else if (accept.includes("image/webp")) {
      return NextResponse.rewrite(
        new URL(
          request.nextUrl.pathname.replace(/\.[^.]+$/, ".webp"),
          request.url,
        ),
      );
    }
  }

  // 5. 봇 감지 및 처리
  const isBot = detectBot(request.headers.get("user-agent") || "");
  if (isBot) {
    // 봇용 정적 버전 제공
    return NextResponse.rewrite(new URL("/api/static-render", request.url));
  }

  // 6. 보안 헤더 추가
  const response = NextResponse.next();

  response.headers.set("X-Frame-Options", "DENY");
  response.headers.set("X-Content-Type-Options", "nosniff");
  response.headers.set("X-XSS-Protection", "1; mode=block");
  response.headers.set("Content-Security-Policy", getCSPHeader(features));

  // 7. 쿠키 설정 (실험 및 기능 플래그)
  response.cookies.set("features", JSON.stringify(features), {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "lax",
    maxAge: 60 * 60 * 24 * 7, // 1주일
  });

  response.cookies.set("experiments", JSON.stringify(experiments), {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "lax",
    maxAge: 60 * 60 * 24 * 30, // 30일
  });

  // 8. 커스텀 헤더 추가 (디버깅용)
  response.headers.set("X-Edge-Location", city || "unknown");
  response.headers.set("X-Edge-Country", country || "unknown");
  response.headers.set("X-Edge-IP", ip || "unknown");

  return response;
}

// 기능 플래그 로드 (Edge Config)
async function getFeatureFlags(context: {
  userId?: string;
  country?: string;
  ip?: string;
}) {
  const flags = await fetch(`https://edge-config.vercel.com/flags`, {
    headers: {
      Authorization: `Bearer ${process.env.EDGE_CONFIG_TOKEN}`,
    },
  }).then((res) => res.json());

  // 조건부 플래그 평가
  const evaluatedFlags: Record<string, boolean> = {};

  for (const [key, config] of Object.entries(flags)) {
    if (typeof config === "boolean") {
      evaluatedFlags[key] = config;
    } else if (typeof config === "object") {
      // 복잡한 규칙 평가
      evaluatedFlags[key] = evaluateRule(config, context);
    }
  }

  return evaluatedFlags;
}

// 실험 할당
function assignExperiments(
  request: NextRequest,
  features: Record<string, boolean>,
): Record<string, string> {
  const experiments: Record<string, string> = {};
  const userId = request.cookies.get("userId")?.value || generateUserId();

  // 각 실험에 대해 일관된 할당
  if (features.newCheckoutFlow) {
    experiments.checkoutVariant = hashToVariant(
      userId,
      ["control", "variant-a", "variant-b"],
      "checkout-experiment",
    );
  }

  if (features.personalizedHomepage) {
    experiments.homepageVariant = hashToVariant(
      userId,
      ["default", "personalized", "ai-powered"],
      "homepage-experiment",
    );
  }

  return experiments;
}

// 일관된 해시 기반 할당
function hashToVariant(
  userId: string,
  variants: string[],
  experimentName: string,
): string {
  const hash = require("crypto")
    .createHash("md5")
    .update(userId + experimentName)
    .digest("hex");

  const hashInt = parseInt(hash.substring(0, 8), 16);
  const index = hashInt % variants.length;

  return variants[index];
}

🏢 실제 기업 사례

Cloudflare Workers 사용

  • Discord: API 라우팅 및 보안
  • Shopify: 동적 콘텐츠 캐싱
  • DoorDash: 지역별 라우팅

Lambda@Edge 사용

  • Amazon Prime Video: 비디오 스트리밍 최적화
  • Slack: 정적 자산 최적화
  • 많은 미디어 기업: 콘텐츠 보호

Fastly Compute@Edge 사용

  • The New York Times: 콘텐츠 전달
  • Reddit: 트래픽 관리
  • Stripe: API 가속

Vercel Edge Functions 사용

  • TikTok: 웹 프론트엔드
  • Hulu: 스트리밍 서비스
  • 많은 Next.js 앱: SSR 최적화

🔧 개발 도구 및 테스팅

로컬 개발 환경

# Cloudflare Workers
wrangler dev

# Vercel Edge
vercel dev

# Fastly
fastly compute serve

# Deno Deploy
deno task dev

테스팅 전략

| 플랫폼 | 단위 테스트 | 통합 테스트 | E2E 테스트 | | --------------- | ----------- | ------------ | ---------- | | Cloudflare | Vitest | Miniflare | Wrangler | | Lambda@Edge | Jest | SAM Local | AWS SDK | | Fastly | Rust tests | Local server | CLI | | Vercel | Jest | Edge Runtime | Playwright |


💰 비용 최적화 전략

비용 절감 팁

  1. 캐싱 극대화: 엣지에서 최대한 캐시
  2. 조건부 요청: 필요한 경우만 오리진 호출
  3. 압축: Brotli/Gzip 적극 활용
  4. 이미지 최적화: 실시간 리사이징

ROI 계산

// 엣지 컴퓨팅 ROI 계산기
function calculateROI({
  monthlyRequests,
  avgLatencyReduction, // ms
  conversionImpact, // %
  avgOrderValue,
  currentConversionRate,
}) {
  // 지연시간 감소로 인한 수익 증가
  const latencyImpact = (avgLatencyReduction / 100) * 0.01; // 100ms = 1% 전환율
  const newConversionRate = currentConversionRate * (1 + latencyImpact);

  // 월간 추가 수익
  const additionalRevenue =
    monthlyRequests *
    (newConversionRate - currentConversionRate) *
    avgOrderValue;

  // 엣지 컴퓨팅 비용
  const edgeCost = calculateEdgeCost(monthlyRequests);

  return {
    additionalRevenue,
    edgeCost,
    netBenefit: additionalRevenue - edgeCost,
    roi: ((additionalRevenue - edgeCost) / edgeCost) * 100,
  };
}

🎯 선택 가이드

Cloudflare Workers 선택 시

✅ 최대 글로벌 커버리지
✅ 낮은 지연시간 중요
✅ KV/R2/D1 필요
✅ WebSocket 지원
❌ 복잡한 계산 작업
❌ 긴 실행 시간

Lambda@Edge 선택 시

✅ AWS 에코시스템
✅ CloudFront 사용 중
✅ 보안 기능 중요
✅ Python 지원 필요
❌ 빠른 콜드 스타트
❌ 실시간 양방향 통신

Fastly Compute@Edge 선택 시

✅ 고성능 필요
✅ Rust/Go 선호
✅ 대용량 처리
✅ 미디어 스트리밍
❌ 작은 규모
❌ 저렴한 비용

Vercel Edge Functions 선택 시

✅ Next.js 사용
✅ 프론트엔드 최적화
✅ 간단한 설정
✅ TypeScript 중심
❌ 복잡한 백엔드
❌ Non-JS 워크로드


📚 추가 리소스

공식 문서

성능 벤치마크

  • Edge Computing Report 2024
  • CDN Performance Benchmark
  • Real User Monitoring (RUM) 데이터

커뮤니티

  • Cloudflare Developers Discord
  • AWS Edge Services Forum
  • Vercel Community
  • Edge Computing Subreddit

💡 핵심 조언: 엣지 컴퓨팅은 "사용자와의 거리"를 줄이는 기술입니다. 단순히 "빠르다"가 아니라 "어디서나 빠르다"가 중요합니다. 글로벌 서비스라면 Cloudflare Workers, AWS 중심이라면 Lambda@Edge, Next.js 앱이라면 Vercel Edge Functions를 선택하세요. 중요한 것은 사용자 경험 개선이지, 기술 자체가 아닙니다.

Found this helpful? Share it with others!
Tweet

🔗 Related Content

You might also be interested in these articles

🏢 company

🏢 Vercel 기술 스택 분석

버셀이 프론트엔드 배포의 미래를 만드는 기술 스택 심층 분석 - Next.js, Edge Functions, 그리고 글로벌 엣지 네트워크

28 min read
vercel, nextjs +9
Read more
🏢 company

🏢 Canva 기술 스택 분석

캔바가 1억 명 이상의 사용자에게 쉽고 강력한 디자인 도구를 제공하는 기술 스택 심층 분석 - Java, TypeScript, AWS로 구축한 대규모 디자인 플랫폼

32 min read
canva, java +10
Read more
🏢 company

🏢 Netflix 기술 스택 분석

넷플릭스가 전 세계 2.4억 사용자에게 스트리밍 서비스를 제공하는 기술 스택 심층 분석 - 마이크로서비스, AWS, Chaos Engineering의 교과서

9 min read
netflix, microservices +11
Read more
🏢 company

🏢 Notion 기술 스택 분석

노션이 수천만 사용자에게 All-in-One 워크스페이스를 제공하는 기술 스택 심층 분석 - 블록 기반 에디터, 실시간 협업, 그리고 확장 가능한 데이터베이스

24 min read
notion, typescript +11
Read more

Found this helpful?

Help us improve this content by contributing on GitHub or sharing your feedback with the community.