
1인 1에이전트의 벽을 넘는 AWS Blocks로 만드는 풀스택 자기 검증 루프
요약
에이전트의 개수가 늘어남에 따라 발생하는 인간의 리뷰 병목 현상을 해결하기 위해, AWS Blocks를 활용한 풀스택 자기 검증 루프 구축 방법을 소개합니다. 로컬 mock과 실제 환경을 일치시켜 에이전트가 스스로 테스트와 타입 체크를 수행하며 오류를 수정할 수 있는 환경을 제안합니다.
핵심 포인트
- 에이전트 확장을 위해 인간의 리뷰를 줄이는 자기 검증 루프가 필수적임
- AWS Blocks를 통해 로컬과 클라우드 간의 코드 일관성 확보 가능
- 타입 체크, API, 로컬 mock을 연결하여 에이전트에게 구체적 피드백 제공
- 에이전트가 스스로 실패를 인지하고 수정하는 'Parallel' 단계로의 이행
어느덧 코딩 에이전트(Coding Agent)를 사용하지 않고 개발하는 날은 거의 사라졌습니다.
조사나 코드 보완뿐만 아니라, 기능 구현, 테스트, 디버깅까지 맡기는 상황이 늘어나고 있습니다. 코드가 완성되는 속도는 분명히 빨라졌습니다. 다음에 생각하게 되는 것은, 여러 에이전트를 병렬로 구동하여 더 넓은 범위를 맡기는 것입니다.
하지만 여기서 인간 측이 막히게 됩니다. 에이전트가 늘어날수록 생성된 차분(diff)을 확인하는 작업도 늘어나기 때문입니다.
Assisted에서 Parallel로의 벽
Boris Cherny 씨는 「Steps of AI Adoption」에서 Claude를 활용하는 개발 팀의 성숙도를 단계별로 정리하고 있습니다.
Assisted
는 1명의 엔지니어와 1개의 에이전트가 페어로 작업하는 단계입니다. 리뷰는 인간이 거의 전건 수행합니다.
Parallel
에서는 1명이 5~10개의 에이전트를 동시에 구동합니다. 팀이 몇 주에 걸쳐 처리하던 백로그(Backlog)가 1명의 오후 시간 만에 해결되는 세계입니다.
이 이행에 필요한 것은 단순히 에이전트의 탭을 늘리는 것이 아닙니다. Cherny 씨가 꼽는 조건 중 하나는 테스트, 빌드, lint, E2E 등을 통해 에이전트가 자신의 변경 사항을 검증할 수 있는 상태여야 한다는 것입니다.
에이전트에게 구현을 맡긴 결과, 변경의 영향 범위를 간과하여 그때까지 잘 작동하던 기능이 망가진 경험은 없으신가요? 테스트도 함께 작성하게 했지만, 모크(Mock)가 구현을 그대로 따라갈 뿐 "이 테스트가 무엇을 보장하고 있는 것일까"라고 느낀 적은 없으신가요?
인간이 매번 모든 것을 확인하는 한, 위임할 수 있는 양에는 상한선이 있습니다. 그래서 프론트엔드부터 API, Block의 로컬 mock까지 한꺼번에 구동하여, 실패를 에이전트에게 되돌려주는 환경을 만들어 보았습니다.
AWS Blocks
2026년 6월에 퍼블릭 프리뷰(Public Preview)로 공개된 AWS Blocks는 TypeScript로 백엔드를 구축하기 위한 프레임워크입니다. 인증이나 데이터 저장 등을 「Block」으로서 선언적으로 조합합니다.
특징적인 것은 동일한 코드가 로컬에서도 AWS 상에서도 그대로 동작한다는 점입니다.
const items = new DistributedTable(scope, 'items', {
schema: itemSchema,
key: { partitionKey: 'userId', sortKey: 'itemId' },
...
이 하나의 선언이 로컬에서는 Block이 제공하는 로컬 mock이 되고, AWS에서는 DynamoDB 테이블이 됩니다. 로컬 데이터는 .bb-data에 영속화되며, 애플리케이션 측에서 환경을 판별하는 if 문을 작성할 필요가 없습니다.
자기 검증 루프(Self-verification loop)를 위해 중요한 것은 프론트엔드, API, Block의 로컬 mock을 연결된 상태로 구동할 수 있다는 점입니다. 앱 고유의 수동 작성 mock을 준비하지 않아도, 에이전트는 타입 에러(Type error)나 테스트 실패라는 구체적인 피드백을 받을 수 있습니다.
자기 검증 루프를 4개 층으로 나누기
모든 것을 하나의 테스트로 보장하려 하지 않고, 책임을 나누었습니다.
| 층 | 무엇을 검증하는가 | 테스트의 목적 |
|---|---|---|
| 타입 체크 | 화면과 API 사이에서 주고받는 데이터의 타입이 맞는가 | 실행 전에 연결 실수를 찾아냄 |
| ... |
처음 3개의 층은 에이전트가 고속으로 회전할 수 있는 로컬 게이트(Local gate)입니다. 로컬 E2E에서는 타입이 지정된 클라이언트(Typed client)로부터 API, 인증, Block의 로컬 mock까지 통과하며 애플리케이션의 동작을 검증합니다.
단, 로컬 mock은 실제 AWS의 에뮬레이터가 아닙니다. IAM, DynamoDB의 키 정의, 서로 다른 도메인 간의 Cookie, CloudFormation에 의한 생성과 삭제는 로컬의 3개 층이 모두 Green이 된 후, 필요한 경우에만 AWS Sandbox에서 확인합니다.
실제로 한 일
AWS Blocks 공식 리포지토리(Repository)에서 제공되는 bare 템플릿을 출발점으로 삼았습니다. DevelopersIO의 「AWS Blocks 워크숍을 만들어 보았다」를 참고하여 인증, 데이터 저장, 프론트엔드를 추가하고, 사용자별로 데이터가 분리된 최소한의 풀스택 앱으로 키워 나갔습니다.
각 기능을 추가할 때마다 다음 사이클을 돌렸습니다.
기대하는 동작을 테스트에 작성 (Red)
↓
테스트가 의도한 이유로 실패하는지 확인
...
여기서부터는 이 사이클과 4단계 검증을 실제 앱 개발에서 어떻게 돌렸는지 되돌아보겠습니다.
greet()
API부터 시작하기
템플릿의 첫 번째 프로젝트에는 백엔드 API가 하나만 들어 있습니다.
const scope = new Scope('my-app');
export const api = new ApiNamespace(scope, 'api', (context) => ({
async greet(name: string) {
...
ApiNamespace에 작성한 메서드가 그대로 타입이 지정된 (typed) API가 됩니다. 프론트엔드에서는 import { api } from 'aws-blocks'를 통해 일반 함수처럼 호출할 수 있으므로, HTTP 요청을 수동으로 구성할 필요가 없습니다. npm run dev를 통해 브라우저에 "Hello, World!"가 나타나는지, npm run test:e2e를 통해 동일한 동작이 자동 테스트에서도 통과하는지 확인하며 여기서부터 시작했습니다.

공식 bare 템플릿을 실행한 첫 화면
여기에 인증 Block (AuthBasic), 데이터 저장 Block (DistributedTable), 입력 검증 라이브러리 (Zod)를 추가하여, 사용자별로 데이터가 분리된 아이템 관리 앱으로 만들어 나갑니다.
타입 체크
기능을 추가할 때마다 우선 npm run typecheck를 실행했습니다. 특히 효과적이었던 것은 테스트를 먼저 작성하여 의도적으로 타입 에러를 일으키는 방식입니다.
예를 들어 인증을 추가할 때, 백엔드에 authApi를 만들기 전에 테스트 측에서 authApi.setAuthState(...)를 호출하는 코드를 작성합니다. 당연히 npm run typecheck는 "authApi가 export 되어 있지 않다"며 에러를 발생시킵니다. 이는 의도한 대로의 실패입니다. 테스트 작성 방식이 틀린 것이 아니라, "아직 만들지 않은 기능을 테스트가 기다리고 있음"을 확인하는 것입니다.

authApi가 미구현 상태이므로, 타입 체크가 의도한 이유로 실패함
백엔드에 인증 Block을 몇 줄 추가하고 authApi를 export 하면, 개발 서버가 클라이언트 코드를 재생성하여 타입 에러가 사라집니다.
const auth = new AuthBasic(scope, 'auth', {
passwordPolicy: { minLength: 8 },
crossDomain: process.env.BLOCKS_SANDBOX === 'true',
...
createItem이나 listItems에서도 같은 과정을 반복했습니다. 테스트에 작성한 API 이름이 아직 백엔드에 없을 때는 타입 에러로 인해 Red 상태가 되고, 구현하여 export 하면 Green 상태가 됩니다. 이 사이클만으로도 프론트엔드, 테스트, 백엔드 간의 연결 실수를 실행 전에 찾아낼 수 있습니다.

authApi 구현 후, 인증 E2E와 타입 체크가 Green이 됨
단위 테스트 (Unit Test)
아이템의 제목에는 "공백만 있으면 거부", "200자까지 OK"와 같은 규칙이 있습니다. 이러한 값의 제약 사항은 API나 DB를 실행하지 않고도 검증할 수 있으므로, Zod 스키마와 단위 테스트로 분리했습니다.
export const titleSchema = z.string().trim().min(1).max(200);
TypeScript의 string 타입은 컴파일 시점의 검사일 뿐이므로, 브라우저에서 전달된 값이 공백뿐이거나 201자여도 그대로 통과되어 버립니다. Zod는 이를 런타임(runtime)에 체크해 주는 라이브러리입니다. .trim()으로 앞뒤 공백을 제거한 뒤 .min(1)로 빈 문자열을 걸러내므로, " "와 같은 입력도 제대로 거부할 수 있습니다.
입력 스키마에는 "' Buy milk ' → 'Buy milk'로 정규화된다", "' '는 거부된다", "200자는 통과하고 201자는 거부한다"라는 3가지 테스트를 작성했습니다.
또한, 프런트엔드(Frontend) 화면 로직에는 "목록을 화면에 반영하기", "생성 후 목록을 다시 가져오기", "빈 제목을 API로 보내지 않기", "API 에러 후에도 재전송 가능하기"라는 4건을 추가했습니다. 인증이나 데이터베이스를 실행하지 않고도, 총 7건을 npm run test:unit만으로 순식간에 확인할 수 있습니다.

입력 스키마 3건과 화면 로직 4건, 총 7건의 단위 테스트 (Unit Test)
로컬 E2E
타입(Type)이 맞고 규칙도 올바르더라도, "인증 → API → Block의 로컬 모의 객체 (Mock)"를 통해 실행하면 다른 문제가 발생합니다. 이 부분을 커버하는 것이 로컬 E2E입니다.
먼저 인증만을 위한 E2E를 작성했습니다. "로그아웃 상태 확인 → 사용자 등록 → 로그인 → signedIn 상태임을 단언 (Assert) → 로그아웃"이라는 흐름입니다. 인증 쿠키 (Cookie)를 테스트 간에 전달하기 위해 installCookieJar()를 사용하였고, 사용자 이름은 매번 무작위로 생성하여 재실행에 대비했습니다.
다음으로 인증, 입력 검증, 데이터 저장을 연결한 API를 만들었습니다. 구현에서 가장 중요하게 생각한 점은 클라이언트로부터 오직 "제목"만 받는 것입니다.
async createItem(title: string) {
const user = await auth.requireAuth(context); // ← 로그인되어 있지 않으면 여기서 차단
const validTitle = titleSchema.parse(title); // ← 빈 문자열이나 너무 긴 제목은 여기서 차단
...
"누가 만들었는지", "언제 만들었는지", "아이템의 ID"는 모두 서버 측에서 결정합니다. 만약 클라이언트가 userId를 보내도록 설계하면, Alice가 "bob"이라고 지정하여 Bob의 데이터를 만들 수 있게 됩니다. 애초에 해당 입력란을 없애버리면 사칭 경로 자체가 존재하지 않습니다. 목록 가져오기도 마찬가지로, 백엔드(Backend)가 처음부터 로그인 중인 사용자의 데이터만 반환하도록 설계했습니다.
E2E 테스트에서는 Alice와 Bob을 하나의 시나리오 안에서 전환하며, "로그인하지 않으면 생성할 수 없다", "Alice의 아이템은 Alice에게만 보인다", "Bob에게는 Alice의 아이템이 보이지 않는다", "제목의 공백 제거 및 글자 수 제한이 API 레벨에서 적용된다"와 같은 동작을 종합적으로 검증했습니다.

인증, 저장, 사용자 분리를 일괄적으로 확인하는 로컬 API E2E
프런트엔드도 구축하고, Playwright를 사용하여 브라우저 E2E를 추가했습니다. 흥미로웠던 점은, 첫 실행 시 화면은 127.0.0.1인데 API는 localhost를 호출하고 있어 쿠키가 전송되지 않아 로그인 상태가 전달되지 않았던 문제입니다. 로그인 화면이 뜨는지 여부만 테스트했다면 알아채지 못했겠지만, "로그인하여 아이템을 생성하기"까지 테스트하고 있었던 덕분에 개발 중에 이를 감지할 수 있었습니다.

쿠키가 전송되지 않아 로그인 후 아이템 생성에 실패했을 때의 화면
수정 후에는 Alice로 생성한 데이터가 새로고침 후에도 남아 있고, Bob으로 전환하면 표시되지 않는 것까지 브라우저를 통해 확인할 수 있었습니다.

Alice로 생성한 데이터는 새로고침 후에도 표시됨

Bob으로 전환하면 Alice의 데이터는 표시되지 않음
여기까지의 로컬 3계층을 타입 체크, 단위 테스트 7건, API E2E 3건, 브라우저 E2E 1건의 4개 명령어로 확인했습니다. 모두 Green(성공) 상태가 된 시점에서 로컬 피드백 루프는 완성됩니다.
Sandbox E2E
마지막으로, 동일한 테스트를 실제 AWS를 대상으로 실행했습니다. npm run sandbox를 실행하면 CloudFormation이 API Gateway, Lambda, DynamoDB 테이블 등 약 50개의 리소스를 한꺼번에 만들어 줍니다. 클라이언트도 자동으로 AWS URL로 전환되므로, 테스트 코드를 전혀 수정하지 않고 그대로 실행할 수 있었습니다.

npm run sandbox로 CloudFormation 변경 세트(Change Set)를 생성하는 모습

API Gateway, Lambda, DynamoDB 등 실제로 생성된 리소스
여기서 확인하고 싶었던 것은 로컬에서는 재현할 수 없는 "AWS만의" 부분입니다. Lambda에 적절한 권한이 부여되어 있는지, DynamoDB의 키 설계가 실제 테이블에 반영되어 있는지, 로컬 화면과 AWS API 사이에서 Cookie가 제대로 주고받아지는지 등입니다. 이러한 점들을 로컬과 동일한 테스트로 확인했습니다.
이번에 로컬 E2E (End-to-End) 테스트는 약 4초 만에 완료되었습니다. 반면, Sandbox는 테스트를 시작하기 전 배포(Deploy)만으로 약 2분이 소요되었습니다. 변경할 때마다 AWS에 배포하고 결과를 기다린다면 시도 횟수를 늘릴 수 없습니다. 먼저 로컬의 3계층(3-tier)에서 결함을 해결하고, AWS에서는 고유한 차이점만 확인합니다. 이 순서를 따르는 의미를 대기 시간의 차이를 통해 실감했습니다.
CloudWatch Logs도 확인했습니다. 테스트가 의도적으로 발생시킨 "미인증 액세스 거부"나 "부정확한 타이틀의 유효성 검사(Validation) 에러"가 요청 ID(Request ID)와 함께 기록되어 있습니다. AWS 상에서 무엇이 일어났는지 로그를 통해 추적할 수 있다는 것을 실제로 눈으로 확인한 것은 좋은 경험이었습니다.
검증 후에는 npm run sandbox:destroy를 통해 AWS 리소스를 삭제했습니다. 다만, Lambda가 실행 시 자동으로 생성한 CloudWatch 로그 그룹이 하나 남아 있는 것을 발견했습니다. CloudFormation의 삭제가 완료되어도 스택(Stack) 외부에서 생성된 리소스까지는 지워주지 않습니다. 스택 이름으로 검색하여 수동으로 삭제하고, 남은 것이 없는지 확인했습니다. "지웠다고 생각하는 것"에서 끝내지 않고, 제대로 확인하는 운영이 필요합니다.

검증 후에는 Sandbox를 삭제하고, 잔존 리소스도 확인했다
에이전트를 늘리기 전에, 검증할 수 있는 범위를 넓히기
여러 에이전트를 병렬로 구동하기 위해서는 생성 속도보다 먼저, 변경의 좋고 나쁨을 에이전트 스스로 판단할 수 있는 환경이 필요했습니다.
이번 구성에서는 타입 체크(Type Check), 단위 테스트(Unit Test), 로컬 E2E를 짧은 루프로 돌리고, IAM이나 DynamoDB, Cookie 등 AWS 고유의 경계 부분만을 Sandbox에서 확인했습니다. AWS Blocks를 사용하면 동일한 백엔드 정의와 타입이 지정된 API (Typed API)를 유지하면서 로컬과 Sandbox의 검증을 연결할 수 있습니다.
Parallel(병렬화)로 나아가는 한 걸음은 에이전트의 수를 늘리는 것이 아니라, 우선 "맡긴 변경 사항을 스스로 확인할 수 있는 범위"를 넓히는 것이었습니다.
참고
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기