큐 드라이버를 memory에서 Upstash로 전환하기: 준비와 점검

앞선 글에서 백그라운드 작업을 분리하는 코드를 만들었습니다.
드라이버 경계를 세워 두고, 로컬 QStash 개발 서버로 동작까지 확인했습니다.

이제 남은 것은 실제 Upstash 계정을 붙이는 일입니다.
이 글은 그 전환 직전에 무엇을 확인하고, 어떤 값이 왜 필요하며,
전환이 됐는지 어떻게 판별하는가를 정리한 것입니다.

지금 어떤 상태인지부터 확인합니다

전환 작업에서 제일 먼저 할 일은 "지금 무엇이 돌고 있는가"를 사실로 확인하는 것입니다.
환경변수를 직접 볼 수 없는 경우가 많으니, 동작으로 판별하는 방법을 만들어 두면 편합니다.

제 경우 워커 엔드포인트의 응답 코드가 그 역할을 합니다.

curl -s -o /dev/null -w '%{http_code}\n' \
-X POST https://example.com/api/jobs -d '{}'

응답 | 의미
404 | QUEUE_DRIVER=memory. 워커가 꺼져 있음
503 | upstash로 지정됐지만 서명 키가 없음
401 | upstash 전환 완료. 서명 검증이 동작 중

라우트를 이렇게 짜 뒀기 때문입니다.

if (env.queueDriver !== "upstash") {
return fail("워커가 활성화되지 않았습니다.", 404);
}
if (!env.qstashCurrentSigningKey) {
return fail("워커가 설정되지 않았습니다.", 503);
}
// 이후 서명 검증 실패 시 401

세 상태를 다른 코드로 갈라 둔 것이 여기서 값을 합니다.
"안 된다"가 아니라 어느 단계에서 멈췄는지를 알려줍니다.

확인해 보니 제 배포는 404, 즉 아직 memory 드라이버였습니다. 예상대로입니다.

같은 방식으로 관리자 토큰 설정 여부도 확인할 수 있습니다.
쓰기 API가 503이면 토큰 미설정, 401이면 설정된 상태입니다.
값을 만들어내지 않는 요청이라 운영 환경에 쏴도 부작용이 없습니다.

전환하면 무엇이 달라지나

코드는 이미 양쪽을 다 지원합니다. 환경변수만 바뀝니다.

| memory | upstash
처리 시점 | enqueue() 가 끝나면 이미 처리됨 | 몇 초 뒤 워커가 처리
실패 시 | 로그만 남고 사라짐 | QStash가 재시도
요청 제한 | 인스턴스별 카운터 | 전역 공유 카운터
POST /api/jobs | 404 | 401 (서명 필요)

여기서 처리 시점의 변화가 가장 중요합니다.
memory에서는 enqueue() 직후에 결과를 읽어도 값이 들어 있습니다.
upstash로 바꾸면 그 코드가 깨집니다.

그래서 저는 처음부터 이 규칙을 문서에 박아 뒀습니다.

enqueue() 직후 결과를 읽지 않는다. 확인이 필요하면 상태 필드를 폴링한다.

전환은 환경변수를 바꾸는 일이지만, 이 규칙을 어긴 코드가 있으면 전환이 곧 장애가 됩니다.
전환 전에 한 번 훑어보시길 권합니다.

필요한 값과 각각의 역할

콘솔에서 받아야 하는 값이 여럿입니다. 그냥 복사해 붙이기보다
각각이 무엇을 하는지 알고 넣는 편이 나중에 디버깅할 때 낫습니다.

큐 (QStash)

변수 | 역할
QSTASH_TOKEN | 우리가 QStash에 작업을 보낼 때 쓰는 인증 정보
QSTASH_CURRENT_SIGNING_KEY | QStash가 우리를 호출할 때 붙이는 서명의 검증 키
QSTASH_NEXT_SIGNING_KEY | 키 교체(rotation) 대비. 현재 키로 실패하면 이걸로 재시도

토큰과 서명 키의 방향이 반대라는 점이 핵심입니다.
토큰은 나가는 요청용, 서명 키는 들어오는 요청용입니다.
이걸 헷갈리면 "토큰은 맞는데 왜 401이 나지" 같은 상황에 빠집니다.

서명 키가 두 개인 이유는 무중단 키 교체 때문입니다.
키를 바꾸는 순간 이미 큐에 들어가 있던 메시지는 옛 키로 서명돼 있는데,
두 개를 모두 갖고 있으면 그 메시지도 정상 처리됩니다.

요청 제한 (Redis)

변수 | 역할
UPSTASH_REDIS_REST_URL | REST 엔드포인트
UPSTASH_REDIS_REST_TOKEN | 인증 토큰

Upstash Redis는 TCP가 아니라 HTTP(REST)로 접근합니다.
서버리스 환경에서 TCP 커넥션을 유지할 수 없기 때문에 나온 설계입니다.
그래서 방화벽이나 커넥션 풀을 신경 쓸 일이 없습니다.

큐만 먼저 켜보고 싶으시면 Redis는 나중에 추가해도 됩니다.
드라이버가 따로 갈리기 때문입니다(QUEUE_DRIVER, RATELIMIT_DRIVER).

그리고 잊기 쉬운 하나

APP_URL=https://example.com

QStash는 우리 앱을 바깥에서 호출합니다. 공개 주소를 모르면 배달할 곳이 없습니다.
enqueue() 할 때 "이 URL로 보내달라"고 알려줘야 하는데, 그 URL이 이 값입니다.

Vercel은 VERCEL_URL 을 자동으로 넣어주지만 배포마다 주소가 바뀝니다.
커스텀 도메인을 쓰신다면 APP_URL 을 명시하는 편이 안전합니다.

콘솔에서 받는 값

https://console.upstash.com 에서 QStash와 Redis를 각각 준비합니다.

공식 문서 기준으로 QStash 토큰은 QStash 탭에서 리전을 고른 뒤 Quickstart 섹션에서
복사합니다. 서명 키도 같은 화면에서 확인할 수 있습니다.
Redis는 데이터베이스를 하나 만들면 REST URL과 토큰이 나옵니다.

콘솔 UI는 종종 바뀌므로, 화면 위치는
공식 문서를 함께 보시는 편이 정확합니다.
이 글에서 값을 갖는 부분은 각 값이 무엇을 하는가이지 클릭 순서가 아닙니다.

Vercel에 넣고 재배포

환경변수를 추가합니다. Production·Preview·Development를 모두 체크합니다.

QUEUE_DRIVER=upstash
RATELIMIT_DRIVER=upstash
APP_URL=https://example.com
QSTASH_TOKEN=...
QSTASH_CURRENT_SIGNING_KEY=...
QSTASH_NEXT_SIGNING_KEY=...
UPSTASH_REDIS_REST_URL=...
UPSTASH_REDIS_REST_TOKEN=...

환경변수만 추가해서는 반영되지 않습니다. 재배포가 필요합니다.
빌드 시점에 결정되는 것은 아니지만, 실행 중인 인스턴스는 기존 환경으로 떠 있기 때문입니다.
Deployments에서 Redeploy 하거나 아무 커밋이나 push하면 됩니다.

이 단계를 잊으면 "환경변수를 넣었는데 그대로네" 하며 시간을 씁니다.

전환됐는지 확인

앞서 만든 판별법을 그대로 씁니다.

# 1. 워커가 켜졌는가
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://example.com/api/jobs -d '{}'
# 404 → 401 로 바뀌면 전환 완료

# 2. 전체 흐름
./scripts/smoke.sh https://example.com

스모크는 전환 전후 모두 통과합니다. 큐 처리 확인 단계를 폴링으로 만들어 뒀기 때문입니다.
차이를 눈으로 보고 싶으면 주문 직후 바로 조회해 보시면 됩니다.
memory에서는 processedAt 이 이미 채워져 있고, upstash에서는 잠깐 null 입니다.

그 null 이 곧 "요청 경로에서 후처리가 빠졌다"는 증거입니다.

전환 전에 로컬에서 리허설할 수 있습니다

계정을 만들기 전에 똑같은 흐름을 로컬에서 돌려볼 수 있습니다.
QStash는 개발 서버를 제공합니다.

npx @upstash/qstash-cli dev

실행하면 로컬 엔드포인트와 토큰, 서명 키를 출력해 줍니다.
그 값으로 앱을 띄우면 실제 QStash와 같은 방식으로 동작합니다.

저는 이 방식으로 전환 코드를 먼저 검증했습니다.
비동기 처리, 서명 검증 실패 시 401, 공유 카운터 동작까지 전부 로컬에서 확인했습니다.
계정을 붙이는 일이 "되는지 보는 단계"가 아니라 "값만 바꾸는 단계"가 됩니다.

온프레미스는 전환하지 않습니다

같은 코드가 온프레미스에서도 돌지만, 거기는 memory 를 유지합니다.

QStash는 우리 앱을 바깥에서 호출하는 구조라 사설망 주소로는 배달이 안 됩니다.
굳이 쓰려면 워커 엔드포인트를 공개 도메인으로 노출해야 하는데,
얻는 것에 비해 치르는 비용이 큽니다.

한 코드베이스가 환경에 따라 다른 드라이버로 도는 것, 이게 경계를 만든 목적입니다.

QUEUE_DRIVER=memory # 온프레미스
QUEUE_DRIVER=upstash # 클라우드

정리

전환 전에 현재 상태를 동작으로 확인합니다. 응답 코드를 상태별로 갈라 두면
"어느 단계에서 멈췄는지"가 바로 보입니다.

토큰과 서명 키는 방향이 반대입니다. 나가는 요청용과 들어오는 요청용.

APP_URL 을 잊지 않습니다. 큐가 우리를 호출하려면 공개 주소를 알아야 합니다.

환경변수 추가 후 재배포해야 반영됩니다.

전환의 실질은 처리 시점의 변화입니다. enqueue() 직후 결과를 읽는 코드가 있으면
전환이 곧 장애가 됩니다.

계정 없이 로컬에서 리허설할 수 있습니다. 그러면 전환은 값만 바꾸는 일이 됩니다.

관련 글

온프레미스로 돌던 서비스를 Vercel + Supabase 로 옮기고 워커를 분리하기까지,
같은 작업에서 나온 기록입니다.

Supabase에 붙었더니 ENOTFOUND: 직결, 풀러(Supavisor), 그리고 IPv6
직결 주소가 IPv6 전용이라 붙지 않는 문제와, 커넥션 풀러의 두 가지 모드.

Cloudflare Tunnel로 Vercel을 연결하면 안 되는 이유
DEPLOYMENT_NOT_FOUND 를 응답 헤더로 추적한 기록과 도메인 연결 방식 선택.

Vercel Hobby 플랜은 기본이 "AI 학습 옵트인"입니다
무료 플랜의 데이터 사용 조항과, 올리기 전에 꺼야 하는 이유.

서버리스 환경의 백그라운드 작업 분리: Upstash QStash + Redis 적용
큐 드라이버 경계 설계, 워커 엔드포인트 서명 검증, 멱등성.