Neon 서버리스 PostgreSQL 패턴은(는) 무엇을 하나요?
빠른 가이드: 엣지/서버리스 데이터베이스 액세스에는
@neondatabase/serverless를 사용하세요. 단일 쿼리(더 빠르고, 상태 비저장)에는neon()HTTP 함수를 선호하고, 인터랙티브 트랜잭션에는Pool/Client를 사용하세요. 서버리스 워크로드에는 풀링된 연결 문자열(-pooler접미사)을 사용하고, 마이그레이션에만 직접 연결을 사용하세요. 카피-온-라이트 의미 체계를 사용하여 개발/프리뷰 환경을 위해 데이터베이스를 브랜칭하세요. 항상 스케일-투-제로(200-500ms 웨이크업)로 인한 콜드 스타트를 처리하세요.
<critical_requirements>
중요: 이 스킬 사용 전 필수 사항
모든 코드는 CLAUDE.md의 프로젝트 규칙을 따라야 합니다 (kebab-case, 명명된 내보내기, 가져오기 순서,
import type, 명명된 상수)
(엣지/서버리스 런타임에서 단일 쿼리에는 반드시 neon() HTTP 함수를 사용해야 합니다 -- 단발성 작업의 경우 WebSocket보다 2-3배 빠릅니다)
(서버리스 환경에서는 반드시 동일한 요청 핸들러 내에서 Pool/Client 연결을 종료해야 합니다 -- WebSocket 연결이 단일 요청보다 오래 지속될 수 없습니다)
(서버리스 워크로드에는 반드시 풀링된 연결 문자열(-pooler 접미사)을 사용해야 합니다 -- 직접 연결은 제한된 연결 슬롯을 고갈시킵니다)
(적절한 연결 타임아웃 및 재시도 로직으로 스케일-투-제로 웨이크업 지연 시간(200-500ms)을 반드시 처리해야 합니다)
(신뢰할 수 있고 안전하다고 알려진 문자열(예: 테이블/컬럼 이름)에만 sql.unsafe()를 사용하고, 사용자 입력에는 절대 사용하지 마십시오)
</critical_requirements>
자동 탐지: Neon, @neondatabase/serverless, neon(), neonConfig, neon 서버리스 드라이버, neon 데이터베이스, neon 브랜치, neonctl, neon 커넥션 풀링, neon 스케일-투-제로, neon 자동 확장, neon postgres, ep-*-pooler
사용 시점:
- 엣지/서버리스 함수에서 Postgres 쿼리 (엣지 런타임, 서버리스 플랫폼)
- 다양한 워크로드에 대한 연결 문자열(풀링 vs 직접) 설정
- 개발, 프리뷰 또는 CI 환경을 위한 데이터베이스 브랜치 생성
- 스케일-투-제로 동작 및 콜드 스타트 최적화 관리
- 서버리스 컨텍스트에서 트랜잭션 실행 (HTTP 배치 또는 WebSocket)
- Neon API 또는 neonctl CLI를 통한 프로그래매틱 브랜치 관리
다루는 주요 패턴:
- SQL 태그 템플릿과 구성 가능한 프래그먼트를 사용한
neon()HTTP 쿼리 - 적절한 생명 주기 관리를 포함한
Pool/ClientWebSocket 연결 - 풀링된 (
-pooler) 연결 문자열과 직접 연결 문자열, 각각의 사용 시기 - 데이터베이스 브랜칭 (개발 브랜치, PR 프리뷰 브랜치, 스키마 전용 브랜치)
- 스케일-투-제로 동작, 콜드 스타트 완화 및 자동 확장
- 비대화형 HTTP 트랜잭션을 위한
sql.transaction() - 프로그래매틱 브랜치 관리를 위한 Neon API 및 neonctl CLI
사용하지 말아야 할 때:
- 전통적인 장기 실행 서버 연결 (표준
pg드라이버와 TCP 사용) - 복잡한 ORM 특정 패턴 (해당 ORM의 자체 스킬 사용)
- 일반 PostgreSQL 쿼리 구문 (SQL/Postgres 스킬 사용)
상세 리소스:
- 결정 프레임워크 및 빠른 조회 테이블은 reference.md를 참조하세요.
드라이버 및 쿼리:
- examples/core.md -- 드라이버 설정, HTTP 쿼리, WebSocket 연결, 트랜잭션
브랜칭 및 운영:
- examples/branching.md -- 개발 브랜치, PR 프리뷰, neonctl CLI, Neon API, CI/CD 워크플로우
철학
Neon은 PostgreSQL의 스토리지와 컴퓨팅을 분리하여 기존 Postgres로는 불가능한 서버리스 기능(스케일-투-제로, 즉시 브랜칭, 자동 확장)을 가능하게 합니다. @neondatabase/serverless 드라이버는 TCP를 HTTP 및 WebSocket으로 대체하여 Postgres를 TCP를 지원하지 않는 엣지 런타임에서도 액세스할 수 있게 합니다.
핵심 원칙:
- 속도를 위한 HTTP, 세션을 위한 WebSocket --
neon()함수는 단일 쿼리에 HTTP fetch(약 3회 왕복)를 사용합니다.Pool/Client는 세션 또는 인터랙티브 트랜잭션이 필요할 때 WebSocket(약 8회 왕복)을 사용합니다. 작업에 맞는 적절한 전송을 선택하세요. - 기본적으로 풀링 -- 풀링된 연결은 PgBouncer(트랜잭션 모드)를 통해 라우팅되어 최대 10,000개의 동시 클라이언트를 처리합니다. 직접 연결은 컴퓨팅 크기(100-4,000)에 의해 제한되며 마이그레이션이나 세션 상태가 필요한 기능에만 사용해야 합니다.
- 브랜치는 저렴하다 -- 카피-온-라이트는 500GB 데이터베이스의 브랜치가 데이터가 분기될 때까지 추가 스토리지를 할당하지 않습니다. 개발, 프리뷰, 테스트 및 CI를 위해 자유롭게 브랜치를 사용하세요.
- 스케일-투-제로가 기본 -- 컴퓨팅은 5분간 활동이 없으면 일시 중단됩니다. 콜드 스타트는 200-500ms가 소요됩니다. 타임아웃, 재시도 및 커넥션 풀링을 통해 이에 맞게 설계하세요.
- SQL 인젝션 방지 기능 내장 -- 태그 템플릿 함수는 자동으로 매개변수를 처리합니다. v1.0부터 일반 함수로
neon()을 호출하면 타입 오류가 발생하여 우발적인 인젝션을 방지합니다.
Neon 서버리스 드라이버를 사용해야 하는 경우:
- TCP 연결을 열 수 없는 엣지/서버리스 함수
- 데이터베이스 브랜칭(PR당 프리뷰 환경)의 이점을 활용하는 애플리케이션
- 스케일-투-제로의 이점을 활용하는 비용에 민감한 워크로드
- 커넥션 풀링이 필요한 높은 동시성의 서버리스 앱
사용하지 말아야 하는 경우:
- 영구 연결이 있는 장기 실행 서버 프로세스 (TCP를 통한 표준
pg사용) - PgBouncer를 통해 세션 수준 기능(LISTEN/NOTIFY, SET, 임시 테이블)이 필요한 워크로드
- 항상 켜져 있는 컴퓨팅이 필요한 16 CU보다 큰 데이터베이스 (16 CU 이상에서는 스케일-투-제로를 사용할 수 없음)
핵심 패턴
패턴 1: neon()을 사용한 HTTP 쿼리
neon() 함수는 SQL 태그 템플릿을 사용하여 HTTP 기반 쿼리 함수를 생성합니다. 단일 비대화형 쿼리에 가장 빠른 경로입니다.
import { neon } from "@neondatabase/serverless";
const DATABASE_URL = process.env.DATABASE_URL!;
const sql = neon(DATABASE_URL);
// 태그 템플릿 -- 매개변수가 자동으로 파라미터화됨 (인젝션으로부터 안전)
const userId = "abc-123";
const posts =
await sql`SELECT id, title FROM posts WHERE author_id = ${userId}`;
왜 좋은가: 태그 템플릿은 값을 자동으로 파라미터화하여 SQL 인젝션을 방지하고, HTTP는 WebSocket의 ~8회 왕복에 비해 ~3회 왕복이며, 상태 비저장(관리할 연결 없음)입니다.
// 나쁨: 일반 함수로 neon() 호출 (v1.0+ 타입 오류)
const sql = neon(DATABASE_URL);
const result = await sql(`SELECT * FROM posts WHERE id = ${id}`); // 타입 오류 + SQL 인젝션 위험
왜 나쁜가: v1.0부터 일반 함수(태그 템플릿 아님)로 쿼리 함수를 호출하는 것은 런타임 및 타입 오류입니다. 이는 문자열 보간으로 인한 SQL 인젝션을 방지하기 위해 변경되었습니다.
동적 SQL을 위한 sql.query()
쿼리 문자열이 변수에 있는 경우(템플릿 리터럴이 아님), 번호가 매겨진 플레이스홀더와 함께 .query() 메서드를 사용하세요:
const q = "SELECT * FROM posts WHERE id = $1 AND status = $2";
const posts = await sql.query(q, [postId, "published"]);
사용 시점: 런타임에 빌드된 동적 SQL 문자열 또는 변수에 저장된 쿼리. $1, $2 등을 통해 매개변수가 여전히 안전하게 파라미터화됩니다.
패턴 2: 구성 가능한 쿼리 프래그먼트
템플릿 쿼리는 프래그먼트 간 자동 매개변수 번호 재지정을 통한 구성을 지원합니다.
import { neon } from "@neondatabase/serverless";
const sql = neon(DATABASE_URL);
// 재사용 가능한 프래그먼트로 쿼리 빌드
const whereClause = sql`WHERE status = ${"active"} AND role = ${"admin"}`;
const orderClause = sql`ORDER BY created_at DESC`;
const PAGE_SIZE = 20;
const limitClause = sql`LIMIT ${PAGE_SIZE}`;
const users =
await sql`SELECT id, name, email FROM users ${whereClause} ${orderClause} ${limitClause}`;
왜 좋은가: 구성된 프래그먼트 간에 매개변수가 자동으로 번호가 다시 지정되며, 페이지 크기에 명명된 상수를 사용하고, 프래그먼트를 쿼리 간에 재사용할 수 있습니다.
sql.unsafe()를 사용한 동적 테이블/컬럼 이름
// 신뢰할 수 있고 안전하다고 알려진 값에만 사용 -- 사용자 입력 절대 금지
const TABLE_NAME = "posts";
const results =
await sql`SELECT * FROM ${sql.unsafe(TABLE_NAME)} WHERE id = ${postId}`;
사용 시점: SQL에서 파라미터화할 수 없는 신뢰할 수 있는 식별자(테이블 이름, 컬럼 이름)를 보간해야 하는 경우에만 사용하세요. 사용자가 제공한 값을 sql.unsafe()에 절대 전달하지 마십시오.
패턴 3: sql.transaction()을 사용한 HTTP 트랜잭션
WebSocket 연결 없이 HTTP를 통해 여러 쿼리를 원자적으로 실행합니다.
import { neon } from "@neondatabase/serverless";
const sql = neon(DATABASE_URL);
// 배열 형태 -- 모든 쿼리가 단일 HTTP 왕복으로 실행됨
const [posts, totalCount] = await sql.transaction([
sql`SELECT id, title FROM posts ORDER BY created_at DESC LIMIT 10`,
sql`SELECT count(*) FROM posts`,
]);
// 트랜잭션 옵션이 있는 함수 형태
const [, transferResult] = await sql.transaction(
(txn) => [
txn`UPDATE accounts SET balance = balance - ${amount} WHERE id = ${fromId}`,
txn`UPDATE accounts SET balance = balance + ${amount} WHERE id = ${toId}`,
],
{ isolationLevel: "Serializable" },
);
왜 좋은가: WebSocket 오버헤드 없이 원자적 실행, 배열 형태는 읽기 전용 배치에 간결함, 함수 형태는 트랜잭션 옵션을 지원함, 격리 수준은 일관성 보장을 제어함
// 나쁨: 관련 쿼리를 별도의 HTTP 요청으로 실행
const posts = await sql`SELECT * FROM posts WHERE author_id = ${authorId}`;
const author = await sql`SELECT * FROM users WHERE id = ${authorId}`;
// 두 번의 개별 HTTP 왕복, 원자적이지 않음, 경합 조건 가능성
왜 나쁜가: 별도의 HTTP 호출은 원자적이지 않으며, 쿼리 사이에 데이터가 변경될 수 있고, 네트워크 지연 시간이 두 배가 됩니다.
트랜잭션 옵션
isolationLevel:ReadUncommitted|ReadCommitted|RepeatableRead|SerializablereadOnly: boolean (기본값false)deferrable: boolean (기본값false,readOnly: true+Serializable에서만 효과적)
패턴 4: Pool/Client를 사용한 WebSocket 연결
인터랙티브 트랜잭션이나 node-postgres API 호환성을 위해 Pool 및 Client를 사용하세요. 서버리스 환경에서는 단일 요청 핸들러 내에서 연결을 생성하고 종료해야 합니다.
import { Pool } from "@neondatabase/serverless";
export async function handleRequest(request: Request): Promise<Response> {
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
try {
// ... 쿼리 ...
} finally {
await pool.end(); // 항상 동일한 요청 내에서 종료
}
}
핵심 규칙: 핸들러 내부에서 Pool을 생성하고, finally에서 pool.end()로 종료하세요. 전역 Pool은 서버리스 호출 간에 WebSocket 연결을 누출합니다.
요청 범위 풀, 인터랙티브 트랜잭션 및 Node.js WebSocket 구성에 대한 자세한 내용은 examples/core.md -- 패턴 5-7을 참조하세요.
패턴 5: 연결 문자열 설정
Neon은 풀링(PgBouncer를 통해) 및 직접의 두 가지 연결 문자열 형식을 제공합니다.
# 풀링된 연결 (참고: 엔드포인트 ID에 -pooler 접미사)
# 사용처: 서버리스 함수, 웹 앱, 높은 동시성 워크로드
DATABASE_URL=postgresql://user:[email protected]/dbname?sslmode=require
# 직접 연결 (-pooler 접미사 없음)
# 사용처: 마이그레이션, pg_dump, LISTEN/NOTIFY, 세션 수준 기능
DIRECT_DATABASE_URL=postgresql://user:[email protected]/dbname?sslmode=require
왜 좋은가: 풀링은 PgBouncer 트랜잭션 모드를 통해 최대 10,000개의 동시 클라이언트를 처리하고, 직접 연결은 관리 작업을 위한 완전한 세션 기능을 제공하며, 별도의 환경 변수로 구분을 명확히 합니다.
PgBouncer 트랜잭션 모드 제한 사항
풀링된 연결은 트랜잭션 모드로 PgBouncer를 실행하므로 각 트랜잭션 후에 연결이 풀로 반환됩니다. 이로 인해 다음이 금지됩니다:
SET/RESET문 (대신ALTER ROLE ... SET사용)LISTEN/NOTIFYWITH HOLD CURSOR- SQL 수준
PREPARE/DEALLOCATE(최대 1,000개의 프로토콜 수준 준비된 명령문은 지원됨) PRESERVE/DELETE ROWS가 있는 임시 테이블- 세션 수준 어드바이저리 잠금
패턴 6: 콜드 스타트 및 스케일-투-제로 처리
Neon 컴퓨팅은 기본적으로 5분간 활동이 없으면 자동 일시 중단됩니다. 콜드 스타트는 200-500ms가 소요됩니다. 적절한 타임아웃 및 재시도 로직으로 이에 맞게 설계하세요.
const CONNECTION_TIMEOUT_MS = 10_000;
const sql = neon(process.env.DATABASE_URL!, {
fetchOptions: { signal: AbortSignal.timeout(CONNECTION_TIMEOUT_MS) },
});
핵심 규칙: 콜드 스타트 지연 시간을 수용할 수 있도록 명시적인 타임아웃(10-15초)을 설정하세요. 안정성을 위해 지수 백오프 재시도를 사용하세요.
전체 타임아웃 구성 및 재시도 패턴은 examples/core.md -- 패턴 8을 참조하세요.
스케일-투-제로 정보
- 기본 자동 일시 중단: 5분 활동 없음
- 콜드 스타트 지연 시간: ~200-500ms (지역 및 컴퓨팅 크기에 따라 다름)
- 최대 16 CU의 컴퓨팅에서만 사용 가능 (더 큰 컴퓨팅은 항상 켜짐 상태 유지)
- 유료 요금제는 일시 중단 타임아웃을 조정(최대 7일)하거나 완전히 비활성화할 수 있음
- 활성 논리적 복제 구독자는 일시 중단을 방지함
패턴 7: 데이터베이스 브랜칭
Neon 브랜치는 카피-온-라이트 의미 체계를 사용합니다. 500GB 데이터베이스를 브랜칭하는 것은 즉각적이며 데이터가 분기될 때까지 추가 스토리지를 할당하지 않습니다.
# neonctl 설치
npm install -g neonctl
# 메인에서 개발 브랜치 생성
neonctl branches create --name dev-alice --project-id <project-id>
# 자동 만료가 있는 브랜치 생성 (CI/프리뷰용)
neonctl branches create --name preview/pr-42 --project-id <project-id> --expires-at "2025-04-01T00:00:00Z"
# 스키마 전용 브랜치 생성 (데이터 복사 없음 -- 민감한 환경용)
neonctl branches create --name ci-test --schema-only --project-id <project-id>
# 개발 브랜치를 현재 프로덕션과 일치하도록 재설정
neonctl branches reset dev-alice --parent --project-id <project-id>
# 브랜치 삭제
neonctl branches delete preview/pr-42 --project-id <project-id>
왜 좋은가: 명명된 브랜치는 Git 워크플로우에 매핑되고, TTL 만료는 CI 브랜치를 자동 정리하며, 스키마 전용 브랜칭은 민감한 데이터를 보호하고, 재설정은 재생성 없이 개발을 프로덕션과 동기화합니다.
브랜치 생성 옵션
| 옵션 | 설명 | 사용 사례 |
|---|---|---|
--name | 브랜치 이름 | Git 브랜치와 일치시키는 데 필수 |
--parent | 상위 브랜치 (기본값: 현재) | 특정 기준점에서 분기 |
--schema-only | 스키마만 복사, 데이터는 복사하지 않음 | 테스트 환경, 민감한 데이터 보호 |
--expires-at | 자동 삭제를 위한 ISO 타임스탬프 | CI/CD, 임시 프리뷰 |
자세한 내용은 examples/branching.md -- 패턴 1-4를 참조하세요.
패턴 8: Neon API 및 neonctl CLI
프로그래매틱 브랜치 관리를 위해 Neon API 및 neonctl CLI를 사용하세요.
# neonctl을 통한 API 인증
neonctl auth
# 현재 프로젝트 목록
neonctl projects list
# 환경 변수로 연결 문자열 가져오기
neonctl connection-string --project-id <project-id> --branch-id <branch-id> --pooled
# API를 통한 브랜치 생성 (예: CI/CD에서)
curl -X POST "https://console.neon.tech/api/v2/projects/<project-id>/branches" \
-H "Authorization: Bearer $NEON_API_KEY" \
-H "Content-Type: application/json" \
-d '{"branch": {"name": "ci-branch-$(date +%s)"}}'
왜 좋은가: CLI는 수동 개발 워크플로우에 적합하고, API는 CI/CD 파이프라인에 완전히 통합되며, 자격 증명은 안전하게 관리됩니다.
CI/CD 통합 및 환경 변수 관리를 포함한 자세한 내용은 examples/branching.md -- 패턴 5-8을 참조하세요.
패턴 9: 에러 처리 및 재시도
서버리스 환경, 특히 콜드 스타트 중에는 견고한 에러 처리가 중요합니다.
import { neon, NeonQueryFunction } from "@neondatabase/serverless";
async function queryWithRetry(
sql: NeonQueryFunction<false, false>,
query: any,
retries = 3
) {
for (let i = 0; i < retries; i++) {
try {
return await query;
} catch (error) {
if (i === retries - 1) throw error;
// 지수 백오프: 200ms, 400ms, 800ms...
await new Promise(resolve => setTimeout(resolve, 200 * Math.pow(2, i)));
}
}
}
const sql = neon(process.env.DATABASE_URL!, {
fetchOptions: { signal: AbortSignal.timeout(10_000) },
});
const posts = await queryWithRetry(sql, sql`SELECT * FROM posts`);
핵심 규칙: 모든 데이터베이스 쿼리에 타임아웃을 설정하고, 지수 백오프로 재시도하며, 모든 에러가 적절히 전파되고 기록되는지 확인
모범 사례 요약
- 올바른 전송 선택: 단일 쿼리에는
neon(), 인터랙티브 트랜잭션에는Pool/Client - 서버리스 함수에서 연결 종료: 풀 생성 및 종료는 항상 동일한 요청 핸들러 내에서 수행
- 풀링된 연결 문자열 선호: 서버리스 워크로드에는
-pooler접미사 사용 - 타임아웃 설정: 콜드 스타트를 수용하기 위해 명시적 타임아웃(10-15초) 설정
- 재시도 구현: 데이터베이스 연결 실패 처리에 재시도 로직 포함
- 브랜치 활용: 개발, 프리뷰 및 CI에 브랜치 사용, TTL을 사용하여 정리 자동화
- SQL 인젝션 방지: 파라미터화된 쿼리에 항상 태그 템플릿 사용,
.query()또는sql.unsafe()는 필요한 경우에만 사용 - 모니터링 이해: 콜드 스타트, 연결 풀링 통계 및 브랜치 사용을 추적하여 최적화
추가 리소스
빠른 참조
| 작업 | 메서드/구성 | 비고 |
|---|---|---|
| 단일 쿼리 | neon() 태그 템플릿 | HTTP, 빠름, 상태 비저장 |
| 동적 SQL | sql.query() | 번호가 매겨진 파라미터, 안전 |
| 읽기 전용 배치 | sql.transaction() (배열) | 단일 HTTP 왕복 |
| 읽기-쓰기 트랜잭션 | sql.transaction() (함수) | 격리 수준 지원 |
| 인터랙티브 트랜잭션 | Pool/Client | WebSocket, 세션 필요 |
| 커넥션 풀링 | -pooler 접미사 | PgBouncer, 최대 10k 클라이언트 |
| 직접 연결 | 접미사 없음 | 마이그레이션, SET 등 |
| 브랜치 생성 | neonctl branches create | 카피-온-라이트 |
| 브랜치 재설정 | neonctl branches reset | 상위와 동기화 |
| API 브랜치 관리 | POST /branches | CI/CD 통합 |
| 타임아웃 설정 | fetchOptions.signal | 10-15초, 콜드 스타트 수용 |
| 재시도 로직 | 지수 백오프 | 안정성을 위해 필수 |