클라우드 에뮬레이터 스택 — 4번째 환경의 설계
클라우드 에뮬레이터 스택 — 4번째 환경의 설계
대부분의 모노레포는 local / dev / prod 라는 익숙한 3단 구성. 이 셋의 공통점은 앱과 인프라가 한 묶음. local 에서 띄우는 Postgres · Redis 도, prod 의 그것도 "우리가 운영하는 인프라" 의 일부. 외부 클라우드 (Supabase · AWS) 에 의존하는 시점이 오면 이 3단 구성이 쪼개집니다 — 우리 인프라가 아닌 남의 인프라가 끼어들기 때문. 이 글은 그 상황에서 4번째 환경으로 cloud 를 도입한 설계.
1. 무엇이 문제인가
외부 클라우드를 직접 호출하는 코드가 늘어나면 다음 비용이 일상화:
- 인터넷 의존 — 비행기 · 지하철 · 해외 출장에서 손이 멈춤.
- 할당량 / 비용 — 무료 티어가 빠르게 마름. CI 가 매번 호출하면 비용 누적.
- 공유 자원의 오염 — 한 명이 만든 테스트 데이터가 다른 사람의 화면에 뜸.
- 시크릿 노출 — 로컬
.env에 실 운영 키가 박히면 깃 히스토리에 영원히 남을 위험.
해결은 두 갈래.
- 모킹 — SDK 호출을 jest mock 으로 가림. 코드 행위는 빠르게 검증되지만 실제 SDK · HTTP 흐름이 생략되어 통합 시점에 문제가 터짐.
- 로컬 에뮬레이터 — Supabase · LocalStack · MiniStack 같은 프로세스를 로컬에 띄우고 endpoint 만 바꿈. SDK · HTTP · 인증 흐름까지 그대로 검증. 비용은 컨테이너 자원.
cloud 환경은 두 번째 길의 보관 장소.
2. 별 환경으로 분리
기존 LOCAL / DEV / PROD 의 compose 와 같은 폴더에 두지 않음. infra/cloud/ 라는 네 번째 폴더를 신설:
- 외부 클라우드 에뮬레이터는 켜고 끄는 단위가 다름. local 인프라 (DB · Redis) 는 매일 켜지만, AWS 에뮬레이터는 SES 메일 검증할 때만 띄움.
- 포트 대역도 다름. LOCAL / DEV / PROD 는 표준 + 1만 + 2만 규칙을 따르지만, AWS 에뮬레이터는 4566 같은 도구별 디폴트 포트를 그대로 받는 게 호환성 측면에서 유리.
- 운영 배포 대상이 아님. PROD 에는 진짜 클라우드.
3. 각 에뮬레이터를 독립 compose 로
infra/cloud/
├── supabase/
│ └── docker-compose.yml # 14 컨테이너 (Auth · REST · Storage · Realtime · Studio · ...)
├── ministack/
│ └── docker-compose.yml # MiniStack + 동반 Redis
├── localstack/
│ └── docker-compose.yml # LocalStack 단독
├── firebase/
│ └── docker-compose.yml # Firebase Local Emulator (Auth · Firestore · RealtimeDB · Storage · Pub/Sub)
└── wiremock/
└── docker-compose.yml # 임의 외부 HTTP API stub
다섯 개를 한 compose 로 합치지 않는 이유는 선택적 기동. Supabase Auth 검증할 때는 supabase 만, AWS S3 멀티파트 PoC 할 때는 ministack 만 띄우면 메모리 · 시작 시간 절약. compose 의 profiles: 로도 가능하지만 디렉터리 분리가 README · 환경변수 · 트러블슈팅 동선에서 더 깔끔.
cd infra/cloud/supabase && docker compose up -d # 필요한 것만
cd infra/cloud/ministack && docker compose up -d
4. 포트는 도구의 디폴트를 존중
LocalStack · MiniStack 둘 다 4566 이 디폴트. boto3 · Terraform · @aws-sdk 모두 이 포트를 가정해 만든 예제 · 튜토리얼이 인터넷에 깔려 있음. 한쪽을 LOCAL / DEV 의 1만 / 2만 규칙에 맞추면 외부 자료를 쓸 때마다 매번 변환해야 함.
대신 두 에뮬레이터를 동시에 띄울 때만 한쪽을 시프트. 한 프로젝트 셋업은 LocalStack 만 호스트 포트를 4666 으로 옮김 (컨테이너 내부 4566 은 그대로 — SDK 호환 유지).
| 도구 | 호스트 포트 | 컨테이너 내부 |
|---|---|---|
| MiniStack | 4566 | 4566 |
| LocalStack | 4666 | 4566 |
Supabase 는 54320 대역 (Kong / DB / Studio / Inbucket) 을 묶어서 사용. supabase CLI 의 디폴트와 동일.
5. LOCAL / DEV / PROD 와 직교성 보장
cloud 의 어떤 포트도 LOCAL / DEV / PROD 의 포트와 충돌하면 안 됨. 그래야 사용자가 infra/local/docker-compose.yml 의 DB · Redis 와 infra/cloud/ministack/docker-compose.yml 의 MiniStack 을 동시에 띄울 수 있음.
여기서 한 가지 함정 — MiniStack 은 동반 Redis 가 필수이고 디폴트 6379 노출. local 의 Redis 도 6379. 충돌. 해결은 단순 — MiniStack 의 동반 Redis 를 호스트에 expose 안 함. 그건 MiniStack 컨테이너 내부 통신용이지 앱이 직접 붙을 대상이 아님.
ministack-redis:
image: redis:7-alpine
# ports 절 없음 — 컨테이너 네트워크 내부에서만 접근
6. 호스트를 가로지르는 안티패턴
cloud 에뮬레이터들은 각자 별 docker-compose 의 별 네트워크에 살아 있음. 이걸 호스트 도커 소켓 마운트 한두 개로 연결하면 안 됨. 예를 들어 Supabase Storage 가 MiniStack 의 S3 를 백엔드로 쓰게 만드는 건 가능하지만, 두 compose 가 동시에 떠 있을 때만 동작. 사용자가 ministack 만 끄면 supabase 가 바로 깨짐.
대신 각 스택은 자체 완결:
- Supabase Storage 는
STORAGE_BACKEND=file— 자체 디스크에 저장. - MiniStack 의 S3 가 필요한 코드는
endpoint=http://localhost:4566으로 직접 호출.
서로의 의존을 끊어야 "필요한 만큼만 띄운다" 가 약속이 됩니다.
7. 시크릿 다루기
cloud 에뮬레이터의 시크릿 (예: Supabase 의 JWT_SECRET · ANON_KEY) 은 로컬 전용 데모 키. 운영 키가 아님. 그래서 .env.example 에 기본값을 박아두는 것이 안전 — 로테이션 의무도 없고 노출 위험도 없음.
대신 한 가지를 지킴 — 운영 시크릿이 cloud 의 .env 에 섞이지 않도록 .env 자체를 git 무시. 누군가 실수로 운영 키를 같은 파일에 적어 commit 하는 사고를 막음.
8. 다른 환경과 조합
| 시나리오 | 함께 띄우는 것 |
|---|---|
| 단순 로컬 개발 | LOCAL (DB · Redis) + 호스트의 pnpm dev |
| 오프라인 개발 (외부 Supabase 차단) | LOCAL + cloud/supabase |
| AWS S3 멀티파트 PoC | cloud/ministack 만 (LOCAL 불필요) |
| SES 메일 라우팅 검증 | cloud/localstack 만 |
| Kakao Login · FCM · Gemini stub | cloud/wiremock 만 |
| Firebase Auth · Firestore 검증 | cloud/firebase 만 |
| CI 에서 통합 테스트 | LOCAL + cloud/{supabase,localstack} (필요한 것만) |
원칙은 단순 — 필요한 환경의 합집합. 4단 구성으로 옮기면서 잃은 건 없고 얻은 건 켜고 끄는 단위의 자유.
9. 한계
- 메모리 발자국 — 다섯 다 띄우면 idle
3.5 GB. Supabase 풀 셋업이 가장 무거움 (3 GB). 노트북 사양에 따라 항상 켜둘 수는 없음. - 버전 핀 부담 — 에뮬레이터의 릴리스 주기가 빠름. 오래된 compose 를 그대로 두면 어느 날 갑자기 unhealthy 가 뜸. 정기적 새 태그 갱신.
- 한 머신 한 사용자 가정 — cloud 환경의 시크릿 · 데모 키는 단일 개발자 PC 가정. 팀이 공유 인프라로 쓰려면 별도 인증 · 분리.
- 에뮬레이터 ≠ 실제 클라우드 — 통합 테스트가 cloud 에서 통과해도 운영 직전에는 실제 클라우드에서 한 번 더 검증. cloud 환경은 빠른 검증 채널이지 최종 검증 채널이 아님.
- FCM 미포함 — Firebase Local Emulator 가 FCM 을 공식 지원하지 않음. 푸시는 WireMock stub 으로 우회.
하고픈 말
local / dev / prod 3단은 자체 인프라만 다루던 시대의 표준. 외부 클라우드 의존이 늘면 4번째 환경 cloud 가 자연스러운 답. 핵심은 각 에뮬레이터를 자체 완결 + 도구 디폴트 포트 존중 + 켜고 끄는 단위를 작게. 이 셋이 함께 있으면 비행기 안에서도 SDK 흐름을 그대로 시험할 수 있습니다.
Next
- (infra 끝)
Supabase self-hosted · LocalStack · MiniStack · Firebase Local Emulator · WireMock 를 참고합니다.