AIMetaClean 빌드인퍼블릭: 기술 스택과 아키텍처 결정
AI Meta Clean의 기술 스택과 아키텍처를 AI와 하나씩 비교하며 결정했습니다. Next.js, NestJS, BullMQ, PostgreSQL, Redis, MinIO부터 파일 폐기와 보안 원칙까지 문서화한 과정을 공개합니다.
AI Meta Clean을 아이디어부터 기획하고, 개발하고, 출시하고, 홍보하는 전 과정을 공개하고 있습니다.
현재 진행 단계는 다음과 같습니다.
아이디어 발견 → 프로젝트 환경 세팅 → 제품 기획
→ 기술 스택과 아키텍처 결정
→ MVP 개발 → 배포 → 사용자 모집 → 출시와 홍보 → 실제 반응과 개선
이번 글에서는 제품 기획을 바탕으로 기술 스택과 전체 아키텍처를 결정한 과정을 정리합니다.
이제 드디어 코드를 작성할 것 같지만, 아직은 아닙니다.
코드를 짜기 전에 어떤 기술을 사용하고, 각 기능을 어디에서 처리하며, 파일은 어떻게 보관하고 삭제할지부터 결정했습니다.
새로운 작업은 새로운 TASK에서 시작합니다
이번 작업 역시 vibeops로 새로운 TASK를 열면서 시작했습니다.
goodtek aimetaclean % vibeops task add
실행 결과는 다음과 같습니다.
→ Checking integration branch…
Integration develop is ready.
✔ What are you doing now? (short) 기술 스택과 아키텍처를 설정
· Scaffold via codex-oauth
✓ Created TASK-002 → docs/tasks/TASK-002-set-tech-stack-and-architecture.md
→ Starting task branch…
integration develop @ 9c37fc3
task branch task/002-set-tech-stack-and-architecture
Stashed governance-only changes (.vibeops/, docs/) before branch switch.
Restored stashed governance paths on the task branch.
✓ Branch task/002-set-tech-stack-and-architecture ready
file docs/tasks/TASK-002-set-tech-stack-and-architecture.md
Next in Cursor
Ask: @docs/tasks/TASK-002-set-tech-stack-and-architecture.md — plan Scope / Acceptance Criteria
Agent: same file — implement
Ship: vibeops task ship TASK-002
TASK-002 문서와 전용 브랜치가 만들어졌습니다.
이제 기술 스택과 아키텍처에 관한 논의와 결정은 모두 이 TASK 안에서 관리됩니다.
이번 작업은 Codex로 진행했습니다

저는 Cursor와 Codex 모두 에이전트 대화창을 메인으로 두고 사용합니다.
오른쪽에는 파일이나 브라우저를 배치하고, 아래에는 터미널을 열어둡니다.
그리고 이 프로젝트는 모바일 ChatGPT와도 실시간으로 연결해 두었습니다.
PC 앞에 있지 않더라도 모바일에서 프로젝트 상태를 확인하고, AI와 논의를 이어가며 작업할 수 있습니다.
이번 TASK는 실제 구현보다는 기술적인 의사결정이 중심이었습니다.
어느 정도 사용할 기술 스택을 생각해 둔 상태였기 때문에, 가장 높은 단계의 모델까지 사용할 필요는 없다고 판단했습니다.
그래서 Sol보다 한 단계 낮은 Terra Medium, 속도는 Standard로 시작했습니다.
첫 프롬프트는 간단했습니다
Codex에 다음과 같이 요청했습니다.
TASK-001-ai-meta-clean-project-product-planning.md 프로젝트 기획을 참고해서
기술 스택 및 아키텍처를 정하려고 하는데
나랑 티키타카 하면서 정해 봅시다.
정리가 완료되면
TASK-002-set-tech-stack-and-architecture.md에 정리해 주시면 됩니다.
제가 원한 것은 AI가 알아서 기술 스택을 선택하고 문서 하나를 만들어 주는 방식이 아니었습니다.
하나씩 질문을 받고, 제 생각을 이야기하고, 서로 비교하면서 결정하는 방식이었습니다.
AI가 추천한 스택을 그대로 사용하지 않았습니다
기술 스택을 결정할 때 AI가 제안한 내용을 그대로 받아들이지는 않았습니다.
질문에 하나씩 답하면서 다음과 같이 진행했습니다.
- 제 생각과 맞으면 확정
- 아닌 것 같으면 제 의견을 제시
- 다른 대안이 궁금하면 비교 요청
- 이해되지 않으면 쉽게 다시 설명해 달라고 요청
- 향후 확장까지 고려했을 때 약하다고 판단되면 재검토
- 이미 운영 중인 서버나 도구가 있으면 현재 환경을 알려주고 다시 설계
예를 들어 처음부터 모든 결정이 지금의 형태였던 것은 아닙니다.
진행 상태 전달 방식으로 단순 Polling이 언급되었지만, 저는 MVP라고 해서 구조를 약하게 만들고 싶지는 않았습니다.
그래서 Polling, SSE, WebSocket을 다시 비교했고 최종적으로는 다음 구조를 선택했습니다.
WebSocket + Redis + Socket.IO
사용자마다 자신의 작업 진행률을 실시간으로 확인하고, 작업이 완료되면 다운로드 버튼이 나타나는 구조입니다.
브라우저 연결이 끊겼다가 다시 연결되더라도 PostgreSQL에 저장된 현재 작업 상태를 기준으로 화면을 복구할 수 있도록 했습니다.
최종 기술 스택
논의를 통해 최종 결정된 주요 기술은 다음과 같습니다.
프론트엔드
Next.js
TypeScript
Tailwind CSS
shadcn/ui
next-intl
Next.js는 사용자 화면과 다국어 라우팅을 담당합니다.
지원 언어는 다음 네 가지입니다.
한국어
English
日本語
简体中文
첫 방문 시 브라우저 언어를 확인해 적절한 언어 경로로 이동합니다.
한국어는 별도의 /ko 경로를 사용하지 않고 루트 /를 사용합니다.
한국어 /
English /en
日本語 /ja
简体中文 /zh-CN
사용자가 직접 선택한 언어는 localStorage와 locale cookie에 함께 저장합니다.
API 서버
NestJS
Socket.IO
처음에는 Next.js Route Handler만으로 API를 구성하는 방법도 검토했습니다.
하지만 이 서비스에는 다음 기능이 필요합니다.
- 이미지 스트리밍 업로드
- 작업 생성과 권한 확인
- WebSocket 연결
- Redis 기반 다중 인스턴스 이벤트 전달
- 일회성 다운로드 토큰 검증
- Worker와 Queue 분리
- 향후 관리 기능과 외부 API 확장
따라서 핵심 파일 처리 API는 Next.js에 넣지 않고 별도의 NestJS 애플리케이션으로 분리하기로 했습니다.
비동기 작업 처리
Redis
BullMQ
NestJS Worker
이미지의 메타데이터를 확인하고 제거하는 작업은 처리 시간이 일정하지 않습니다.
이를 HTTP 요청 안에서 동기적으로 처리하면 동시 사용자가 늘어났을 때 타임아웃과 메모리 압박이 발생할 수 있습니다.
따라서 API는 다음까지만 담당합니다.
파일 검증
업로드
작업 생성
Queue 등록
실제 처리는 별도의 Worker가 담당합니다.
메타데이터 탐지
C2PA 제거
EXIF/XMP/IPTC 제거
처리 결과 재검증
결과 파일 생성
파일 폐기
메타데이터 처리
ExifTool
c2pa-rs 기반 Rust Helper
일반적인 이미지 메타데이터인 EXIF, XMP, IPTC는 ExifTool로 처리합니다.
C2PA는 서비스의 핵심 기능이기 때문에 ExifTool만으로 처리하지 않고, 공식 c2pa-rs 기반 Rust Helper를 별도로 사용하기로 했습니다.
Worker의 처리 순서는 다음과 같습니다.
1. C2PA 존재 여부와 상태 확인
2. C2PA 제거
3. ExifTool로 EXIF/XMP/IPTC 정리
4. c2pa-rs와 ExifTool로 처리 결과 재검증
5. 제거·유지·실패 사유를 보고서에 기록
c2pa-rs는 아직 변화 가능성이 있는 도구이므로 정확한 버전을 고정하고 자동으로 업그레이드하지 않도록 결정했습니다.
포맷별 테스트 이미지도 따로 준비해 제거 전후 결과를 자동으로 검증할 예정입니다.
데이터베이스
PostgreSQL
Prisma
처음에는 파일을 보관하지 않는 서비스이기 때문에 데이터베이스가 꼭 필요한지 생각해 볼 수 있습니다.
하지만 실제 운영에서는 다음 정보가 필요합니다.
- 작업 성공률
- 작업 실패율
- 실패 원인
- 파일 폐기 성공 여부
- 처리 시간
- 사용 형식과 용량 구간
- 서비스 사용량
이 정보가 있어야 서비스를 계속 운영할 가치가 있는지 판단할 수 있습니다.
따라서 PostgreSQL을 사용하되, 개인정보와 연결될 수 있는 데이터는 최소화하기로 했습니다.
DB에 저장하는 정보는 다음과 같습니다.
작업 상태
생성·처리·폐기 시각
이미지 형식
파일 용량 구간
성공·실패 여부
실패 범주
폐기 결과
최소 운영 통계
반대로 다음 정보는 저장하지 않습니다.
원본 이미지
결과 이미지
파일명
원본 메타데이터
다운로드 토큰 원문
세션 쿠키 원문
원시 IP 로그
ORM은 Prisma를 선택했습니다.
장기적인 스키마 관리와 Migration 안정성을 우선했고, 복잡한 운영 통계가 필요할 때만 제한적으로 Raw SQL을 사용하기로 했습니다.
임시 파일 저장소
Private MinIO
MinIO와 RustFS, 호스트 파일시스템을 비교했습니다.
최종적으로는 운영 경험과 S3 호환성, Lifecycle 기능이 충분히 검증된 MinIO를 선택했습니다.
중요한 점은 MinIO를 인터넷에 공개하지 않는다는 것입니다.
브라우저가 MinIO에 직접 파일을 올리거나 다운로드하지 않습니다.
모든 업로드와 다운로드는 NestJS API를 통해 스트리밍됩니다.
Browser
→ NestJS API
→ Private MinIO
다운로드에도 MinIO의 Presigned URL을 브라우저에 직접 노출하지 않습니다.
NestJS API가 일회성 다운로드 토큰을 검증한 뒤 파일을 스트리밍합니다.
파일은 언제 삭제할 것인가
AI Meta Clean의 가장 중요한 원칙 중 하나는 다음과 같습니다.
처리 파일을 서버에 영구 보관하지 않는다.
하지만 단순히 “처리 후 삭제합니다”라고 쓰는 것으로는 부족합니다.
정확히 어느 시점에 무엇을 삭제할지 정했습니다.
원본과 중간 파일
처리 결과 검증이 끝나는 즉시 삭제
결과 파일과 처리 보고서
사용자가 다운로드한 뒤 즉시 삭제
다운로드하지 않은 파일
최대 30분 뒤 자동 삭제
삭제는 Worker의 정리 작업과 MinIO Lifecycle Rule을 함께 사용합니다.
Worker가 정상적으로 파일을 삭제하지 못하더라도 MinIO Lifecycle이 다시 한번 폐기하는 이중 안전장치입니다.
로그인 없이 사용자를 어떻게 구분할까
이 서비스는 초기 MVP에서 회원가입을 요구하지 않을 예정입니다.
그렇다고 모든 작업과 다운로드가 공개되어서는 안 됩니다.
그래서 로그인 대신 무작위 세션을 사용하기로 했습니다.
256-bit 무작위 opaque session cookie
Cookie 설정은 다음과 같습니다.
HttpOnly
Secure
SameSite=Lax
PostgreSQL에는 세션 원문을 저장하지 않고 해시만 저장합니다.
각 작업은 해당 세션 해시와 연결됩니다.
다운로드 토큰도 일회성 고엔트로피 토큰으로 만들고, 사용 여부를 원자적으로 처리합니다.
IP 주소나 기기 Fingerprint로 사용자를 식별하지는 않습니다.
서버와 배포 구조
현재 운영 중인 서버 환경을 그대로 활용합니다.
Oracle Cloud
Oracle Linux 9
aarch64
rootless Podman
Host Caddy
서비스는 다음 컨테이너로 분리합니다.
Next.js Web
NestJS API
NestJS Worker
PostgreSQL
Redis
MinIO
호스트에 직접 설치하는 방식도 검토했지만, 의존성 격리와 롤백, 재현 가능한 배포를 위해 rootless Podman으로 확정했습니다.
외부에 공개되는 것은 Caddy뿐입니다.
Internet
│
▼
Host Caddy
├─ / → Next.js
├─ /en → Next.js
├─ /ja → Next.js
├─ /zh-CN → Next.js
├─ /api/* → NestJS API
└─ /socket.io → Socket.IO Gateway
Next.js와 NestJS는 Loopback에만 바인딩합니다.
PostgreSQL, Redis, MinIO는 Podman 내부 네트워크에서만 접근할 수 있습니다.
Worker는 더 강하게 격리합니다
이미지 파일은 사용자가 업로드하는 외부 입력입니다.
따라서 실제 이미지 처리 도구가 실행되는 Worker는 일반 웹 애플리케이션보다 더 강한 격리가 필요합니다.
Worker에는 다음 제한을 적용하기로 했습니다.
non-root 실행
read-only root filesystem
Linux capability drop
no-new-privileges
CPU 제한
메모리 제한
작업 시간 제한
전용 임시 디렉터리
외부 인터넷 접근 차단
Worker는 내부의 MinIO, Redis, PostgreSQL에만 접근할 수 있습니다.
또한 사용자가 입력한 값을 셸 명령어 문자열로 조합하지 않도록 설계합니다.
비밀 정보는 애플리케이션이 직접 가져오지 않습니다
현재 self-hosted Infisical을 운영하고 있기 때문에 기존 환경을 활용합니다.
CI/CD는 다음 흐름으로 구성하기로 했습니다.
GitHub Actions
→ OIDC
→ Infisical Machine Identity
→ Short-lived Credential
→ Podman Secret
애플리케이션 컨테이너가 실행 중에 Infisical에 직접 접근하지 않습니다.
배포 시 필요한 비밀만 Podman Secret으로 전달합니다.
Production Runner는 격리하고, 일반 Pull Request가 Production Runner나 Production Secret에 접근하지 못하도록 제한합니다.
디자인도 코드만큼 문서화합니다
이번 대화에서 제가 다시 짚은 부분이 하나 있습니다.
처음 기술 스택을 정리할 때 Tailwind CSS와 shadcn/ui는 포함되었지만, 디자인을 어떤 기준으로 통제할지가 빠져 있었습니다.
그래서 루트의 DESIGN.md를 제품 디자인의 단일 기준 문서로 두기로 했습니다.
DESIGN.md에는 다음 내용이 들어갑니다.
제품 디자인 원칙
Typography
Spacing
Radius
Shadow
Semantic Color Token
공용 컴포넌트 계약
화면별 상태
C2PA 경고 문구
결과 및 폐기 안내
다국어 문구 규칙
접근성 기준
모바일 Safe Area
디자인 검토 체크리스트
Tailwind CSS와 shadcn/ui가 디자인의 기준이 아닙니다.
두 도구는 DESIGN.md에 정의된 디자인을 구현하는 수단일 뿐입니다.
새로운 화면이나 공용 컴포넌트, 사용자 문구가 추가되면 같은 변경 안에서 DESIGN.md도 함께 업데이트합니다.
최종 저장소 구조
프로젝트는 pnpm Workspace와 Turborepo를 사용하는 모노레포로 구성합니다.
apps/
web/ Next.js 사용자 UI
api/ NestJS HTTP API + Socket.IO gateway
worker/ NestJS BullMQ worker
packages/
contracts/ API, Queue, Socket.IO 이벤트 타입
database/ Prisma schema, client, migrations
config/ 공유 설정
infra/
podman/ Podman 구성
caddy/ Caddy reverse proxy
deploy/ 배포 정의
DESIGN.md 디자인 단일 기준 문서
docs/
tasks/ TASK 문서
project/ 아키텍처·결정·현재 상태
최종 아키텍처
전체 구조를 한 번에 정리하면 다음과 같습니다.
Browser
│
▼
Host Caddy
├─ / → Next.js Web
├─ /en → Next.js Web
├─ /ja → Next.js Web
├─ /zh-CN → Next.js Web
├─ /api/* → NestJS API
└─ /socket.io → NestJS Socket.IO Gateway
│
├─ PostgreSQL
│ └─ 작업 상태, 세션 해시, 운영 통계
│
├─ Redis
│ └─ BullMQ, Rate Limit, Socket.IO Fan-out
│
└─ Private MinIO
└─ 원본, 중간 파일, 결과, 보고서
▲
│
NestJS BullMQ Worker
├─ c2pa-rs Helper
└─ ExifTool
바이브코딩을 블랙박스로 만들지 않는 방법
이번 단계에서 결정한 내용은 하나의 TASK 문서에만 들어가지 않습니다.
각 결정의 성격에 맞게 여러 문서에 나누어 반영했습니다.
TASK-002-set-tech-stack-and-architecture.md
docs/project/03-architecture.md
docs/project/05-current-state.md
docs/project/06-decisions.md
DESIGN.md
TASK-001-ai-meta-clean-project-product-planning.md
기술 스택, 아키텍처, 현재 상태, 결정 이유, 디자인 기준이 각각의 문서에 체계적으로 기록됩니다.
향후 구현 중 결정이 변경되면 코드만 수정하는 것이 아니라 관련 문서도 함께 업데이트합니다.
이렇게 하면 바이브코딩도 더 이상 AI가 내부에서 무언가 만들어 내는 블랙박스 작업이 아닙니다.
기획과 결정, 구현과 변경 이력이 연결된 하나의 지속 가능한 운영 시스템이 됩니다.
이제 실제 개발만 남았습니다
여기까지가 머리를 가장 많이 써야 하는 단계였습니다.
무엇을 만들지 정했고, 기능과 정책을 정했고, 기술 스택과 아키텍처까지 결정했습니다.
이제 다음 단계에서는 이 아키텍처를 기준으로 구현 계획을 작성하고, 작업을 작은 단위로 나누어 하나씩 개발하면 됩니다.
물론 구현 과정에서 예상하지 못한 문제는 나오겠지만, 적어도 매번 다음 질문을 처음부터 반복할 필요는 없습니다.
어떤 기술을 사용하지?
파일은 어디에 저장하지?
작업은 어디서 처리하지?
사용자는 어떻게 구분하지?
완료 상태는 어떻게 전달하지?
파일은 언제 삭제하지?
이미 기준이 문서에 정리되어 있기 때문입니다.
티키타카한 전체 내용은 너무 길어 본문에는 모두 넣지 못했습니다.
대신 전체 대화 기록을 첨부합니다.
관심이 있으시다면 제가 AI의 답을 그대로 받아들이는 것이 아니라, 어떤 식으로 질문하고 반박하고 다시 비교하면서 바이브코딩의 기술적 의사결정을 진행하는지 확인하실 수 있습니다.
다음 글에서는 이번에 결정한 기술 스택과 아키텍처를 실제 구현 작업으로 나누고, MVP 개발을 시작하겠습니다.
그리고 모델 사용량은?
100%에서 시작했는데 작업을 마치고 보니 97%가 남아 있었습니다.
기술 스택과 아키텍처를 꽤 오래 티키타카했는데 3% 사용.
이 정도면 별로 안 쓴 건가요? ㅎㅎ