v0로 만든 프로토타입을 기업 DB에 바로 붙이면 안 되는 이유와 안전한 연결법
TuBrief 편집팀
2026년 7월 23일
0
컴퓨터/소프트웨어원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
커뮤니티의 다른 글
댓글 (0)
Log in to leave a comment
아직 작성된 글이 없습니다
원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
Log in to leave a comment
아직 작성된 글이 없습니다
비개발 직원이 v0나 Cursor 같은 바이브 코딩 툴로 사흘 만에 근사한 웹 앱을 만들어옵니다. 대표는 신나서 당장 상용 DB에 붙여서 고객한테 쓰게 하자고 하죠. 이때 개발진이나 CTO는 식은땀이 납니다. AI가 짜준 코드를 열어보면 클라이언트 컴포넌트 한가운데 DB 접속 문자열(DSN)을 그대로 박아두거나, 아무런 검증 없이 SQL 쿼리를 날리는 판국이니까요.
이걸 그대로 운영 서버에 올리면 사고 나는 건 시간문제입니다. OWASP Top 10 for LLM Applications 2025 리포트에서도 AI 생성 코드의 과도한 권한 부여(LLM06)와 입력값 미검증(LLM05)을 가장 위험한 보안 요소로 짚었습니다. 실제로 웹 애플리케이션 보안 침해 사고의 73%가 입력 검증 부재를 노립니다.
비개발자의 생산성을 꺾지 않으면서도 회사 DB를 지키려면, 프로토타입과 DB 사이에 단단한 통제 레이어를 둬야 합니다.
가장 먼저 할 일은 비개발자가 만든 앱이 DB를 직접 호출하지 못하도록 길목을 막는 겁니다. Next.js App Router의 서버 라우트 핸들러를 프록시로 두고, 중간에서 Zod 라이브러리로 들어오는 데이터를 실시간으로 검증해야 합니다. TypeScript의 타입 체크는 개발할 때만 작동할 뿐, 실제 유저가 이상한 데이터를 보낼 때 서버가 터지는 걸 막아주지 못합니다.
| 검증 항목 | 브라우저 UI 단 검증 | 서버 사이드 Zod 프록시 레이어 |
|---|---|---|
| 실행 위치 | 고객 브라우저 | Vercel 서버리스 런타임 |
| 보안성 | 개발자 도구로 쉽게 우회 | 서버에서 강제 차단하여 DB 보호 |
| 데이터 검증 | 형식적인 텍스트 체크 | 런타임 값 범위 및 타입 정밀 검증 |
| 실패 처리 | 화면에 경고 문구 표시 | HTTP 400 반환 및 Sentry 오류 기록 |
구축 순서는 단순합니다.
app/api/v1/customer-records/route.ts 파일부터 만듭니다.companyName은 최소 2자 이상, contactEmail은 이메일 양식, employeeCount는 양의 정수만 받도록 규격을 짭니다.request.json()으로 들어온 요청을 schema.parse()로 검사합니다. 통과한 알맹이만 Prisma ORM으로 DB에 넣고, 실패하면 그 자리에서 400 에러를 뱉고 튕겨냅니다.앱 단 검증과 함께 DB 자체에도 방어막을 쳐야 합니다. PostgreSQL 기반 Supabase를 쓴다면 Row Level Security(RLS) 설정이 답입니다. 유저 역할 정보를 사용자가 조작할 수 있는 raw_user_meta_data 대신 관리자만 만지는 raw_app_meta_data에 넣어두고 JWT를 검증합니다.
-- 1. 테이블 RLS 활성화
ALTER TABLE public.enterprise_documents ENABLE ROW LEVEL SECURITY;
-- 2. JWT에서 사용자 역할 추출하는 함수 생성
CREATE OR REPLACE FUNCTION get_user_role()
RETURNS text AS $$
SELECT NULLIF(current_setting('request.jwt.claims', true)::json->'app_metadata'->>'user_role', '');
$$ LANGUAGE sql STABLE;
-- 3. 부서가 같거나 관리자인 경우만 조회 허용하는 정책
CREATE POLICY "Department Access Policy" ON public.enterprise_documents
FOR SELECT USING (
get_user_role() = 'admin' OR
department = (current_setting('request.jwt.claims', true)::json->'app_metadata'->>'department')
);
이 작업을 해두면 비개발자가 실수로 서비스 역할 키(Service Role Key)를 코드에 흘려도, 다른 부서의 보안 문서는 절대로 터릴 일이 없습니다.
AI 툴은 일단 화면을 띄우는 데 집중하도록 설계되어 있어서, API 키나 DB 커넥션 문자열을 브라우저에 노출되는 위치에 막 씁니다. 가장 자주 보이는 실수가 NEXT_PUBLIC_ 접두사를 아무 데나 붙이는 겁니다. Next.js에서 이 접두사가 붙은 변수는 브라우저 JavaScript 파일에 평문으로 들어갑니다. NEXT_PUBLIC_OPENAI_API_KEY 같은 변수를 만드는 순간, F12 개발자 도구를 열 줄 아는 누구든 내 API 키를 가져가서 마음껏 쓸 수 있습니다.
2026년 4월 Vercel 인프라에서 발생한 보안 침해 사고만 봐도 그렇습니다. 연동된 서깃파티 AI 도구의 권한이 뚫리면서 감춰두지 않은 환경 변수들이 노출됐고, 수많은 팀이 밤을 새우며 DB 비밀번호와 API 키를 일일이 재발급해야 했습니다.
이런 실수를 사람이 눈으로 일일이 검수하는 건 불가능합니다. GitHub Actions에 Gitleaks 정적 분석 도구를 붙여서 코드 합치기 전에 자동으로 털어내야 합니다.
name: Security Scan
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
gitleaks-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install and Run Gitleaks
run: |
wget https://github.com/gitleaks/gitleaks/releases/download/v8.18.0/gitleaks_8.18.0_linux_x64.tar.gz
tar -xzvf gitleaks_8.18.0_linux_x64.tar.gz
sudo mv gitleaks /usr/local/bin/
gitleaks detect --source=. --verbose --redact --exit-code=1
public-env-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check Unsafe Public Keys
run: |
UNSAFE=$(grep -rn "NEXT_PUBLIC_.*\(SECRET\|KEY\|PASSWORD\|TOKEN\|DSN\)" . || true)
if [ -n "$UNSAFE" ]; then
echo "클라이언트에 노출된 민감 변수 발견:"
echo "$UNSAFE"
exit 1
fi
이 파이프라인을 세팅해두면 비개발자가 API 키를 소스 코드나 NEXT_PUBLIC_ 변수에 넣고 Push하는 순간 배포가 자동으로 차단됩니다.
Vercel 대시보드에 변수를 등록할 때도 Development, Preview, Production 스코프를 확실히 나눠야 합니다. 특히 Sensitive Environment Variable 체크박스를 반드시 켜두세요. 빌드 로그에 출력되지 않고 대시보드에서도 평문 복사가 안 되게 암호화됩니다.
OpenAI나 Anthropic 서버가 먹통이 되거나 응답이 늦어지면 서버리스 핸들러 전체가 대기 상태에 빠지면서 연쇄 장애가 납니다. 이럴 때는 Node.js의 opossum 라이브러리로 서킷 브레이커를 걸어줘야 합니다.
import CircuitBreaker from 'opossum';
async function callLLM(prompt: string) {
// LLM API 호출 로직
}
const options = {
timeout: 5000, // 5초 넘어가면 실패 처리
errorThresholdPercentage: 50, // 실패율 50% 넘으면 차단기 오픈
resetTimeout: 30000 // 30초 후 다시 시도
};
const breaker = new CircuitBreaker(callLLM, options);
breaker.fallback(() => ({ error: "AI 응답이 지연되고 있습니다. 잠시 후 다시 시도해주세요." }));
API 응답이 늦어지면 0.1초 만에 준비된 안내 문구를 보내서 전체 서비스가 다운되는 걸 막을 수 있습니다.
장애 상황을 비개발자 제작자가 바로 알 수 있도록 Sentry와 Slack 웹훅도 연결해둡니다.
// app/api/webhooks/sentry-to-slack/route.ts
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
try {
const event = await request.json();
const webhookUrl = process.env.SLACK_INCOMING_WEBHOOK_URL;
if (!webhookUrl) return NextResponse.json({ error: 'Webhook 없음' }, { status: 500 });
const title = event.data?.issue?.title || '알 수 없는 시스템 오류';
const issueUrl = event.data?.issue?.permalink || '#';
await fetch(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
blocks: [
{
type: 'header',
text: { type: 'plain_text', text: '🚨 프로덕션 오류 발생' }
},
{
type: 'section',
text: { type: 'mrkdwn', text: `*오류 내용:*\n${title}` }
},
{
type: 'actions',
elements: [
{
type: 'button',
text: { type: 'plain_text', text: 'Sentry 리포트 보기' },
url: issueUrl,
style: 'danger'
}
]
}
]
})
});
return NextResponse.json({ success: true });
} catch (err) {
return NextResponse.json({ error: '웹훅 실패' }, { status: 500 });
}
}
오류가 커져서 메인 페이지가 안 뜨는 P1 등급 장애가 발생하면 개발자를 기다릴 필요도 없습니다. Vercel은 이전 배포본을 그대로 보관하고 있어서 클릭 몇 번으로 직전 상태로 돌릴 수 있습니다.
30초만 기다리면 도메인 라우팅이 이전 버전으로 바뀝니다. Zod 검증, Gitleaks 스캔, Vercel 롤백 절차까지 갖춰두면 비개발자가 마음껏 앱을 만들게 두면서도 밤에 다리 뻗고 잘 수 있습니다.