한국어

Next.js 미들웨어를 사용한 고급 요청 수정 기술을 살펴보세요. 복잡한 라우팅, 인증, A/B 테스트, 현지화 전략을 처리하여 견고한 웹 애플리케이션을 구축하는 방법을 배우세요.

Next.js 미들웨어 엣지 케이스: 요청 수정 패턴 마스터하기

Next.js 미들웨어는 애플리케이션의 라우트에 도달하기 전에 요청을 가로채고 수정할 수 있는 강력한 메커니즘을 제공합니다. 이 기능은 간단한 인증 확인부터 복잡한 A/B 테스트 시나리오 및 국제화 전략에 이르기까지 광범위한 가능성을 열어줍니다. 그러나 미들웨어를 효과적으로 활용하려면 엣지 케이스와 잠재적인 함정에 대한 깊은 이해가 필요합니다. 이 종합 가이드에서는 고급 요청 수정 패턴을 탐색하고, 견고하고 성능이 뛰어난 Next.js 애플리케이션을 구축하는 데 도움이 되는 실용적인 예제와 실행 가능한 통찰력을 제공합니다.

Next.js 미들웨어의 기본 이해하기

고급 패턴을 살펴보기 전에 Next.js 미들웨어의 기본 사항을 다시 살펴보겠습니다. 미들웨어 함수는 요청이 완료되기 전에 실행되어 다음을 수행할 수 있습니다:

미들웨어 함수는 /pages 또는 /app 디렉토리(Next.js 버전 및 설정에 따라 다름)의 middleware.js 또는 middleware.ts 파일에 위치합니다. 들어오는 요청을 나타내는 NextRequest 객체를 받고 후속 동작을 제어하기 위해 NextResponse 객체를 반환할 수 있습니다.

예제: 기본 인증 미들웨어

이 예제는 간단한 인증 확인을 보여줍니다. 사용자가 인증되지 않은 경우(예: 쿠키에 유효한 토큰이 없는 경우) 로그인 페이지로 리디렉션됩니다.


import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const authToken = request.cookies.get('authToken')

  if (!authToken) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  return NextResponse.next()
}

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

이 미들웨어는 /protected/:path*와 일치하는 라우트에 대해서만 실행됩니다. authToken 쿠키의 존재 여부를 확인합니다. 쿠키가 없으면 사용자는 /login 페이지로 리디렉션됩니다. 그렇지 않으면 요청은 NextResponse.next()를 사용하여 정상적으로 진행되도록 허용됩니다.

고급 요청 수정 패턴

이제 Next.js 미들웨어의 진정한 힘을 보여주는 몇 가지 고급 요청 수정 패턴을 살펴보겠습니다.

1. 쿠키를 이용한 A/B 테스트

A/B 테스트는 사용자 경험을 최적화하기 위한 중요한 기술입니다. 미들웨어를 사용하여 사용자를 애플리케이션의 다른 변형에 무작위로 할당하고 그들의 행동을 추적할 수 있습니다. 이 패턴은 쿠키를 사용하여 사용자에게 할당된 변형을 유지합니다.

예제: 랜딩 페이지 A/B 테스트


import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const VARIANT_A = 'variantA'
const VARIANT_B = 'variantB'

export function middleware(request: NextRequest) {
  let variant = request.cookies.get('variant')?.value

  if (!variant) {
    // Randomly assign a variant
    variant = Math.random() < 0.5 ? VARIANT_A : VARIANT_B
    const response = NextResponse.next()
    response.cookies.set('variant', variant)
    return response
  }

  if (variant === VARIANT_A) {
    return NextResponse.rewrite(new URL('/variant-a', request.url))
  } else if (variant === VARIANT_B) {
    return NextResponse.rewrite(new URL('/variant-b', request.url))
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/'],
}

이 예제에서 사용자가 처음으로 루트 경로(/)를 방문하면 미들웨어는 무작위로 variantA 또는 variantB에 할당합니다. 이 변형은 쿠키에 저장됩니다. 동일한 사용자의 후속 요청은 할당된 변형에 따라 /variant-a 또는 /variant-b로 재작성됩니다. 이를 통해 다른 랜딩 페이지를 제공하고 어떤 것이 더 나은 성과를 내는지 추적할 수 있습니다. Next.js 애플리케이션에 /variant-a/variant-b에 대한 라우트가 정의되어 있는지 확인하세요.

전 세계적 고려사항: A/B 테스트를 수행할 때 지역적 차이를 고려하세요. 북미에서 공감을 얻는 디자인이 아시아에서는 효과적이지 않을 수 있습니다. IP 주소 조회나 사용자 선호도를 통해 얻은 지리적 위치 데이터를 사용하여 특정 지역에 맞게 A/B 테스트를 조정할 수 있습니다.

2. URL 재작성을 이용한 현지화(i18n)

국제화(i18n)는 전 세계 고객에게 도달하는 데 필수적입니다. 미들웨어를 사용하여 사용자의 선호 언어를 자동으로 감지하고 사이트의 적절한 현지화 버전으로 리디렉션할 수 있습니다.

예제: `Accept-Language` 헤더 기반 리디렉션


import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const SUPPORTED_LANGUAGES = ['en', 'fr', 'es', 'de']
const DEFAULT_LANGUAGE = 'en'

function getPreferredLanguage(request: NextRequest): string {
  const acceptLanguage = request.headers.get('accept-language')
  if (!acceptLanguage) {
    return DEFAULT_LANGUAGE
  }

  const languages = acceptLanguage.split(',').map((lang) => lang.split(';')[0].trim())

  for (const lang of languages) {
    if (SUPPORTED_LANGUAGES.includes(lang)) {
      return lang
    }
  }

  return DEFAULT_LANGUAGE
}

export function middleware(request: NextRequest) {
  const pathname = request.nextUrl.pathname

  // Check if there's an existing locale in the pathname
  if (
    SUPPORTED_LANGUAGES.some(
      (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
    )
  ) {
    return NextResponse.next()
  }

  const preferredLanguage = getPreferredLanguage(request)

  return NextResponse.redirect(
    new URL(`/${preferredLanguage}${pathname}`, request.url)
  )
}

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

이 미들웨어는 요청에서 Accept-Language 헤더를 추출하여 사용자의 선호 언어를 결정합니다. URL에 이미 언어 접두사(예: /en/about)가 포함되어 있지 않으면 미들웨어는 사용자를 적절한 현지화된 URL(예: 프랑스어의 경우 /fr/about)로 리디렉션합니다. `/pages` 또는 `/app` 디렉토리에 다른 로케일에 맞는 적절한 폴더 구조가 있는지 확인하세요. 예를 들어, `/pages/en/about.js`와 `/pages/fr/about.js` 파일이 필요합니다.

전 세계적 고려사항: i18n 구현이 오른쪽에서 왼쪽으로 쓰는 언어(예: 아랍어, 히브리어)를 올바르게 처리하는지 확인하세요. 또한 콘텐츠 전송 네트워크(CDN)를 사용하여 사용자에게 더 가까운 서버에서 현지화된 자산을 제공하여 성능을 향상시키는 것을 고려하세요.

3. 기능 플래그

기능 플래그를 사용하면 새 코드를 배포하지 않고도 애플리케이션의 기능을 활성화하거나 비활성화할 수 있습니다. 이는 새로운 기능을 점진적으로 출시하거나 프로덕션 환경에서 기능을 테스트하는 데 특히 유용합니다. 미들웨어를 사용하여 기능 플래그의 상태를 확인하고 그에 따라 요청을 수정할 수 있습니다.

예제: 베타 기능 활성화


import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const BETA_FEATURE_ENABLED = process.env.BETA_FEATURE_ENABLED === 'true'

export function middleware(request: NextRequest) {
  if (BETA_FEATURE_ENABLED && request.nextUrl.pathname.startsWith('/new-feature')) {
    return NextResponse.next()
  }

  // Optionally redirect to a "feature unavailable" page
  return NextResponse.rewrite(new URL('/feature-unavailable', request.url))
}

export const config = {
  matcher: ['/new-feature/:path*'],
}

이 미들웨어는 BETA_FEATURE_ENABLED 환경 변수의 값을 확인합니다. 이 값이 true로 설정되어 있고 사용자가 /new-feature 아래의 라우트에 액세스하려고 하면 요청이 진행되도록 허용됩니다. 그렇지 않으면 사용자는 /feature-unavailable 페이지로 리디렉션됩니다. 개발, 스테이징, 프로덕션 등 다양한 환경에 맞게 환경 변수를 적절하게 구성해야 합니다.

전 세계적 고려사항: 기능 플래그를 사용할 때 모든 지역의 규정을 준수하지 않을 수 있는 기능을 활성화하는 것의 법적 영향을 고려하세요. 예를 들어, 데이터 프라이버시와 관련된 기능은 특정 국가에서 비활성화해야 할 수 있습니다.

4. 장치 감지 및 적응형 라우팅

현대 웹 애플리케이션은 다양한 화면 크기와 장치 기능에 반응하고 적응해야 합니다. 미들웨어를 사용하여 사용자의 장치 유형을 감지하고 사이트의 최적화된 버전으로 리디렉션할 수 있습니다.

예제: 모바일 사용자를 모바일 최적화 하위 도메인으로 리디렉션


import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { device } from 'detection'

export function middleware(request: NextRequest) {
  const userAgent = request.headers.get('user-agent')

  if (userAgent) {
    const deviceType = device(userAgent)

    if (deviceType.type === 'phone') {
      const mobileUrl = new URL(request.url)
      mobileUrl.hostname = 'm.example.com'
      return NextResponse.redirect(mobileUrl)
    }
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/'],
}

이 예제는 `detection` 라이브러리를 사용하여 User-Agent 헤더를 기반으로 사용자의 장치 유형을 결정합니다. 사용자가 휴대폰을 사용하는 경우 m.example.com 하위 도메인으로 리디렉션됩니다(모바일 최적화 버전의 사이트가 해당 위치에 호스팅되어 있다고 가정). `detection` 패키지를 설치해야 합니다: `npm install detection`.

전 세계적 고려사항: 장치 감지 로직이 장치 사용의 지역적 차이를 고려하는지 확인하세요. 예를 들어, 피처폰은 일부 개발도상국에서 여전히 널리 사용됩니다. 더 강력한 솔루션을 위해 User-Agent 감지와 반응형 디자인 기술을 조합하여 사용하는 것을 고려하세요.

5. 요청 헤더 보강

미들웨어는 애플리케이션 라우트에서 처리되기 전에 요청 헤더에 정보를 추가할 수 있습니다. 이는 사용자 역할, 인증 상태 또는 요청 ID와 같은 사용자 정의 메타데이터를 추가하는 데 유용하며, 이는 애플리케이션 로직에서 사용될 수 있습니다.

예제: 요청 ID 추가


import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { v4 as uuidv4 } from 'uuid'

export function middleware(request: NextRequest) {
  const requestId = uuidv4()
  const response = NextResponse.next()
  response.headers.set('x-request-id', requestId)
  return response
}

export const config = {
  matcher: ['/api/:path*'], // Only apply to API routes
}

이 미들웨어는 uuid 라이브러리를 사용하여 고유한 요청 ID를 생성하고 x-request-id 헤더에 추가합니다. 이 ID는 로깅, 추적 및 디버깅 목적으로 사용될 수 있습니다. uuid 패키지를 설치해야 합니다: `npm install uuid`.

전 세계적 고려사항: 사용자 정의 헤더를 추가할 때 헤더 크기 제한에 유의하세요. 이 제한을 초과하면 예기치 않은 오류가 발생할 수 있습니다. 또한 헤더에 추가된 민감한 정보가 제대로 보호되는지 확인하세요. 특히 애플리케이션이 리버스 프록시나 CDN 뒤에 있는 경우 더욱 그렇습니다.

6. 보안 강화: 속도 제한

미들웨어는 속도 제한을 구현하여 악의적인 공격에 대한 첫 번째 방어선 역할을 할 수 있습니다. 이는 클라이언트가 특정 시간 내에 할 수 있는 요청 수를 제한하여 남용을 방지합니다.

예제: 간단한 저장소를 사용한 기본 속도 제한


import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const requestCounts: { [ip: string]: number } = {}
const WINDOW_SIZE_MS = 60000; // 1 minute
const MAX_REQUESTS_PER_WINDOW = 100;

export function middleware(request: NextRequest) {
  const clientIP = request.ip || '127.0.0.1' // Get client IP, default to localhost for local testing

  if (!requestCounts[clientIP]) {
    requestCounts[clientIP] = 0;
  }

  requestCounts[clientIP]++;

  if (requestCounts[clientIP] > MAX_REQUESTS_PER_WINDOW) {
    return new NextResponse(
      JSON.stringify({ message: 'Too many requests' }),
      { status: 429, headers: { 'Content-Type': 'application/json' } }
    );
  }

  // Reset count after window
  setTimeout(() => {
    requestCounts[clientIP]--;
    if (requestCounts[clientIP] <= 0) {
        delete requestCounts[clientIP];
    }
  }, WINDOW_SIZE_MS);

  return NextResponse.next();
}

export const config = {
  matcher: ['/api/:path*'], // Apply to all API routes
}

이 예제는 간단한 인메모리 저장소(requestCounts)를 유지하여 각 IP 주소의 요청 수를 추적합니다. 클라이언트가 WINDOW_SIZE_MS 내에서 MAX_REQUESTS_PER_WINDOW를 초과하면 미들웨어는 429 Too Many Requests 오류를 반환합니다. 중요: 이것은 단순화된 예제이며 확장되지 않고 서비스 거부 공격에 취약하기 때문에 프로덕션 환경에는 적합하지 않습니다. 프로덕션 용도로는 Redis나 전용 속도 제한 서비스와 같은 더 강력한 속도 제한 솔루션을 사용하는 것을 고려하세요.

전 세계적 고려사항: 속도 제한 전략은 애플리케이션의 특정 특성과 사용자의 지리적 분포에 맞게 조정되어야 합니다. 다른 지역이나 사용자 세그먼트에 대해 다른 속도 제한을 사용하는 것을 고려하세요.

엣지 케이스와 잠재적 함정

미들웨어는 강력한 도구이지만, 그 한계와 잠재적인 함정을 인지하는 것이 중요합니다:

Next.js 미들웨어 사용을 위한 모범 사례

Next.js 미들웨어의 이점을 극대화하고 잠재적인 문제를 피하려면 다음 모범 사례를 따르세요:

결론

Next.js 미들웨어는 엣지에서 요청을 수정하고 애플리케이션의 동작을 사용자 정의할 수 있는 강력한 방법을 제공합니다. 이 가이드에서 논의된 고급 요청 수정 패턴을 이해함으로써 견고하고 성능이 뛰어나며 전 세계적으로 인식되는 Next.js 애플리케이션을 구축할 수 있습니다. 엣지 케이스와 잠재적인 함정을 신중하게 고려하고, 위에 설명된 모범 사례를 따라 미들웨어 기능이 신뢰할 수 있고 유지 관리 가능하도록 하세요. 미들웨어의 힘을 받아들여 탁월한 사용자 경험을 만들고 웹 애플리케이션의 새로운 가능성을 열어보세요.