
프론트엔드뿐만 아니라 백엔드까지 포함하는 프리뷰 환경 (Preview Environments)
요약
프론트엔드 프리뷰 환경과 달리 백엔드는 공유 스테이징을 사용하여 발생하는 격리성 문제를 다룹니다. Neon의 브랜치 기능을 활용해 데이터와 컴퓨팅(Functions)까지 완전히 격리된 PR별 백엔드 환경을 구축하는 방법을 소개합니다.
핵심 포인트
- 기존 프리뷰 환경은 프론트엔드만 격리되고 백엔드는 공유되어 충돌 위험이 있음
- Neon 브랜치는 Copy-on-write 방식으로 데이터와 스키마를 완벽히 복제함
- Neon Functions를 통해 각 브랜치마다 고유한 백엔드 URL을 제공함
- CI와 연동하여 PR 생성 시 백엔드를 자동 생성하고 종료 시 삭제 가능
- Scale-to-zero 지원으로 격리된 프리뷰 환경 운영 비용을 최소화함
풀 리퀘스트 (Pull Request)를 열면 프론트엔드 호스트가 프리뷰 URL을 제공합니다. Vercel, Netlify, Cloudflare Pages 모두 이 기능을 지원합니다. 모든 PR은 병합하기 전에 클릭하여 확인할 수 있는 고유하고 격리된 빌드를 갖게 됩니다. 이는 지난 10년 동안 등장한 진정으로 훌륭한 DevOps 편의 기능 중 하나입니다.
그런 다음 그 프리뷰가 무엇과 통신하는지 살펴보게 됩니다. 그 뒤에 있는 API와 데이터베이스는 거의 항상 단일 공유 스테이징 환경입니다. 모든 열린 PR은 동일한 백엔드에 접속하고, 동일한 스키마(Schema)에 대해 마이그레이션 (Migration)을 실행하며, 동일한 행(Row)을 읽고 씁니다. 따라서 프리뷰는 절반의 프리뷰일 뿐입니다. 프론트엔드는 격리되어 있지만, 그것이 의존하는 대상은 무질서한 상태입니다.
Neon은 "브랜치 (Branch)"가 무엇을 포함하는지를 변화시킵니다. 브랜치는 단순히 스키마의 복사본이 아니라, 데이터의 복사본 (Copy-on-write copy)이기도 하며, Neon Functions를 사용하면 컴퓨팅 (Compute) 또한 해당 브랜치에 배포됩니다. 따라서 브랜치는 데이터베이스, 데이터, 그리고 백엔드가 함께 포크(Fork)된 것이며, 각각 고유한 URL을 가집니다. 이를 통해 실제 PR별 백엔드를 매 풀 리퀘스트마다 생성하고 폐기할 수 있을 만큼 저렴하게 만들 수 있습니다. 이 포스트에서는 워크플로우를 보여주고 라이브 함수 (Live function)를 통해 격리성을 증명한 다음, 이를 CI에 연결하는 방법을 스케치합니다.
요약 (TL;DR)
- 프론트엔드 프리뷰는 PR별로 격리됩니다. 하지만 호출하는 백엔드는 대개 그렇지 않으며, 따라서 프리뷰들은 하나의 스테이징 데이터베이스와 그 마이그레이션 및 데이터를 공유합니다.
- Neon 브랜치는 스키마와 데이터(Copy-on-write)를 복사하며, Neon Functions는 브랜치에 배포되므로, 각 브랜치는 고유한 함수 URL을 가진 완전하게 격리된 백엔드가 됩니다.
- 테스트 결과: 라이브 todos API를 브랜치로 분기했고, 브랜치는 메인(main)의 행 복사본과 함께 생성되었습니다. 브랜치에 쓰기 작업을 수행해도 메인은 영향을 받지 않았으며, 브랜치는 자체적인 URL을 가졌습니다.
- CI에서의 과정은 다음과 같습니다: PR 오픈 시 브랜치를 생성하고 함수를 배포합니다. 프론트엔드 프리뷰에 해당 브랜치의 URL을 전달합니다. PR 종료 시 브랜치를 삭제하면 모든 것이 함께 삭제됩니다.
- 브랜치는 Copy-on-write 방식이고 함수는 스케일 투 제로 (Scale to zero)를 지원하기 때문에, 프리뷰 백엔드는 유휴 상태일 때 비용이 거의 들지 않습니다.
사전 요구 사항 (Prerequisites)
- 플랫폼 프리뷰(Functions,
us-east-2)에 배포된 함수가 포함된 Neon 프로젝트 - Neon CLI (
npm i -g neon실행 후neon login수행) - Pull Request (PR) 이벤트 발생 시 CLI 명령어를 실행할 수 있는 CI 시스템 (예제에서는 GitHub Actions 사용)
공유 스테이징(Shared Staging)이 조용히 해를 끼치는 이유
공유 스테이징 백엔드는 문제가 발생하여 직접적인 타격을 입기 전까지는 놓치기 쉬운 방식으로 실패합니다:
- 마이그레이션(Migrations) 충돌. 두 개의 PR이 각각 컬럼을 추가하거나, 한쪽에서 테이블 이름을 변경했는데 다른 쪽에서는 여전히 기존 이름을 읽으려 할 때 발생합니다. 두 번째로 마이그레이션을 실행하는 쪽은 깨진 스테이징 환경을 마주하게 되며, 결과적으로 두 프리뷰 모두 잘못된 상태가 됩니다.
- PR 간 데이터 유출. 한 PR의 테스트 실행 중에 생성된 레코드가 다른 PR의 프리뷰에 표시됩니다. 버그가 누가 무엇을 실행했느냐에 따라 나타났다 사라지기를 반복하며, 아무도 이를 재현할 수 없게 됩니다.
- 프리뷰가 프로덕션(Production)과 다름. 실제 데이터에 영향을 주는 것을 피하기 위해, 스테이징은 종종 소량의 시드 픽스처(Seed fixtures) 세트만 실행합니다. 따라서 프리뷰는 실제 데이터의 형태나 규모를 결코 접할 수 없으며, "프리뷰에서 작동함"이 곧 "프로덕션에서 작동함"을 의미하지 않게 됩니다.
- 초기화(Resetting)에 대한 두려움. 모두가 공유하는 환경이기 때문에, 아무도 스테이징을 초기화하는 사람이 되고 싶어 하지 않습니다. 그 결과 잘못된 데이터가 몇 달 동안 쌓이게 됩니다.
이 중 어느 것도 프론트엔드 측의 도구 결함이 아닙니다. 문제는 백엔드가 프리뷰의 일부로 실제로 포함된 적이 없었다는 점입니다.
Neon 브랜치(Branch)가 제공하는 것
Neon 브랜치는 특정 시점의 데이터베이스를 Copy-on-write 방식으로 포크(Fork)한 것입니다. 물리적으로 바이트를 복사하지 않고도 부모의 스키마(Schema)와 데이터를 즉시 가져오며, 데이터를 쓸 때만 분기(Diverge)됩니다. Neon Functions는 이를 확장합니다. 함수를 배포하면 해당 함수가 브랜치에 적용되며, 모든 브랜치는 https://<branch>-<function>.compute.<region>.aws.neon.tech 형태의 고유한 함수 URL을 갖게 됩니다.
이들을 결합하면 브랜치는 독립적인 백엔드가 됩니다. 자체적인 데이터베이스, 자체적인 데이터 복사본, 그리고 자체적인 API 엔드포인트를 가집니다. 브랜치에서 수행하는 그 어떤 작업도 부모에게 영향을 주지 않습니다.
격리(Isolation) 증명
저는 이미 main 브랜치에 몇 개의 행(rows)과 함께 배포된 작은 todos API (Neon Function 상의 Hono + Drizzle)를 가지고 있습니다. 다음은 실제 출력 결과와 함께 이를 대상으로 한 전체 프리뷰-백엔드(preview-backend) 라이프사이클입니다.
이것이 하나의 시퀀스로 보여주는 핵심입니다. 브랜치가 생성되면서 자체적인 함수 URL(function URL)과 main의 4개 행을 복사한 데이터를 가졌고, 쓰기(write) 작업은 브랜치에만 반영되었으며, main은 4개 상태를 유지했습니다. 그리고 브랜치를 삭제하자 데이터베이스, 데이터, 엔드포인트(endpoint)가 한 번에 정리되었습니다. 여기에 표시된 모든 숫자는 실제 실행 결과입니다.
CI에 연결하기 (Wire it into CI)
수동 명령은 풀 리퀘스트(pull-request) 자동화로 직접 매핑됩니다. PR이 열리거나 업데이트될 때, PR 이름으로 된 브랜치를 생성하고 함수를 배포합니다. 브랜치의 함수 URL을 프론트엔드 프리뷰의 API 베이스(API base)로 노출합니다. PR이 닫히면 브랜치를 삭제합니다.
# .github/workflows/preview-backend.yml
name: preview-backend
on:
...
이제 프론트엔드 프리뷰와 백엔드 프리뷰는 함께 생성되고 함께 소멸합니다. 리뷰어들은 해당 PR의 실제 코드가 실제 프로덕션 데이터의 복사본으로부터 시딩(seeded)된 해당 PR만의 데이터베이스를 대상으로 실행되는 프리뷰를 클릭하게 되며, 이 중 그 어떤 것도 다른 사람에게 영향을 줄 수 없습니다.
공유 스테이징(Shared staging) vs PR당 브랜치
| 공유 스테이징 백엔드 (Shared staging backend) | PR당 브랜치 (Branch per PR) | |
|---|---|---|
| 격리 (Isolation) | 모든 PR을 위한 하나의 데이터베이스 | PR당 고유한 데이터베이스 + 데이터 + URL |
| ... |
브랜치는 Copy-on-write 방식이기 때문에 디스크 상의 데이터를 복제하지 않고, 차이가 발생하는 부분만 저장합니다. 유휴 상태일 때 0으로 스케일링(scale to zero)되는 함수와 결합하면, 아무도 활발하게 클릭하지 않는 PR을 위한 프리뷰 백엔드는 비용이 거의 들지 않습니다. 이것이 'PR당 하나'의 방식을 예산 문제가 아닌 실용적인 선택으로 만드는 이유입니다.
리포지토리 (The repo)
여기서 사용된 todos API (Neon Function 상의 Hono + Drizzle)는 이 시리즈의 첫 번째 포스트에서 사용된 것과 동일합니다:
https://github.com/The-DevOps-Daily/neon-functions-demo
마무리하며 (Wrapping up)
프리뷰 환경 (Preview environments)은 모든 PR (Pull Request)이 깨끗하고, 클릭 가능하며, 격리된 빌드를 갖는 프론트엔드 분야에서 명성을 얻었습니다. 반면 백엔드는 공유된 스테이징 (Staging) 환경에 뒤처져 있었고, 바로 그 지점에서 혼란스러운 버그와 마이그레이션 (Migration) 교착 상태가 발생했습니다. Neon 브랜치는 스키마 (Schema), 데이터 (Data), 그리고 이제는 함수 (Function)까지 함께 가져가기 때문에, 각 PR에 고유한 실제 백엔드를 제공하고 머지 (Merge) 시점에 이를 삭제할 수 있습니다. 이제 프론트엔드 프리뷰도 마침내 자신만큼이나 일회적이고 격리된 무언가와 통신할 수 있게 되었습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기