Edge Runtime 살펴보기

in-ch·2025년 9월 13일
1

꿀팁

목록 보기
17/17
post-thumbnail

들어가며

최근에 React Suspense 기반 데이터 fetcher 직접 구현해보기를 통해서 Suspense의 동작 원리에 대해서 살펴보며, Suspense의 핵심은 'Promise를 throw하여 렌더링을 중단시킨다'는 개념에 집중하여 직접 fetcher를 구현해 보았습니다.

이제는 Suspense를 실무 환경에서 사용할 때 동작 원리를 이해하는 것을 넘어서 특정 상황에서 Streaming을 더 빠르게 동작시키는 법을 살펴보도록 하겠습니다.

따라서 Edge runtime 모드에 대해서 알아보도록 하며, Edge runtime을 활용하여 Nextjs Suspense Streaming을 보다 효과적으로 사용하는 법을 살펴본 후 더 나아가 다국어 처리 및 middleware에서도 어떻게 효과적으로 적용되는지 살펴보겠습니다.

두가지 Runtime

Next.js는 애플리케이션의 서버 측 로직을 실행하기 위해 두 가지 다른 서버 런타임을 제공합니다.
개발자는 애플리케이션의 특정 요구사항에 맞게 이 두 런타임 중 하나를 선택하거나 혼합하여 사용할 수 있습니다.

두 런타임의 종류는 다음과 같습니다.

  • Node.js Runtime (기본값): 모든 Node.js API 및 생태계의 호환 가능한 패키지에 접근할 수 있습니다.
  • Edge Runtime: 더 제한된 API 세트를 포함합니다.

기본적으로 Next.js는 Node.js 런타임을 사용하며, 이는 우리가 일반적으로 알고 있는 서버 환경입니다. 그러나 Edge 런타임이라는 새로운 대안도 제공하는데, 이는 더 빠르고 효율적인 처리를 위해 설계된 경량 런타임입니다.

Edge runtime을 활용하면 Streaming / Suspense를 더 효과적으로 사용할 수 있으며 Middleware 및 글로벌 환경에서도 효과적입니다.

Node.js Runtime (기본값)

Node.js

Next.js의 기본 서버 런타임인 Node.js는 가장 익숙하고 광범위하게 사용되는 환경입니다.

Node.js 런타임은 서버 환경에서 JavaScript를 실행하기 위해 Google의 V8 엔진을 기반으로 구축되었습니다.
일반적으로 우리가 생각하는 Node.js 그대로를 생각하면 됩니다.

Edge Runtime

Edge

Edge Runtime은 웹 표준 API를 기반으로 구축된 경량 런타임입니다.

전 세계 여러 지점에 분산된 엣지 서버에서 실행되므로, 사용자에게 더 가까운 곳에서 코드를 실행하여 네트워크 지연 시간을 최소화할 수 있습니다.

Edge라는 이름에서 알 수 있듯이, 전 세계에 분산된 엣지 서버에서 실행됩니다. 이는 사용자에게 가장 가까운 위치에서 코드를 실행하여 네트워크 지연 시간을 최소화하는 핵심적인 장점을 제공합니다.

즉, 가장 큰 장점은 사용자와 가깝다는 것입니다.

예를 들어 북극에 서버가 있다고 가정해 보겠습니다. 이때 한국에서 데이터를 요청하면 수천 km가 떨어진 곳까지 데이터를 전송해야 하므로 시간은 매우 많이 소요될 수 밖에 없습니다.

이때 Edge Location을 활용하면 최대한 데이터를 요청한 최종 사용자의 위치에 가까운 데이터 센터에서 데이터를 전송할 수 있게 됩니다.

또한 Edge Runtime은 가볍고 '콜드 스타트' 시간이 거의 없어 요청에 매우 빠르게 응답할 수 있습니다.
Node.js Runtime에 비해 메모리 사용량이 적고, 초기화 비용이 낮아 성능 최적화에 유리합니다.

Edge Runtime을 쓰기 전에 주의해야 할 점

Edge Runtime은 강력하지만, 몇 가지 중요한 제약 사항이 있습니다.

  1. 배포 환경 의존성: Vercel에 배포했을 때 진정한 Edge의 성능을 경험할 수 있습니다.
    다른 클라우드 환경에서는 Edge Runtime을 직접 지원하지 않지만, Lambda@Edge나 CloudFront Functions와 같은 유사한 기능을 사용할 수는 있습니다.

  2. 제한된 API: Edge 런타임은 fs, net, child_process와 같은 Node.js 네이티브 API를 지원하지 않습니다. 이는 Edge 런타임이 Node.js 환경이 아닌, 웹 표준 기반의 런타임이기 때문입니다.

  3. 제한된 node_modules 사용: 파일 시스템 접근, 소켓 통신 등 Node.js에 특화된 기능은 사용할 수 없기 때문에 node_modules에 있는 라이브러리도 이러한 네이티브 API를 사용한다면 Edge 런타임에서 동작하지 않습니다.

  4. 데이터베이스 연결: 대부분의 전통적인 데이터베이스 드라이버(예: PostgreSQL, MySQL)는 TCP 소켓을 사용하므로 Edge 런타임에서 직접 연결하는 것은 제한적입니다. 그러나 Vercel에서 지원하는 Postgres Serverless Driver나 Cloudflare D1과 같은 HTTP 기반의 서버리스 데이터베이스 솔루션은 Edge 런타임에서도 사용할 수 있습니다. 따라서 무거운 DB 트랜잭션이 필요한 복잡한 로직은 여전히 Node.js 런타임에서 처리해야 합니다.

  5. Incremental Static Regeneration (ISR) 미지원: Edge 런타임은 ISR을 지원하지 않습니다.
    ISR은 Node.js 서버에서 페이지를 백그라운드에서 주기적으로 재생성하는 기능입니다. ISR이 필요한 페이지는 runtime 설정을 nodejs로 명시해야 합니다.

  6. ES 모듈 사용 필수: Edge 런타임에서는 require()를 사용할 수 없으며, 모든 의존성은 ES 모듈(import/export) 문법을 사용하여 불러와야 합니다.

  7. 특정 JavaScript 기능 제한: eval(), new Function(), WebAssembly.compile(), WebAssembly.instantiate()와 같은 동적 코드 실행 기능은 보안상의 이유와 샌드박스 환경의 제약으로 인해 Edge 런타임에서 사용할 수 없습니다. 이 기능들은 잠재적으로 런타임 환경에 위협을 가하거나 예측 불가능한 동작을 유발할 수 있기 때문입니다.

언제 Edge Runtime을 선택할까?

가장 오해하기 쉬운 점은 Node.js RuntimeEdge Runtime이 베타적이라는 착각입니다.

즉, Edge Runtime을 선택했다고 해서 Next.js로 구축한 애플리케이션 전체가 Edge Runtime으로 변경되는 것은 아닙니다.

대부분의 Next.js 애플리케이션은 두 런타임을 혼합하여 사용하게 됩니다.

예를 들어, 인증 미들웨어와 SEO 관련 로직은 Edge 런타임에서, 사용자 데이터베이스에 접근하는 복잡한 API는 Node.js 런타임에서 처리하는 식으로 구성하는 것이 가장 효율적인 아키텍처입니다.

이를 좀 더 목록화해서 정리해보도록 하겠습니다.

사실 실무에서는 워낙 복잡하게 엮힌 상황이 많이 발생하므로 아래에 대한 기준이 절대적이지 않아 참고 정도만 하면 될 것 같습니다.

Node.js 런타임Edge 런타임
복잡한 서버 연산 (이미지 처리, 데이터 분석 등)빠른 응답 속도가 중요한 미들웨어 및 API
안정적인 데이터베이스 트랜잭션이 필요한 경우사용자 인증 및 리디렉션과 같은 가벼운 로직
파일 시스템 접근이나 네이티브 Node.js 모듈을 사용해야 할 때React Streaming 및 Suspense를 활용하여 사용자 경험을 극대화하고자 할 때
ISR (Incremental Static Regeneration) 기능이 필요한 페이지전 세계 사용자를 대상으로 지연 시간을 최소화해야 할 때
웹 소켓이나 TCP 기반의 통신이 필요한 경우HTTP 기반의 API 호출이나 가벼운 데이터 페칭이 필요한 경우

Edge Runtime 사용하기

설정 방법

Next.js에서 Edge 런타임을 사용하려면, 파일 상단에 export const runtime = 'edge';를 추가하면 됩니다.

// app/api/hello/route.ts
export const runtime = 'edge'; 

export async function GET() {
  const data = { message: 'Hello from the Edge!' };
  return new Response(JSON.stringify(data), {
    headers: { 'content-type': 'application/json' },
  });
}

이렇게만 하더라도 Edge Runtime의 가벼운 특성을 활용하여 더 빠른 동작을 가능하게 합니다.

Suspense와 Streaming에 사용하여 렌더링을 세밀하게 조정하기

Suspense

Edge 런타임의 가장 큰 매력은 React SuspenseStreaming을 효과적으로 지원한다는 점입니다.
App Router는 기본적으로 서버 컴포넌트를 사용하며, SuspenseStreaming을 활용하여 초기 로딩 시간을 줄이고 사용자 경험을 향상시킵니다.

Suspense와 Streaming은 '데이터를 기다리는 동안 UI를 분리해서 렌더링'하는 개념입니다.

서버에서 HTML을 생성할 때, 무거운 데이터 페칭이 필요한 컴포넌트는 잠시 렌더링을 중단하고 로딩 상태(fallback)를 먼저 보냅니다. 이후 데이터가 준비되면 해당 컴포넌트의 HTML을 완성하여 스트리밍 방식으로 클라이언트에 전송합니다.

Edge 런타임은 바로 이 스트리밍 프로세스를 최적화합니다.

Node.js 런타임에 비해 가볍고 시작 시간이 빠르므로, 서버에서 첫 번째 HTML 응답을 클라이언트에 보내기까지 걸리는 시간을 최소화합니다. 이는 Suspense의 fallback UI가 사용자에게 더 빠르게 보이도록 하여, 실제 콘텐츠를 기다리는 동안에도 페이지가 정지된 것처럼 느껴지지 않게 해줍니다.

'use client'
import { isServer, useSuspenseQuery } from '@tanstack/react-query'
import { Suspense } from 'react'

export const runtime = 'edge' // 'nodejs' (default) | 'edge'

function getBaseURL() {
  if (!isServer) {
    return ''
  }
  if (process.env.VERCEL_URL) {
    return `https://${process.env.VERCEL_URL}`
  }
  return 'http://localhost:3000'
}
const baseUrl = getBaseURL()
function useWaitQuery(props: { wait: number }) {
  const query = useSuspenseQuery({
    queryKey: ['wait', props.wait],
    queryFn: async () => {
      const path = `/api/wait?wait=${props.wait}`
      const url = baseUrl + path

      const res: string = await (
        await fetch(url, {
          cache: 'no-store',
        })
      ).json()
      return res
    },
  })

  return [query.data as string, query] as const
}

function MyComponent(props: { wait: number }) {
  const [data] = useWaitQuery(props)

  return <div>result: {data}</div>
}

export default function MyPage() {
  return (
    <>
      <Suspense fallback={<div>waiting 100....</div>}>
        <MyComponent wait={100} />
      </Suspense>
      <Suspense fallback={<div>waiting 200....</div>}>
        <MyComponent wait={200} />
      </Suspense>
      <Suspense fallback={<div>waiting 300....</div>}>
        <MyComponent wait={300} />
      </Suspense>
      <Suspense fallback={<div>waiting 400....</div>}>
        <MyComponent wait={400} />
      </Suspense>
      <Suspense fallback={<div>waiting 500....</div>}>
        <MyComponent wait={500} />
      </Suspense>
      <Suspense fallback={<div>waiting 600....</div>}>
        <MyComponent wait={600} />
      </Suspense>
      <Suspense fallback={<div>waiting 700....</div>}>
        <MyComponent wait={700} />
      </Suspense>

      <fieldset>
        <legend>
          combined <code>Suspense</code>-container
        </legend>
        <Suspense
          fallback={
            <>
              <div>waiting 800....</div>
              <div>waiting 900....</div>
              <div>waiting 1000....</div>
            </>
          }
        >
          <MyComponent wait={800} />
          <MyComponent wait={900} />
          <MyComponent wait={1000} />
        </Suspense>
      </fieldset>
    </>
  )
}

해당 예제를 살펴보면 다음과 같습니다.

Edge Runtime을 활용하여 첫 HTML 응답을 빠르게 보내고, 이후 개별 Suspense 경계(boundary) 내의 데이터가 준비되는 대로 클라이언트에 스트리밍하는 과정을 최적화합니다.

이는 페이지 전체가 하얗게 보이다가 한 번에 나타나는 "폭포수 현상(waterfall effect)" 을 방지하고, 사용자에게 로딩 상태를 즉각적으로 보여줌으로써 체감 성능을 크게 향상시킵니다.

특히, <combined /> 필드셋 예제는 여러 컴포넌트를 하나의 <Suspense>로 묶었을 때의 동작을 보여줍니다. 이 경우, <MyComponent wait={800} />, <MyComponent wait={900} />, <MyComponent wait={1000} /> 세 컴포넌트 중 가장 오래 걸리는 데이터(1000ms)가 모두 로딩되어야만 fallback이 사라지고 세 컴포넌트가 한꺼번에 렌더링됩니다.

물론 Edge Runtime을 활성화해야지만 동작하는 코드는 아니지만 Edge RuntimeSuspenseStreaming을 잘 지원한다는 것을 확인해 볼 수 있습니다.

더 다양한 활용법 살펴보기

인증 및 미들웨어

middleware.ts 파일은 Next.js의 특별한 예약어로, 기본적으로 Edge 런타임에서 실행됩니다.

다음은 예시입니다.

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export const config = {
  matcher: ['/dashboard/:path*', '/profile'],
}

export function middleware(request: NextRequest) {
  const token = request.cookies.get('auth_token')
  const isLoggedIn = !!token

  if (isLoggedIn === false) {
    const loginUrl = new URL('/login', request.url)

    return NextResponse.redirect(loginUrl)
  }
  return NextResponse.next()
}

개인화된 콘텐츠

사용자의 위치나 선호 언어 같은 정보를 기반으로 동적으로 콘텐츠를 변경할 수 있습니다.

// app/api/hello/route.ts
import { NextResponse } from 'next/server'
import { get } from '@vercel/edge' // Vercel Edge API

export const runtime = 'edge'

export async function GET(request: Request) {
  const city = request.headers.get('x-vercel-ip-city')
  
  if (city) {
    return NextResponse.json({
      message: `당신의 도시는? ${city}`,
    })
  }

  return NextResponse.json({
    message: 'None',
  })
}

SEO 및 데이터 프리페칭 (SEO and Data Prefetching)

SEO에 중요한 메타 데이터나 페이지에 필요한 초기 데이터를 엣지에서 빠르게 가져와 페이지 로딩을 최적화할 수 있습니다.

특히 서버 컴포넌트 환경에서는 데이터 프리페칭이 기본적으로 강력하게 지원됩니다.

// app/blog/[slug]/page.tsx
import { Suspense } from 'react'
import { getPostBySlug } from '@/lib/api'

export const runtime = 'edge'

export async function generateMetadata({ params }) {
  const post = await getPostBySlug(params.slug);

  if (!post) {
    return {
      title: 'Post not found',
    }
  }

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      images: [post.coverImage],
    },
  }
}

async function PostContent({ slug }) {
  const post = await getPostBySlug(slug);
  
  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  )
}

export default function BlogPage({ params }) {
  return (
    <>
      <Suspense fallback={<div>Loading post...</div>}>
        <PostContent slug={params.slug} />
      </Suspense>
    </>
  )
}

마무리

지금까지 Next.js의 두 가지 핵심 런타임, 즉 Node.js RuntimeEdge Runtime에 대해 깊이 있게 살펴보았습니다.

Node.js Runtime이 무겁고 복잡한 서버 로직을 처리하는 데 최적화된 견고한 백엔드라면, Edge Runtime은 사용자와 가장 가까운 곳에서 빠르고 가벼운 처리를 담당하는 민첩한 도우미입니다.

가장 중요한 점은 이 두 런타임이 서로 배타적인 관계가 아니라는 것입니다. 현대의 웹 애플리케이션은 단일 기술 스택에 의존하기보다, 각기 다른 요구사항에 맞춰 최적의 기술을 조합하는 하이브리드 아키텍처를 추구합니다.

끝..!

참고

Runtimes
React Example
Edge Runtime API Reference

profile
인치

0개의 댓글