Upstash Redis와 Next.js: 완전 가이드 (2026)

발행: (2026년 6월 10일 PM 06:21 GMT+9)
6 분 소요
원문: Dev.to

Source: Dev.to

Carlos Oliva Pascual

Redis는 빠릅니다. 하지만 서버리스 스택에 Redis를 직접 호스팅하는 것은 악몽과도 같습니다—콜드 스타트, 연결 풀 고갈, 그리고 서버리스 함수가 계속 두드리는 영구 서버 관리 등. Upstash는 이러한 문제를 HTTP 기반 Redis API로 해결합니다. 이 API는 0으로 스케일링되고, 요청당 요금이 부과되며, Next.js App Router와 네이티브하게 작동합니다.

이 가이드에서는 실제 운영 환경에서 중요한 패턴들을 다룹니다: 적절한 TTL을 가진 캐시 어사이드, SWR (stale‑while‑revalidate), 세션 스토리지, 그리고 pub/sub. 실제 코드와 실제 트레이드오프를 제공합니다.

전체 기사와 모든 코드 예시는 stacknotice.com에서 확인하세요.

왜 전통적인 Redis 인스턴스보다 Upstash를 선택해야 할까?

표준 Redis는 지속적인 TCP 연결을 사용합니다. 서버리스 함수는 지속적인 연결을 유지하지 않으며—각 호출마다 새로운 연결을 열 수 있습니다. 규모가 커지면 ECONNREFUSED 혹은 최대 연결 오류가 발생하고, 이는 디버깅이 번거롭고 비용이 많이 듭니다.

Upstash의 @upstash/redis 클라이언트는 HTTP/REST를 통해 통신합니다. 연결 풀도 없고, 연결 제한에 대한 고민도 없습니다. 각 요청은 무상태(stateless)이며, 이는 Next.js 서버 컴포넌트와 라우트 핸들러에 딱 맞습니다.

다른 장점들:

요청당 요금 — 한 번도 히트되지 않은 캐시는 비용이 $0

전역 복제 — Vercel 엣지 지역 어디서든 낮은 지연시간

네이티브 Edge Runtime 지원 — Next.js 미들웨어에서도 동작

무료 티어 — 하루 10,000명령, 신용카드 필요 없음

설정

npm install @upstash/redis
// lib/redis.ts
import { Redis } from '@upstash/redis'

export const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!,
})

패턴 1: Cache‑Aside

// lib/cache.ts
import { redis } from './redis'

export async function withCache(
  key: string,
  fetcher: () => Promise<any>,
  options: { ttl?: number; prefix?: string } = {}
): Promise<any> {
  const { ttl = 300, prefix = 'cache' } = options
  const cacheKey = `${prefix}:${key}`

  const cached = await redis.get(cacheKey)
  if (cached !== null) return cached

  const data = await fetcher()
  await redis.setex(cacheKey, ttl, JSON.stringify(data))
  return data
}

서버 컴포넌트에서 사용하기:

// app/products/page.tsx
export default async function ProductsPage() {
  const products = await withCache(
    'products:all',
    () => db.query.products.findMany({ where: eq(products.active, true) }),
    { ttl: 60 * 5 }
  )
  return <div>{/* render products */}</div>
}

패턴 2: 키 네임스페이싱

// lib/cache-keys.ts
export const CacheKeys = {
  userProfile: (userId: string) => `user:${userId}:profile`,
  products: () => 'products:all',
  productById: (id: string) => `product:${id}`,
  pricingPlans: () => 'pricing:plans',
}

// 결정적인 무효화
async function invalidateUserCache(userId: string) {
  await redis.del(CacheKeys.userProfile(userId))
}

패턴 3: Stale‑While‑Revalidate

SWR은 천둥벌레 문제를 해결합니다 — 오래된 데이터를 즉시 제공하고 백그라운드에서 새로 고칩니다:

export async function withSWRCache(
  key: string,
  fetcher: () => Promise<any>,
  options: { freshTtl: number; staleTtl: number }
): Promise<any> {
  const entry = await redis.get(key)

  if (entry !== null) {
    const age = (Date.now() - entry.cachedAt) / 1000
    if (age < options.freshTtl) {
      return entry.value // 아직 신선함
    }
    // 오래된 데이터 반환 + 백그라운드 재검증
    revalidate(key, fetcher, options.staleTtl)
    return entry.value
  }

  // 캐시가 없으면 직접 가져와서 저장
  const data = await fetcher()
  await redis.setex(key, options.freshTtl, JSON.stringify(data))
  return data
}

패턴 4: 세션 스토리지

import { randomBytes } from 'crypto'

const SESSION_TTL = 60 * 60 * 24 // 1일

export async function createSession(data: any): Promise<string> {
  const sessionId = randomBytes(32).toString('hex')
  const session: Session = { ...data, createdAt: Date.now() }
  await redis.setex(`session:${sessionId}`, SESSION_TTL, JSON.stringify(session))
  return sessionId
}

export async function getSession(sessionId: string): Promise<Session | null> {
  return redis.get(`session:${sessionId}`)
}

패턴 5: Rate Limiting

import { Ratelimit } from '@upstash/ratelimit'

export const apiRateLimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(10, '10 s'),
  analytics: true,
})

// 라우트 핸들러 안에서:
const { success, limit, remaining } = await apiRateLimit.limit(ip)
if (!success) {
  return Response.json({ error: 'Too many requests' }, { status: 429 })
}

패턴 6: Distributed Locks

export async function acquireLock(resource: string, ttl: number): Promise<string | null> {
  const lockId = crypto.randomUUID()
  const result = await redis.set(`lock:${resource}`, lockId, { nx: true, ex: ttl })
  return result === 'OK' ? lockId : null
}

무엇을 캐시하고 무엇을 캐시하면 안 될까

캐시할 대상: 데이터베이스 쿼리 결과, 외부 API 응답, 계산·집계된 데이터, 피처 플래그 상태

캐시하면 안 되는 대상: 인증 검사, 금융 거래, 실시간 재고, 사용자 ID에 스코프되지 않은 개인 데이터

Upstash 요금제

TierPriceCommands/day
Free$010,000
Pay-as-you-go$0.20 / 100KUnlimited
Pro$10 / monthUnlimited

전체 패턴, 코드 예시, 미들웨어 수준 캐싱이 포함된 완전 가이드는 stacknotice.com/blog/upstash-redis-nextjs-complete-guide-2026에서 확인하세요.

0 조회
Back to Blog

관련 글

더 보기 »

Eidentic 소개

Today we're releasing Eidentic, an open-source TypeScript SDK for building AI agents with self-improving memory and the production fundamentals built in — not b...

Typescript의 타입

Introdução Tipos são uma forma de definir a “forma” ou o contrato dos dados que estamos usando no código. Pensando em Javascript puro, ele é dinâmico: você pode...