1인 SaaS에서 사용자 코드를 안전하게 돌리며 월 50달러로 방어하는 법
TuBrief 편집팀
2026년 8월 22일
0
컴퓨터/소프트웨어원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
커뮤니티의 다른 글
댓글 (0)
Log in to leave a comment
아직 작성된 글이 없습니다
원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
Log in to leave a comment
아직 작성된 글이 없습니다
사용자가 직접 작성한 스크립트를 서비스 안에서 돌려주는 기능은 매력적입니다. AI 에이전트나 데이터 자동화 툴을 만들 때 특히 그렇습니다. 문제는 보안입니다. 혼자 백엔드를 도맡는 상황에서 남의 임의 코드를 서버에 올리는 일은 늘 등골이 서늘합니다. 컨테이너 탈출 공격이라도 터지면 환경 변수에 든 API 키가 전부 털립니다. 그렇다고 쿠버네티스 클러스터를 띄우자니 인프라 관리 부담과 비용이 감당 안 됩니다.
이럴 때 Vercel Sandbox API와 Vercel Blob 조합이 현실적인 대안이 됩니다. AWS Firecracker 기반 MicroVM을 쓰기 때문에 세션마다 격리된 커널이 수백 밀리초 만에 떴다 사라집니다.
사용자 코드를 직접 eval()이나 child_process로 실행하면 단일 프로세스 전체가 뻗습니다. 도커 컨테이너를 상시 띄워두는 방식도 1인 개발자에겐 부담스럽습니다. 사용자가 네트워크 요청을 기다리거나 sleep을 걸어두는 I/O 대기 시간에도 서버 비용이 그대로 나가기 때문입니다.
Vercel Sandbox는 실제 연산이 일어나는 Active CPU 시간만 과금합니다.
| 플랫폼 | 기본 월 구독료 | CPU 과금 방식 | I/O 대기 시간 비용 | 최대 동시성 한도 |
|---|---|---|---|---|
| Vercel Sandbox (Pro) | $20 | Active CPU ($0.128/vCPU-hr) | $0 | 2,000 세션 |
| E2B (Pro) | $150 | 전체 실행 시간 ($0.0504/vCPU-hr) | 정상 청구 | 100 세션 |
| CodeSandbox (Scale) | $170 | 전체 실행 시간 ($0.1486/hr Nano) | 정상 청구 | 250 세션 |
Pro 플랜 기본 크레딧 범위 안에서 트래픽을 관리하면 고정비 부담 없이 월 50달러 안쪽으로 유지할 수 있습니다.
무한 루프나 메모리 누수를 막으려면 인스턴스 수명주기를 코드로 묶어둬야 합니다. 실행이 끝나면 즉시 가상 머신을 폐기하도록 persistent: false를 지정해야 스냅샷 저장 비용(GB당 월 $0.08)이 붙지 않습니다.
import { Sandbox } from '@vercel/sandbox';
interface CodeExecutionRequest {
code: string;
timeoutMs?: number;
}
interface CodeExecutionResult {
stdout: string;
stderr: string;
exitCode: number;
durationMs: number;
}
export async function executeUserCode(
payload: CodeExecutionRequest
): Promise<CodeExecutionResult> {
const timeout = payload.timeoutMs || 15_000;
const startTime = Date.now();
const sandbox = await Sandbox.create({
timeout,
persistent: false,
runtime: 'node24',
});
try {
await sandbox.writeFiles([
{
path: 'index.js',
content: Buffer.from(payload.code),
},
]);
const commandResult = await sandbox.runCommand('node', ['index.js']);
const stdout = await commandResult.stdout();
const stderr = await commandResult.stderr();
const exitCode = commandResult.exitCode;
return {
stdout,
stderr,
exitCode,
durationMs: Date.now() - startTime,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
stdout: '',
stderr: `Execution Failed: ${errorMessage}`,
exitCode: -1,
durationMs: Date.now() - startTime,
};
} finally {
await sandbox.stop();
}
}
finally 블록에서 sandbox.stop()을 직접 닫아주지 않으면 타임아웃까지 리소스가 물려있어 불필요한 과금이 생깁니다.
프론트엔드에서 백엔드 API를 거쳐 코드를 올리면 Vercel Serverless Function의 4.5MB 페이로드 제한에 걸립니다. 클라이언트가 Vercel Blob Storage로 파일을 바로 쏘게 만들어야 백엔드 병목이 사라집니다.
import { issueSignedToken } from '@vercel/blob';
import { handleUploadPresigned, type HandleUploadPresignedBody } from '@vercel/blob/client';
import { NextResponse } from 'next/server';
async function authenticateRequest(req: Request) {
return { userId: 'user_dev_01', isAuthorized: true };
}
export async function POST(request: Request): Promise<NextResponse> {
const body = (await request.json()) as HandleUploadPresignedBody;
try {
const jsonResponse = await handleUploadPresigned({
body,
request,
getSignedToken: async (pathname) => {
const user = await authenticateRequest(request);
if (!user.isAuthorized) {
throw new Error('Unauthorized access');
}
const token = await issueSignedToken({
pathname,
operations: ['put'],
allowedContentTypes: ['text/plain', 'application/javascript', 'application/json', 'application/zip'],
maximumSizeInBytes: 5 * 1024 * 1024,
validUntil: Date.now() + 10 * 60 * 1000,
});
return {
token,
urlOptions: {
addRandomSuffix: true,
allowOverwrite: false,
},
};
},
onUploadCompleted: async ({ blob }) => {
console.log(`Blob storage upload completed: ${blob.url}`);
},
});
return NextResponse.json(jsonResponse);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return NextResponse.json({ error: message }, { status: 400 });
}
}
Vercel Sandbox는 인바운드 트래픽 요금이 무료입니다. Blob Storage에 올라간 스크립트 파일을 샌드박스로 긁어올 때 추가 네트워크 비용은 나가지 않습니다.
실행 상태를 클라이언트에게 실시간으로 보여주지 않으면 "멈춘 것 같다"는 문의가 쏟아집니다. 백엔드에서 { detached: true } 옵션과 command.logs() 비동기 이터레이터를 구독하면 stdout과 stderr를 분리해서 프론트에 바로 전달할 수 있습니다.
const command = await sandbox.runCommand({
cmd: 'node',
args: ['index.js'],
detached: true,
});
for await (const line of command.logs()) {
if (line.stream === 'stdout') {
process.stdout.write(`[STDOUT]: ${line.data}`);
} else if (line.stream === 'stderr') {
console.error(`[STDERR]: ${line.data}`);
}
}
const executionResult = await command.wait();
if (executionResult.exitCode !== 0) {
console.warn(`Execution failed with exit code: ${executionResult.exitCode}`);
}
운영 시 챙겨야 할 세부 설정들이 몇 가지 있습니다.
iad1, $0.128/hr)나 클리블랜드(cle1)로 지정합니다. 파리(cdg1, $0.177/hr) 같은 리전보다 연산 단가가 낮습니다.169.254.169.254) 접근을 막는 에그레스 규칙을 적용해 SSRF 공격을 차단합니다.MicroVM 기반 격리와 Blob 직접 전송 파이프라인을 엮어두면 인프라 작업에 매이지 않고 제품 기능에만 집중할 수 있습니다.