- Published on
Next.js 14 Edge 런타임 crypto is not defined 해결법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버리스/엣지로 옮기면 갑자기 터지는 에러 중 하나가 ReferenceError: crypto is not defined입니다. Next.js 14에서 특히 흔한 이유는 Edge 런타임이 Node.js 런타임이 아니라 Web 표준 기반 런타임이기 때문입니다. 즉, Node의 require('crypto')나 global.crypto를 기대하는 코드/라이브러리가 섞이면, 로컬(Node)에서는 되다가 배포(Edge)에서만 깨집니다.
이 글에서는 Next.js 14의 Edge 런타임에서 crypto is not defined가 나는 대표 패턴을 분해하고, **가장 안전한 해결책(런타임 분리)**부터 Web Crypto로 치환, 의존성 교체/번들링 회피까지 단계적으로 정리합니다.
> 참고: 외부 API 호출을 Edge에서 처리하다 인증/헤더 문제까지 겹치면 원인 파악이 더 어려워집니다. 키는 맞는데 인증이 401로 떨어지는 케이스는 별도로 Responses API 401인데 키가 맞는 7가지 이유도 함께 점검해보세요.
Edge 런타임에서 왜 crypto가 없을까?
Next.js에는 크게 두 가지 서버 실행 환경이 있습니다.
- Node.js runtime: Node 내장 모듈(
crypto,fs,net등) 사용 가능 - Edge runtime: V8 isolate 기반(Web API 중심). Node 내장 모듈은 기본적으로 사용 불가
Edge 런타임에는 보통 Web Crypto API가 제공됩니다. 즉, crypto는 “Node의 crypto 모듈”이 아니라 “Web 표준의 globalThis.crypto”를 의미합니다.
문제는 다음 같은 코드/라이브러리가 섞일 때 발생합니다.
import crypto from 'crypto'또는const crypto = require('crypto')crypto.randomBytes(...)같은 Node 전용 API 사용- 어떤 라이브러리가 내부에서 Node
crypto를 require 하거나, 번들 과정에서 폴리필을 기대
증상 체크: 에러가 어디서 터지는지 먼저 분리하기
1) 에러가 Edge Route Handler에서만 발생하는가?
app/api/.../route.ts가 기본적으로 Edge로 동작하진 않지만, 다음 중 하나면 Edge에서 실행될 수 있습니다.
export const runtime = 'edge'를 명시- 미들웨어(
middleware.ts)에서 발생 - 특정 배포 환경(예: Vercel Edge Functions)에서 Edge로 올려버린 경우
2) 로컬에서는 되고 배포에서만 실패하는가?
로컬 next dev는 Node 실행에 가까워서 “우연히” 통과하는 경우가 많습니다. 배포에서만 깨지면 런타임 차이를 의심해야 합니다.
해결 1순위: Edge에서 Node crypto가 필요한 코드는 Node 런타임으로 분리
가장 확실한 해결은 해당 API 라우트를 Node 런타임으로 강제하는 것입니다.
Route Handler를 Node 런타임으로 고정
// app/api/sign/route.ts
export const runtime = 'nodejs';
import { NextResponse } from 'next/server';
import crypto from 'crypto';
export async function POST(req: Request) {
const body = await req.json();
const hmac = crypto
.createHmac('sha256', process.env.SIGNING_SECRET!)
.update(JSON.stringify(body))
.digest('hex');
return NextResponse.json({ hmac });
}
- 장점: 수정량 최소, Node 전용 라이브러리 그대로 사용 가능
- 단점: Edge의 초저지연 이점은 포기(대부분의 서명/토큰 생성은 Node로 돌려도 충분한 경우가 많음)
Middleware에서 터진다면: 미들웨어는 Edge 고정이라는 점을 기억
middleware.ts는 기본적으로 Edge에서 실행됩니다. 따라서 미들웨어에서는 Node crypto 의존 코드를 제거해야 합니다.
해결 2: Node crypto를 Web Crypto API로 치환 (Edge 친화)
Edge에서 crypto를 쓰려면 Node 모듈이 아니라 Web Crypto를 사용해야 합니다.
SHA-256 해시(Web Crypto) 예제
// lib/webcrypto.ts
export async function sha256Hex(input: string) {
const enc = new TextEncoder();
const data = enc.encode(input);
const digest = await crypto.subtle.digest('SHA-256', data);
return [...new Uint8Array(digest)].map(b => b.toString(16).padStart(2, '0')).join('');
}
Route Handler(Edge)에서 사용:
// app/api/hash/route.ts
export const runtime = 'edge';
import { NextResponse } from 'next/server';
import { sha256Hex } from '@/lib/webcrypto';
export async function POST(req: Request) {
const { text } = await req.json();
const hash = await sha256Hex(text);
return NextResponse.json({ hash });
}
HMAC 서명(Web Crypto) 예제
// lib/webcrypto-hmac.ts
export async function hmacSha256Hex(secret: string, message: string) {
const enc = new TextEncoder();
const key = await crypto.subtle.importKey(
'raw',
enc.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const sig = await crypto.subtle.sign('HMAC', key, enc.encode(message));
return [...new Uint8Array(sig)].map(b => b.toString(16).padStart(2, '0')).join('');
}
- 장점: Edge에서도 동작, 표준 API
- 단점: Node crypto와 API가 달라 마이그레이션 필요
해결 3: 문제를 만드는 의존성(라이브러리)을 Edge 호환으로 교체
실무에서 가장 흔한 케이스는 “내 코드”가 아니라 의존성 내부에서 Node crypto를 호출하는 경우입니다.
대표적으로 다음 부류가 위험합니다.
- 오래된 JWT 라이브러리(내부에서 Node crypto 강제)
- 특정 SDK가 Node 전용 모듈을 당연하게 사용
uuid구버전/특정 빌드가 Node crypto에 기대는 경우
예: JWT 처리 전략
- Edge에서 JWT 검증/서명까지 해야 한다면 Web Crypto 기반 라이브러리를 선택
- 또는 “JWT 처리 API만 Node runtime”으로 분리
이때 트래픽이 많고 재시도/폴백까지 고려해야 하는 외부 API 연동이라면, 장애 시 재시도 설계도 같이 가져가야 합니다. (예: 500/503 대응) 관련해서는 OpenAI Responses API 500·503 대응 재시도 폴백 서킷브레이커 같은 패턴이 Edge/Node 모두에 유효합니다.
해결 4: "crypto" 전역을 잘못 가정한 코드 정리 (globalThis.crypto 사용)
간혹 번들/트랜스파일 과정에서 crypto라는 식별자를 잘못 참조하는 코드가 섞입니다.
- 브라우저/Edge에서는
globalThis.crypto가 더 명시적 - Node에서는
globalThis.crypto가 버전에 따라 다르고, Node crypto 모듈과는 별개
따라서 Edge 타겟 코드에서는 다음처럼 명시하는 편이 안전합니다.
const webCrypto = globalThis.crypto;
if (!webCrypto?.subtle) {
throw new Error('Web Crypto API is not available in this runtime');
}
디버깅 체크리스트: 어디서 Node crypto가 끌려오는지 찾기
1) runtime 선언을 먼저 확인
export const runtime = 'edge' | 'nodejs'가 어디에 붙어 있는지- 특히
middleware.ts는 Edge 고정
2) 의존성 트레이싱
- 에러 스택 트레이스에서
node_modules/...경로 확인 crypto를 import/require 하는 파일을 grep
grep -R "from 'crypto'" -n .
grep -R "require('crypto')" -n .
3) Route 단위로 격리
- Edge로 돌릴 이유가 없는 API는 Node로 내리기
- Edge가 필요한 API만 Web Crypto로 치환
권장 아키텍처: Edge는 "라우팅/검증/캐싱", Node는 "서명/민감연산"
Next.js 14에서 Edge는 다음에 특히 강합니다.
- 지리적으로 가까운 곳에서 빠른 응답
- 간단한 토큰 검증, 헤더 검사, A/B 라우팅
- 캐시 가능한 GET 응답
반면 다음은 Node로 두는 편이 운영이 편합니다.
- Node 전용 SDK/암호화 모듈 의존
- 파일/스트림 처리
- 복잡한 암호 연산(라이브러리 의존이 큰 경우)
이렇게 역할을 분리하면 crypto is not defined 같은 런타임 충돌을 구조적으로 줄일 수 있습니다.
결론
Next.js 14 Edge 런타임의 crypto is not defined는 대부분 “Node crypto를 Edge에서 쓰려 했다”로 정리됩니다. 해결 우선순위는 다음이 실전에서 가장 빠르고 안전합니다.
- Node crypto가 꼭 필요하면 해당 Route를
runtime = 'nodejs'로 분리 - Edge에서 암호화가 필요하면 Web Crypto API(
crypto.subtle)로 치환 - Node 전용 의존성이 원인이면 Edge 호환 라이브러리로 교체하거나 Node로 내리기
globalThis.crypto등 전역 가정 코드를 정리해 런타임 차이를 명확히 하기
이 4단계를 적용하면, 로컬에서는 되는데 배포에서만 깨지는 Edge 특유의 크립토 이슈를 대부분 깔끔하게 정리할 수 있습니다.