엣지에서 개인화 서비스하기: Vercel과 Cloudflare로 50ms 안에 사용자별 콘텐츠 제공하기

프론트엔드

엣지 컴퓨팅VercelCloudflare WorkersNext.js Middleware개인화

이 글은 누구를 위한 것인가

  • 글로벌 서비스에서 응답 속도 문제를 겪는 프론트엔드 개발자
  • A/B 테스트나 지역별 콘텐츠를 빠르게 서빙하고 싶은 팀
  • Vercel Edge Functions, Cloudflare Workers가 무엇인지 이해하고 싶은 분

들어가며

"서울에서는 빠른데 미국 사용자가 느리다고 해요."

이 문제의 근본 원인은 서버 위치다. 서버가 서울에 있으면 서울 사용자는 빠르지만, 미국 사용자는 데이터가 태평양을 왕복하는 시간(약 150~200ms)이 더해진다.

기존 CDN은 정적 파일(HTML, CSS, 이미지)만 전 세계에 분산 저장하고 빠르게 제공했다. 동적 콘텐츠(사용자별 개인화, A/B 테스트, 인증 검사)는 여전히 오리진 서버를 거쳐야 했다.

엣지 컴퓨팅은 이 한계를 넘는다. CDN 서버에서 단순 파일 제공을 넘어, 코드 실행이 가능해진다. 사용자와 가장 가까운 서버(엣지 노드)에서 개인화 로직이 실행되고, 결과가 즉시 응답된다.


1. 엣지 컴퓨팅이란 (CDN과의 차이)

[기존 CDN]
사용자 ──▶ CDN (정적 파일 캐시) ──▶ 오리진 서버 (동적 처리)
                                        (멀면 150~300ms 추가)

[엣지 컴퓨팅]
사용자 ──▶ 엣지 노드 (코드 실행 가능) ──▶ 오리진 서버 (필요 시만)
            └─ 50ms 이내 응답 가능

Vercel은 전 세계 70+ 엣지 위치, Cloudflare는 300+ 엣지 위치에 노드를 운영한다. 한국 사용자는 서울 또는 일본 엣지 노드가 처리한다.

엣지에서 실행 가능한 것들:

  • 인증 쿠키 검증 → 로그인 여부에 따라 리다이렉트
  • 지역 감지 → 국가별 다른 언어 페이지로 리다이렉트
  • A/B 테스트 → 어떤 변형을 보여줄지 결정
  • 요청 변환 → 헤더 추가/수정, URL 재작성
  • 사용자 세그멘트 분류 → 개인화된 응답 선택

2. Next.js Middleware로 엣지 개인화 구현

Next.js의 middleware.ts는 기본적으로 엣지에서 실행된다. 모든 요청이 오리진 서버에 도달하기 전에 Middleware를 거친다.

기본 Middleware 구조

// middleware.ts (프로젝트 루트에 위치)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // 요청에서 정보 추출
  const country = request.geo?.country || 'KR';
  const userAgent = request.headers.get('user-agent') || '';

  // 응답 헤더에 정보 추가 (페이지에서 사용 가능)
  response.headers.set('x-user-country', country);

  return response;
}

// Middleware 적용 경로 설정
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};

지역 기반 언어 리다이렉트

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const country = request.geo?.country;

  // 루트 경로에서만 적용
  if (pathname === '/') {
    // 일본에서 접속하면 일본어 페이지로
    if (country === 'JP') {
      return NextResponse.redirect(new URL('/ja', request.url));
    }
    // 미국에서 접속하면 영어 페이지로
    if (country === 'US') {
      return NextResponse.redirect(new URL('/en', request.url));
    }
    // 기본은 한국어 유지
  }

  return NextResponse.next();
}

A/B 테스트 구현

export function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // 기존 쿠키 확인 (재방문 사용자는 같은 변형 유지)
  const variant = request.cookies.get('ab-variant')?.value;

  if (!variant) {
    // 새 사용자: 무작위로 변형 배정
    const newVariant = Math.random() < 0.5 ? 'A' : 'B';
    response.cookies.set('ab-variant', newVariant, {
      maxAge: 60 * 60 * 24 * 30, // 30일
      httpOnly: true,
    });
    response.headers.set('x-ab-variant', newVariant);
  } else {
    response.headers.set('x-ab-variant', variant);
  }

  return response;
}

페이지에서 헤더를 읽어 어떤 변형인지 알 수 있다:

// app/page.tsx
import { headers } from 'next/headers';

export default async function Page() {
  const headersList = await headers();
  const variant = headersList.get('x-ab-variant');

  return variant === 'A' ? <VariantA /> : <VariantB />;
}

3. Cloudflare Workers로 더 강력한 엣지 로직

Cloudflare Workers는 Next.js에 종속되지 않는 범용 엣지 플랫폼이다. Node.js 환경이 아닌 V8 엔진 기반의 자체 런타임을 사용한다.

Workers + KV Storage로 개인화

Cloudflare KV는 엣지 노드에 분산 저장되는 키-값 스토리지다. 사용자 세그먼트 정보를 저장하고 조회하는 데 적합하다.

// Cloudflare Worker 예시
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const userId = getCookieValue(request, 'user-id');

    let userSegment = 'anonymous';

    if (userId) {
      // KV에서 사용자 세그먼트 조회 (전 세계 엣지에서 빠르게)
      userSegment = await env.USER_SEGMENTS.get(userId) || 'new-user';
    }

    // 세그먼트에 따라 다른 콘텐츠 경로로 라우팅
    if (userSegment === 'vip') {
      url.pathname = `/vip${url.pathname}`;
    }

    // 오리진 요청에 사용자 정보 헤더 추가
    const modifiedRequest = new Request(url.toString(), {
      ...request,
      headers: {
        ...request.headers,
        'x-user-segment': userSegment,
      },
    });

    return fetch(modifiedRequest);
  },
};

4. 엣지 캐시 전략: ISR과 엣지의 조합

정적 생성(SSG)과 엣지 캐시를 결합하면 최고의 성능을 낼 수 있다.

// Next.js의 ISR (Incremental Static Regeneration) + 엣지 캐시
async function ProductPage({ params }) {
  const product = await fetch(`https://api.example.com/products/${params.id}`, {
    next: {
      revalidate: 3600, // 1시간마다 백그라운드 재생성
      tags: [`product-${params.id}`], // 태그 기반 무효화
    },
  }).then(r => r.json());

  return <ProductDetail product={product} />;
}
// 특정 제품 정보가 바뀌면 해당 캐시만 무효화
import { revalidateTag } from 'next/cache';

// API 라우트나 Server Action에서
await revalidateTag(`product-${productId}`);

캐시 계층 구조

사용자 요청
  → Cloudflare 캐시 (가장 가까운 엣지, 수ms)
    → Vercel 엣지 캐시 (ISR 캐시, ~50ms)
      → Next.js 서버 (캐시 미스 시, 100~500ms)
        → 데이터베이스/API

첫 요청은 오리진까지 가지만, 이후 요청은 엣지 캐시에서 처리된다.


5. 엣지 환경의 제약사항

엣지 런타임은 Node.js가 아니다. 사용할 수 없는 API들이 있다.

사용 불가대안
fs (파일시스템)정적 import 또는 KV Storage
child_process없음 (엣지 특성상 불가)
대부분의 Node.js 내장 모듈Web API 사용
긴 실행 시간 (>30초)복잡한 처리는 오리진 서버로
대용량 메모리 (~128MB 제한)데이터 최소화

Next.js에서 특정 파일을 엣지 런타임으로 명시할 수 있다:

export const runtime = 'edge'; // 이 파일은 엣지에서 실행
// 또는
export const runtime = 'nodejs'; // Node.js에서 실행 (기본값)

6. 비용 분석: Vercel vs Cloudflare Workers

항목Vercel Edge FunctionsCloudflare Workers
무료 티어100만 요청/월10만 요청/일
유료$0.60/100만 요청$0.50/100만 요청
KV StorageNext.js Cache (포함)별도 유료
글로벌 위치70+300+
최대 실행 시간30초30초 (무료) / 무제한 (유료)

소규모 서비스라면 Vercel 무료 티어로 충분하다. 대규모 글로벌 서비스라면 Cloudflare의 더 많은 엣지 위치와 저렴한 단가가 유리하다.


맺으며

엣지 컴퓨팅은 "전 세계 어디서나 빠른 서비스"를 가능하게 하는 핵심 기술이다. Next.js Middleware 하나로 지역별 리다이렉트, A/B 테스트, 인증 검사를 엣지에서 처리할 수 있다.

가장 쉬운 시작점은 Next.js의 middleware.ts다. 오늘 당장 지역 감지 또는 간단한 A/B 테스트를 Middleware로 구현해보자. 오리진 서버에서 처리하던 로직이 엣지로 이동하면서 응답 속도가 눈에 띄게 개선될 것이다.