TuBrief
구독 채널
비디오
커뮤니티

1인 SaaS에서 사용자 코드를 안전하게 돌리며 월 50달러로 방어하는 법

TuBrief 편집팀
2026년 8월 22일
0
컴퓨터/소프트웨어

원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.

한국어Englishहिन्दीРусскийEspañolDeutschBahasa Indonesia中文Français日本語العربيةPortuguês

관련 영상

Ship 26 뉴욕 - 워크숍 - 미니 워커스38:16

Ship 26 뉴욕 - 워크숍 - 미니 워커스

Vercel

커뮤니티의 다른 글

사내 시스템에 llm api 붙일 때 마주하는 현실적인 한계와 대응법

2026년 9월 13일

레거시 백엔드에 GPT-6 Astra 붙일 때 예산 승인과 보안 통과를 먼저 끝내는 법이 있습니다

2026년 9월 13일

에이전트끼리 대화하다 6천만 원 청구서가 나오는 이유

2026년 9월 13일

사내 RAG 벡터 검색에 Okta 권한 필터를 직접 거는 방법

2026년 9월 13일

브라우저 에이전트에게 내 구글 계정을 통째로 넘기면 안 되는 이유

2026년 9월 12일

Apple Won the AI Race

2026년 9월 12일

댓글 (0)

Log in to leave a comment

아직 작성된 글이 없습니다

© 2026 . All rights reserved.

TuBrief
구독 채널
비디오
커뮤니티
로그인

1인 SaaS에서 사용자 코드를 안전하게 돌리며 월 50달러로 방어하는 법

사용자가 직접 작성한 스크립트를 서비스 안에서 돌려주는 기능은 매력적입니다. AI 에이전트나 데이터 자동화 툴을 만들 때 특히 그렇습니다. 문제는 보안입니다. 혼자 백엔드를 도맡는 상황에서 남의 임의 코드를 서버에 올리는 일은 늘 등골이 서늘합니다. 컨테이너 탈출 공격이라도 터지면 환경 변수에 든 API 키가 전부 털립니다. 그렇다고 쿠버네티스 클러스터를 띄우자니 인프라 관리 부담과 비용이 감당 안 됩니다.

이럴 때 Vercel Sandbox API와 Vercel Blob 조합이 현실적인 대안이 됩니다. AWS Firecracker 기반 MicroVM을 쓰기 때문에 세션마다 격리된 커널이 수백 밀리초 만에 떴다 사라집니다.

1. 유휴 자원 과금 피하기

사용자 코드를 직접 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달러 안쪽으로 유지할 수 있습니다.

2. 샌드박스 인스턴스 생성과 리소스 강제 회수

무한 루프나 메모리 누수를 막으려면 인스턴스 수명주기를 코드로 묶어둬야 합니다. 실행이 끝나면 즉시 가상 머신을 폐기하도록 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()을 직접 닫아주지 않으면 타임아웃까지 리소스가 물려있어 불필요한 과금이 생깁니다.

3. 대용량 코드 업로드를 위한 직렬화 우회

프론트엔드에서 백엔드 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에 올라간 스크립트 파일을 샌드박스로 긁어올 때 추가 네트워크 비용은 나가지 않습니다.

4. 로그 스트리밍과 비용 안전장치

실행 상태를 클라이언트에게 실시간으로 보여주지 않으면 "멈춘 것 같다"는 문의가 쏟아집니다. 백엔드에서 { 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 공격을 차단합니다.
  • Vercel 대시보드의 Spend Management에서 결제 한도를 $40 수준으로 걸어둡니다.

MicroVM 기반 격리와 Blob 직접 전송 파이프라인을 엮어두면 인프라 작업에 매이지 않고 제품 기능에만 집중할 수 있습니다.