개발자를 위한 구글 안티그래비티 UI 단위 테스트 완벽 가이드
React 기반 웹 개발
서론: AI 에이전트 시대의 테스팅 패러다임
“테스트를 작성하는 데 코드를 작성하는 것보다 더 많은 시간이 걸린다.” 많은 개발자들이 경험하는 이 딜레마는 2026년 현재, 구글 안티그래비티(Google Antigravity)의 등장으로 근본적인 변화를 맞이하고 있다. 안티그래비티는 단순히 코드를 자동완성해주는 도구가 아니라, 테스트 계획부터 작성, 실행, 검증까지 전체 테스팅 워크플로우를 자율적으로 수행하는 AI 에이전트 플랫폼이다.
React 애플리케이션에서 UI 단위 테스트를 작성하는 것은 여전히 중요하지만, 이제 개발자는 “어떻게 테스트를 작성할 것인가”가 아니라 “어떤 테스트가 필요한가”를 정의하는 설계자(Architect)의 역할로 전환되고 있다. 이 가이드는 안티그래비티를 활용하여 React 애플리케이션의 UI 단위 테스트를 효과적으로 자동화하고 관리하는 실전 방법을 제시한다.
안티그래비티란 무엇인가: 에이전트 우선(Agent-First) 개발 플랫폼
기존 AI 코딩 도구와의 근본적 차이
GitHub Copilot이나 초기 버전의 Cursor는 개발자가 작성하는 코드를 실시간으로 보조하는 “자동완성 도구”였다. 개발자는 여전히 모든 결정을 내리고, 모든 파일을 직접 열어보고, 테스트를 수동으로 실행해야 했다. 반면 안티그래비티는 완전히 다른 접근 방식을 취한다.
안티그래비티는 Visual Studio Code를 기반으로 만들어졌지만, 그 내부는 완전히 에이전트 중심으로 재설계되었다. 하나의 AI 어시스턴트 대신, 여러 전문화된 AI 에이전트들이 팀처럼 협력하여 작업을 수행한다. Browser Agent는 웹사이트를 실제로 열어 UI를 테스트하고 스크린샷을 찍는다. Terminal Agent는 npm install, 테스트 실행, Git 명령어 등을 대신 수행한다. File System Agent는 프로젝트 전체의 파일을 일관성 있게 생성하고 수정한다. Code Analysis Agent는 전체 코드베이스를 이해하고 버그를 찾아내며 최적화를 제안한다.
개발자는 더 이상 “이 컴포넌트의 테스트를 작성해줘”라고 요청한 후 생성된 코드를 복사-붙여넣기하지 않는다. 대신 “로그인 컴포넌트에 대한 완전한 테스트 스위트를 작성하고, 모든 엣지 케이스를 커버하고, 실제로 실행해서 통과하는지 확인해줘”라고 지시하면, 에이전트가 계획을 세우고, 테스트를 작성하고, 실행하고, 실패하면 수정하고, 최종적으로 통과하는 테스트를 제공한다.
두 가지 핵심 인터페이스
안티그래비티는 두 가지 주요 작업 모드를 제공한다.
Editor View(편집기 뷰) 는 익숙한 IDE 환경이다. VS Code처럼 코드를 직접 작성할 수도 있고, 사이드바의 에이전트 패널을 통해 AI에게 자연어로 지시를 내릴 수도 있다. “LoginForm.test.jsx를 작성해줘”라고 입력하면, 에이전트가 코드를 작성하고 변경사항을 커밋하기 전에 승인을 요청한다. 이는 한 명의 개발자와 AI 어시스턴트가 페어 프로그래밍하는 것과 유사하다.
Manager View(관리자 뷰) 는 안티그래비티의 진정한 혁신이 드러나는 곳이다. 이것은 Mission Control이라고도 불리며, 여러 에이전트를 동시에 관리하는 대시보드다. 하나의 에이전트는 백엔드 API 리팩토링을 진행하고, 다른 에이전트는 프론트엔드 CSS 버그를 디버깅하고, 세 번째 에이전트는 단위 테스트를 생성하는 식으로 최대 5개의 에이전트를 병렬로 실행할 수 있다. 각 에이전트의 진행 상황, 생성한 Artifacts(계획서, 스크린샷, 녹화 영상), 그리고 상태(작업 중, 승인 대기, 실패)를 한눈에 볼 수 있다.
이는 마치 여러 명의 주니어 개발자로 구성된 팀을 관리하는 것과 같다. 개발자는 각 에이전트에게 명확한 목표를 부여하고, 그들의 작업을 검토하고, 필요시 방향을 수정하는 PM 또는 테크 리드의 역할을 수행한다.
2026년 현재 무료 제공
안티그래비티는 현재 Public Preview 단계로 완전히 무료로 제공되고 있다. 특히 주목할 만한 점은 Claude Opus 4.5(Thinking) 모델에 대한 접근을 제공한다는 것이다. 이 모델은 SWE-bench에서 80.9%의 정확도를 기록하며, 일반적으로 월 $100-200를 지불해야 사용할 수 있는 최고 수준의 코딩 AI다. 여기에 Gemini 3 Pro(High/Low 변형), Gemini 3 Flash, Claude Sonnet 4.5 Thinking 모드, GPT-OSS 120B까지 선택할 수 있다.
다만 무료 사용자는 사용량 제한이 있다. Google은 정확한 제한을 공개하지 않았지만, 실제 사용자들의 보고에 따르면 집중적으로 코딩할 경우 2-3시간 후 제한에 도달한다고 한다. 사용량은 에이전트가 수행한 “작업량”과 연관되어 있어, 단순한 작업은 할당량을 적게 소비하고 복잡한 추론 작업은 더 많이 소비한다. 헤비 유저라면 월 $20의 AI Pro 플랜을 고려해야 하지만, 테스트와 중간 정도의 사용에는 무료 티어도 충분히 관대하다.
React 프로젝트 설정: 안티그래비티와 테스팅 환경
시스템 요구사항 및 설치
안티그래비티는 macOS, Windows, Linux를 지원한다. 하지만 단순한 텍스트 에디터가 아니라 로컬에서 AI 모델을 실행하고 브라우저 기반 에이전트를 구동하는 무거운 애플리케이션이므로 충분한 시스템 리소스가 필요하다.
macOS의 경우 Monterey(버전 12) 이상이 필요하다. Intel Mac도 지원되지만 Apple Silicon(M1/M2/M3/M4)에서 최적화되어 있어 훨씬 빠른 성능을 제공한다. 통합 메모리 아키텍처가 AI 추론에 큰 도움이 되기 때문이다.
Windows의 경우 64비트 Windows 10 또는 11이 필요하다. AI 에이전트가 터미널 명령을 실행할 때 권한 문제를 피하기 위해 주 시스템 드라이브(일반적으로 C:)에 설치하는 것을 강력히 권장한다. 일부 사용자들은 초기 Windows 빌드에서 간헐적인 크래시를 보고했으나, 최근 업데이트에서 안정성이 크게 개선되었다.
Chrome 브라우저 설치도 필수다. 안티그래비티의 Browser Agent는 Chrome(또는 호환 브라우저)을 통해 UI 테스팅과 자동화된 워크플로우를 실행한다. 또한 현재는 개인 Gmail 계정이 필요하며, 이를 통해 프리뷰 버전에 접근할 수 있다.
설치는 간단하다. antigravity.google/download에서 운영체제에 맞는 설치 파일을 다운로드하고 실행한 후, 첫 실행 시 Google 계정으로 로그인하면 된다. 로그인 후에는 기본 AI 모델을 선택하고 테마를 설정할 수 있다.
워크스페이스 구성 및 보안 설정
처음 안티그래비티를 실행하면 워크스페이스를 설정해야 한다. 기존 Git 리포지토리를 가져올 수도 있고, 완전히 새로운 프로젝트를 시작할 수도 있다. React 프로젝트의 경우, Vite나 Create React App으로 생성한 기존 프로젝트를 그대로 가져오면 된다.
중요한 보안 설정이 있다. AI 에이전트들은 매우 강력한 권한을 가질 수 있으며, 실수로 rm -rf / 같은 치명적인 명령어를 실행할 수도 있다. 실제로 일부 사용자들이 하드 드라이브가 포맷되는 사고를 경험했다. Google도 이를 인지하고 있어, 기본 설정에서는 .scratch 폴더 외에는 파일 접근을 차단하도록 되어 있다.
그러나 테스트를 자동으로 생성하고 실행하려면 프로젝트 폴더에 대한 접근 권한이 필요하다. 이를 위해 다음과 같이 설정한다:
- 우측 상단의 Settings(⚙️) 아이콘 클릭
- Editor-Specific Settings → Open antigravity user settings
- Agent 섹션에서 FILE ACCESS 항목을 프로젝트 폴더로 제한
- Terminal Execution Policy를 “Review”로 설정하여 모든 터미널 명령을 승인 후 실행하도록 설정
이렇게 하면 에이전트가 프로젝트 내에서 자유롭게 작업할 수 있지만, 위험한 명령은 실행 전에 검토할 수 있다. 프로덕션 환경이나 중요한 데이터가 있는 컴퓨터에서는 가상 머신이나 샌드박스 환경에서 사용하는 것을 강력히 권장한다.
React Testing 환경 초기 구성
안티그래비티는 기존 프로젝트의 구조를 자동으로 인식하지만, React 테스팅 환경을 제대로 활용하려면 몇 가지 도구가 설치되어 있어야 한다.
Vite 기반 프로젝트를 사용한다면 Vitest가 가장 자연스러운 선택이다. Vitest는 Vite의 빠른 HMR(Hot Module Replacement)과 네이티브 ESM 지원을 활용하여 기존 Jest보다 10-20배 빠른 테스트 실행 속도를 제공한다. 2026년 현재, React 생태계에서 Vitest는 빠르게 표준으로 자리잡고 있다.
Jest는 여전히 강력한 선택지다. 특히 React Native 프로젝트나 레거시 코드베이스에서는 Jest가 더 안정적이다. Jest 30(2025년 중반 출시)은 성능과 TypeScript 지원이 크게 개선되었으며, 엔터프라이즈 환경에서 검증된 도구다.
React Testing Library(RTL) 는 두 경우 모두 필수다. RTL은 사용자가 실제로 애플리케이션과 상호작용하는 방식으로 컴포넌트를 테스트하도록 설계되었다. 구현 세부사항이 아니라 사용자 관점에서 테스트를 작성하게 유도하므로, 리팩토링에도 강건한 테스트를 만들 수 있다.
안티그래비티에게 다음과 같이 요청하면 자동으로 환경을 구성해준다:
1
2
3
프로젝트에 Vitest와 React Testing Library 테스팅 환경을 설정해줘.
jsdom 환경을 사용하고, @testing-library/jest-dom의 커스텀 매처도 포함해줘.
그리고 vitest.config.ts 파일을 생성해서 globals: true로 설정해줘.
에이전트는 Planning 모드로 전환하여 다음과 같은 작업 계획을 제시한다:
- 필요한 패키지 설치 (vitest, @testing-library/react, @testing-library/jest-dom, jsdom)
- vitest.config.ts 파일 생성
- 테스트 셋업 파일 (vitest-setup.ts) 생성
- package.json에 테스트 스크립트 추가
계획을 승인하면 에이전트가 자동으로 모든 작업을 수행한다. 터미널에서 npm install 명령을 실행하고, 설정 파일들을 생성하고, 모든 것이 제대로 작동하는지 테스트 명령으로 확인까지 한다.
Workflow 시스템: 테스트 표준 정의하기
Workflow란 무엇인가
안티그래비티의 가장 강력한 기능 중 하나는 Workflow 시스템이다. Workflow는 에이전트가 따라야 할 “표준 작업 절차(Standard Operating Procedure)”를 정의하는 것이다. 예를 들어 “단위 테스트를 생성할 때는 항상 AAA 패턴을 따르고, 모든 엣지 케이스를 커버하고, 접근성 테스트를 포함하라”는 규칙을 Workflow로 만들 수 있다.
Workflow가 없으면 매번 테스트를 요청할 때마다 긴 프롬프트로 모든 요구사항을 설명해야 한다. Workflow를 정의하면 /generate-unit-tests라는 짧은 명령만으로 팀의 테스팅 표준에 맞는 테스트를 자동으로 생성할 수 있다.
이는 팀 내에서 일관된 코드 품질을 유지하는 데 매우 효과적이다. 시니어 개발자가 정의한 테스트 표준을 Workflow로 만들어두면, 모든 팀원이 동일한 품질의 테스트를 생성할 수 있다. 또한 Workflow는 버전 관리가 가능하므로, Git 리포지토리에 커밋하여 팀 전체가 공유할 수 있다.
React 컴포넌트 테스트 Workflow 만들기
실제 React 프로젝트에서 사용할 수 있는 Workflow를 만들어보자. 이 Workflow는 React 컴포넌트에 대한 포괄적인 단위 테스트를 생성한다.
안티그래비티 우측 상단의 채팅 패널에서 ... (More) 메뉴를 클릭하고 “Customizations”를 선택한다. “Workflows” 탭으로 이동하여 “+ Workspace”를 클릭하면 새 Workflow 파일이 생성된다.
다음 내용을 붙여넣고 저장한다(Cmd+S / Ctrl+S):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
name: generate-react-component-tests
description: React 컴포넌트에 대한 포괄적인 단위 테스트를 생성합니다 (RTL + Vitest)
# 목표
React 컴포넌트에 대한 완전한 테스트 스위트를 생성하여
사용자 관점의 동작, 엣지 케이스, 접근성을 모두 검증합니다.
# 단계
1. **분석**: 선택된 컴포넌트의 props, state, 이벤트 핸들러, 조건부 렌더링을 파악
2. **계획**: 테스트할 시나리오 목록 작성
- Happy Path (정상 동작)
- Edge Cases (빈 데이터, null, undefined, 극단값)
- User Interactions (클릭, 입력, 폼 제출)
- Accessibility (ARIA 레이블, 키보드 네비게이션)
3. **구현**: React Testing Library를 사용하여 테스트 작성
4. **실행**: 테스트를 실행하고 모두 통과하는지 확인
5. **보고**: 테스트 커버리지와 발견된 이슈 보고
# 규칙
- 모든 테스트는 AAA (Arrange-Act-Assert) 패턴 사용
- `screen` 쿼리 사용 (getByRole, getByLabelText 우선)
- 구현 세부사항이 아닌 사용자 동작 테스트
- `data-testid`는 최후의 수단으로만 사용
- 비동기 작업은 `waitFor`, `findBy*` 사용
- 각 테스트는 독립적이어야 함 (beforeEach로 초기화)
- Mock은 필요한 경우에만 최소한으로 사용
# 출력 형식
- 파일명: `[ComponentName].test.tsx`
- 테스트 스위트는 `describe` 블록으로 그룹화
- 각 테스트는 명확한 설명: "should [expected behavior] when [condition]"
- 주요 테스트에는 간단한 주석 추가
이제 이 Workflow를 사용할 수 있다. React 컴포넌트 파일을 열고 채팅에 /generate-react-component-tests를 입력하면, 에이전트가 위의 표준에 따라 자동으로 테스트를 생성한다.
실전 예제: LoginForm 컴포넌트 테스트 생성
다음과 같은 LoginForm.tsx 컴포넌트가 있다고 가정하자:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
interface LoginFormProps {
onSubmit: (email: string, password: string) => Promise<void>;
isLoading?: boolean;
}
export function LoginForm({ onSubmit, isLoading = false }: LoginFormProps) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!email || !password) {
setError('이메일과 비밀번호를 입력해주세요');
return;
}
try {
await onSubmit(email, password);
} catch (err) {
setError('로그인에 실패했습니다');
}
};
return (
<form onSubmit={handleSubmit} aria-label="로그인 폼">
{error && <div role="alert">{error}</div>}
<label htmlFor="email">이메일</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={isLoading}
/>
<label htmlFor="password">비밀번호</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={isLoading}
/>
<button type="submit" disabled={isLoading}>
{isLoading ? '로그인 중...' : '로그인'}
</button>
</form>
);
}
LoginForm.tsx 파일을 열고 채팅에서 다음과 같이 입력한다:
1
/generate-react-component-tests
에이전트는 먼저 컴포넌트를 분석하고 Planning Artifact를 생성한다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
## LoginForm 테스트 계획
### 컴포넌트 분석
- Props: onSubmit (비동기 함수), isLoading (boolean)
- State: email, password, error
- 이벤트: form submit, input changes
- 조건부 렌더링: error 메시지, 로딩 상태
### 테스트 시나리오
1. 렌더링
- 모든 폼 요소가 올바르게 렌더링되는지
- 초기 상태가 비어있는지
2. 사용자 입력
- 이메일 입력이 작동하는지
- 비밀번호 입력이 작동하는지
3. 폼 제출 - Happy Path
- 유효한 데이터로 제출 시 onSubmit이 호출되는지
4. 유효성 검증
- 빈 이메일로 제출 시 에러 메시지 표시
- 빈 비밀번호로 제출 시 에러 메시지 표시
5. 로딩 상태
- isLoading이 true일 때 버튼 텍스트 변경
- isLoading이 true일 때 입력 필드 비활성화
6. 에러 처리
- onSubmit 실패 시 에러 메시지 표시
7. 접근성
- 폼이 올바른 ARIA 레이블을 가지는지
- 에러 메시지가 role="alert"를 가지는지
계획을 승인하면 에이전트가 LoginForm.test.tsx 파일을 생성한다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';
describe('LoginForm', () => {
let mockOnSubmit: ReturnType<typeof vi.fn>;
let user: ReturnType<typeof userEvent.setup>;
beforeEach(() => {
mockOnSubmit = vi.fn().mockResolvedValue(undefined);
user = userEvent.setup();
});
describe('Rendering', () => {
it('should render all form elements', () => {
render(<LoginForm onSubmit={mockOnSubmit} />);
expect(screen.getByLabelText('이메일')).toBeInTheDocument();
expect(screen.getByLabelText('비밀번호')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '로그인' })).toBeInTheDocument();
});
it('should have empty initial values', () => {
render(<LoginForm onSubmit={mockOnSubmit} />);
const emailInput = screen.getByLabelText('이메일') as HTMLInputElement;
const passwordInput = screen.getByLabelText('비밀번호') as HTMLInputElement;
expect(emailInput.value).toBe('');
expect(passwordInput.value).toBe('');
});
});
describe('User Input', () => {
it('should update email input when user types', async () => {
render(<LoginForm onSubmit={mockOnSubmit} />);
const emailInput = screen.getByLabelText('이메일');
await user.type(emailInput, 'test@example.com');
expect(emailInput).toHaveValue('test@example.com');
});
it('should update password input when user types', async () => {
render(<LoginForm onSubmit={mockOnSubmit} />);
const passwordInput = screen.getByLabelText('비밀번호');
await user.type(passwordInput, 'password123');
expect(passwordInput).toHaveValue('password123');
});
});
describe('Form Submission - Happy Path', () => {
it('should call onSubmit with email and password when form is submitted', async () => {
render(<LoginForm onSubmit={mockOnSubmit} />);
// Arrange: 사용자가 폼을 작성
await user.type(screen.getByLabelText('이메일'), 'test@example.com');
await user.type(screen.getByLabelText('비밀번호'), 'password123');
// Act: 폼 제출
await user.click(screen.getByRole('button', { name: '로그인' }));
// Assert: onSubmit이 올바른 인자로 호출됨
expect(mockOnSubmit).toHaveBeenCalledTimes(1);
expect(mockOnSubmit).toHaveBeenCalledWith('test@example.com', 'password123');
});
});
describe('Validation', () => {
it('should show error when email is empty', async () => {
render(<LoginForm onSubmit={mockOnSubmit} />);
// 비밀번호만 입력하고 제출
await user.type(screen.getByLabelText('비밀번호'), 'password123');
await user.click(screen.getByRole('button', { name: '로그인' }));
// 에러 메시지가 표시되어야 함
expect(await screen.findByRole('alert')).toHaveTextContent(
'이메일과 비밀번호를 입력해주세요'
);
expect(mockOnSubmit).not.toHaveBeenCalled();
});
it('should show error when password is empty', async () => {
render(<LoginForm onSubmit={mockOnSubmit} />);
// 이메일만 입력하고 제출
await user.type(screen.getByLabelText('이메일'), 'test@example.com');
await user.click(screen.getByRole('button', { name: '로그인' }));
expect(await screen.findByRole('alert')).toHaveTextContent(
'이메일과 비밀번호를 입력해주세요'
);
expect(mockOnSubmit).not.toHaveBeenCalled();
});
});
describe('Loading State', () => {
it('should show "로그인 중..." when isLoading is true', () => {
render(<LoginForm onSubmit={mockOnSubmit} isLoading={true} />);
expect(screen.getByRole('button', { name: '로그인 중...' })).toBeInTheDocument();
});
it('should disable all inputs when isLoading is true', () => {
render(<LoginForm onSubmit={mockOnSubmit} isLoading={true} />);
expect(screen.getByLabelText('이메일')).toBeDisabled();
expect(screen.getByLabelText('비밀번호')).toBeDisabled();
expect(screen.getByRole('button')).toBeDisabled();
});
});
describe('Error Handling', () => {
it('should show error message when onSubmit fails', async () => {
const mockOnSubmitError = vi.fn().mockRejectedValue(new Error('Login failed'));
render(<LoginForm onSubmit={mockOnSubmitError} />);
await user.type(screen.getByLabelText('이메일'), 'test@example.com');
await user.type(screen.getByLabelText('비밀번호'), 'password123');
await user.click(screen.getByRole('button', { name: '로그인' }));
expect(await screen.findByRole('alert')).toHaveTextContent(
'로그인에 실패했습니다'
);
});
});
describe('Accessibility', () => {
it('should have accessible form label', () => {
render(<LoginForm onSubmit={mockOnSubmit} />);
expect(screen.getByRole('form', { name: '로그인 폼' })).toBeInTheDocument();
});
it('should announce errors to screen readers', async () => {
render(<LoginForm onSubmit={mockOnSubmit} />);
await user.click(screen.getByRole('button', { name: '로그인' }));
const alert = await screen.findByRole('alert');
expect(alert).toBeInTheDocument();
});
});
});
에이전트는 테스트를 작성한 후 자동으로 실행한다:
1
Terminal Agent: $ npm run test LoginForm.test.tsx
모든 테스트가 통과하면 Walkthrough Artifact를 생성하여 다음 정보를 제공한다:
- 생성된 테스트 수: 14개
- 커버리지: 100% (모든 분기 포함)
- 실행 시간: 1.2초
- 발견된 이슈: 없음
만약 테스트가 실패하면 에이전트가 자동으로 문제를 분석하고 수정한다. 예를 들어 실제 컴포넌트에 버그가 있어서 테스트가 실패하면, 에이전트가 버그를 찾아내고 수정 방법을 제안한다.
Workflow 고급 활용: 커스텀 규칙 추가
팀마다 고유한 테스트 표준이 있을 수 있다. 예를 들어 특정 회사에서는 모든 폼 컴포넌트가 Formik을 사용하고, 에러 처리는 React Query의 mutation을 통해 이루어진다면, 이를 Workflow에 반영할 수 있다.
1
2
3
4
5
6
# 추가 규칙 섹션
- Formik을 사용하는 경우 `formik.submitForm()`을 테스트
- React Query mutation을 사용하는 경우 `useMutation` mock 제공
- 모든 API 호출은 MSW(Mock Service Worker)로 mock
- 스냅샷 테스트는 사용하지 않음 (유지보수 부담)
- 테스트 커버리지 목표: 구문 90%, 분기 85% 이상
이렇게 하면 에이전트가 항상 팀의 표준에 맞는 테스트를 생성한다.
Browser Agent: 자동화된 UI 검증
Browser Agent의 작동 원리
안티그래비티의 Browser Agent는 단순히 코드를 생성하는 것을 넘어, 실제로 애플리케이션을 브라우저에서 실행하고 테스트한다. 이는 Playwright나 Cypress 같은 E2E 테스팅 도구와 유사하지만, 모든 것이 자동화되어 있다는 점이 다르다.
에이전트에게 “LoginForm 컴포넌트가 제대로 작동하는지 확인해줘”라고 요청하면, 다음과 같은 일이 일어난다:
- Terminal Agent가 개발 서버를 시작한다 (
npm run dev) - Browser Agent가 Chrome을 열고 해당 페이지로 이동한다
- 실제 사용자처럼 폼에 데이터를 입력한다
- 제출 버튼을 클릭한다
- 결과를 확인하고 스크린샷을 찍는다
- 전체 과정을 비디오로 녹화한다
이 모든 과정은 몇 초 안에 이루어지며, 결과는 Artifact로 저장되어 나중에 검토할 수 있다.
실전 예제: 로그인 플로우 통합 테스트
단위 테스트만으로는 실제 사용자 경험을 완전히 검증할 수 없다. 통합 테스트나 E2E 테스트가 필요한데, Browser Agent를 활용하면 이를 자동화할 수 있다.
다음과 같이 요청한다:
1
2
3
4
5
6
7
Browser Agent를 사용해서 로그인 플로우를 검증해줘:
1. /login 페이지로 이동
2. 이메일에 "test@example.com" 입력
3. 비밀번호에 "password123" 입력
4. "로그인" 버튼 클릭
5. 대시보드 페이지로 리다이렉트되는지 확인
6. 전체 과정을 스크린샷과 비디오로 기록해줘
에이전트가 Planning Artifact를 생성한다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
## 로그인 플로우 검증 계획
### 준비
- 개발 서버 시작 (포트 5173 확인)
- 브라우저 자동화 설정
- 테스트 계정 확인
### 실행 단계
1. 브라우저에서 http://localhost:5173/login 열기
2. 이메일 입력 필드 찾기 및 입력
3. 비밀번호 입력 필드 찾기 및 입력
4. 로그인 버튼 클릭
5. URL 변경 대기 (리다이렉트 확인)
6. 대시보드 요소 확인
7. 각 단계마다 스크린샷 촬영
### 성공 기준
- 로그인 후 URL이 /dashboard로 변경됨
- 대시보드에 사용자 이름이 표시됨
- 에러 메시지가 나타나지 않음
에이전트는 실제로 다음과 같은 Playwright 스크립트를 내부적으로 생성하고 실행한다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { chromium } from 'playwright';
async function testLoginFlow() {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
// Step 1: 로그인 페이지로 이동
await page.goto('http://localhost:5173/login');
await page.screenshot({ path: '01-login-page.png' });
// Step 2: 폼 작성
await page.fill('[id="email"]', 'test@example.com');
await page.fill('[id="password"]', 'password123');
await page.screenshot({ path: '02-form-filled.png' });
// Step 3: 제출
await page.click('button[type="submit"]');
// Step 4: 리다이렉트 대기
await page.waitForURL('**/dashboard');
await page.screenshot({ path: '03-dashboard.png' });
// Step 5: 검증
const userName = await page.textContent('[data-testid="user-name"]');
console.log(`✓ 로그인 성공: ${userName}`);
await browser.close();
}
실행이 완료되면 Walkthrough Artifact에 다음이 포함된다:
- 비디오 녹화: 전체 로그인 과정을 보여주는 MP4 파일
- 스크린샷 시리즈: 각 단계의 UI 상태
- 실행 로그: 각 단계의 성공/실패 상태
- 성능 메트릭: 페이지 로드 시간, 인터랙션 응답 시간
만약 문제가 발견되면 (예: 잘못된 셀렉터, 예상치 못한 에러 메시지), 에이전트가 이를 Artifact에 명확히 표시하고 수정 방법을 제안한다.
시각적 회귀 테스트
Browser Agent의 또 다른 강력한 기능은 시각적 회귀 테스트다. 코드 변경으로 인해 UI가 의도치 않게 바뀌는 것을 자동으로 감지할 수 있다.
1
2
LoginForm 컴포넌트의 현재 렌더링을 기준 스크린샷으로 저장해줘.
앞으로 코드가 변경될 때마다 자동으로 비교해서 차이가 있으면 알려줘.
에이전트는 Chromatic과 유사한 방식으로 작동한다. 컴포넌트의 현재 상태를 스크린샷으로 저장하고, 나중에 코드가 변경되면 자동으로 새 스크린샷을 찍어 비교한다. 픽셀 단위의 차이가 발견되면 Artifact에서 두 이미지를 나란히 보여주며, 어떤 부분이 변경되었는지 하이라이트해준다.
이는 CSS 변경, 컴포넌트 리팩토링, 라이브러리 업그레이드 후에 UI가 깨지지 않았는지 확인하는 데 매우 유용하다.
TDD 워크플로우: 테스트 주도 개발의 자동화
전통적 TDD의 과제
테스트 주도 개발(Test-Driven Development, TDD)은 검증된 개발 방법론이지만, 실제 적용에는 어려움이 있다. 테스트를 먼저 작성하면 시간이 오래 걸리고, 초기에는 모든 테스트가 실패하며, 코드가 완성되기 전까지는 피드백을 받기 어렵다. 많은 개발자들이 TDD의 이점을 알면서도 실천하기 어려워하는 이유다.
안티그래비티는 TDD 워크플로우를 극적으로 간소화한다. 에이전트가 테스트를 작성하고, 구현을 생성하고, 테스트를 실행하고, 실패하면 수정하는 전체 사이클을 자동화할 수 있다.
실전 예제: TodoList 컴포넌트 TDD
새로운 TodoList 컴포넌트를 TDD 방식으로 개발한다고 가정하자. 다음과 같이 요청한다:
1
2
3
4
5
6
7
8
9
10
11
12
TDD 방식으로 TodoList 컴포넌트를 만들어줘:
요구사항:
- 할 일 목록을 표시한다
- 새 할 일을 추가할 수 있다
- 할 일을 완료 처리할 수 있다 (체크박스)
- 완료된 할 일은 취소선으로 표시한다
- 할 일을 삭제할 수 있다
- 빈 목록일 때는 "할 일이 없습니다" 메시지를 표시한다
먼저 실패하는 테스트를 작성하고, 그 다음 최소한의 구현으로 테스트를 통과시키는 방식으로 진행해줘.
각 단계마다 테스트 결과를 보여줘.
에이전트가 TDD 사이클을 시작한다:
Cycle 1: 렌더링 테스트
1
2
3
4
5
6
7
8
9
10
// TodoList.test.tsx (First RED)
describe('TodoList', () => {
it('should render empty state message when no todos', () => {
render(<TodoList todos={[]} />);
expect(screen.getByText('할 일이 없습니다')).toBeInTheDocument();
});
});
// 테스트 실행 결과: FAIL ❌
// TodoList 컴포넌트가 존재하지 않음
1
2
3
4
5
6
7
8
9
10
11
12
13
// TodoList.tsx (First GREEN)
interface TodoListProps {
todos: Todo[];
}
export function TodoList({ todos }: TodoListProps) {
if (todos.length === 0) {
return <p>할 일이 없습니다</p>;
}
return null;
}
// 테스트 실행 결과: PASS ✅
Cycle 2: 할 일 목록 표시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// TodoList.test.tsx (Second RED)
it('should render list of todos', () => {
const todos = [
{ id: '1', text: '장보기', completed: false },
{ id: '2', text: '운동하기', completed: false }
];
render(<TodoList todos={todos} />);
expect(screen.getByText('장보기')).toBeInTheDocument();
expect(screen.getByText('운동하기')).toBeInTheDocument();
});
// 테스트 실행 결과: FAIL ❌
// todos가 렌더링되지 않음
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// TodoList.tsx (Second GREEN)
export function TodoList({ todos }: TodoListProps) {
if (todos.length === 0) {
return <p>할 일이 없습니다</p>;
}
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
// 테스트 실행 결과: PASS ✅
Cycle 3: 완료 처리 기능
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// TodoList.test.tsx (Third RED)
it('should show completed todos with strikethrough', () => {
const todos = [
{ id: '1', text: '장보기', completed: true }
];
render(<TodoList todos={todos} />);
const todoText = screen.getByText('장보기');
expect(todoText).toHaveStyle({ textDecoration: 'line-through' });
});
it('should toggle todo completion when checkbox is clicked', async () => {
const mockOnToggle = vi.fn();
const todos = [
{ id: '1', text: '장보기', completed: false }
];
const user = userEvent.setup();
render(<TodoList todos={todos} onToggle={mockOnToggle} />);
const checkbox = screen.getByRole('checkbox', { name: '장보기' });
await user.click(checkbox);
expect(mockOnToggle).toHaveBeenCalledWith('1');
});
// 테스트 실행 결과: FAIL ❌
// 체크박스가 존재하지 않고, 취소선 스타일도 없음
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// TodoList.tsx (Third GREEN)
interface TodoListProps {
todos: Todo[];
onToggle: (id: string) => void;
}
export function TodoList({ todos, onToggle }: TodoListProps) {
if (todos.length === 0) {
return <p>할 일이 없습니다</p>;
}
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<label>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span style=>
{todo.text}
</span>
</label>
</li>
))}
</ul>
);
}
// 테스트 실행 결과: PASS ✅
에이전트는 이런 식으로 계속 진행하여 모든 요구사항을 충족할 때까지 RED → GREEN → REFACTOR 사이클을 반복한다. 각 사이클마다 테스트 결과를 Artifact로 보여주므로, 개발자는 진행 상황을 실시간으로 확인할 수 있다.
최종적으로 에이전트는 다음을 제공한다:
- 완성된
TodoList.tsx컴포넌트 - 포괄적인
TodoList.test.tsx테스트 스위트 (20+ 테스트 케이스) - 100% 테스트 커버리지 보고서
- Refactoring 제안 (중복 코드 제거, 성능 최적화 등)
TDD의 이점을 자동으로 얻기
이 과정을 통해 전통적인 TDD의 모든 이점을 얻으면서도, 수작업으로 테스트를 작성하는 시간을 절약할 수 있다:
- 명확한 요구사항: 테스트가 먼저 작성되므로 무엇을 만들어야 하는지 명확하다
- 최소한의 구현: 테스트를 통과하기 위한 최소한의 코드만 작성된다
- 즉각적인 피드백: 각 단계마다 테스트 결과를 확인할 수 있다
- 회귀 방지: 새 기능을 추가할 때 기존 기능이 깨지지 않는다
- 리팩토링 안전성: 테스트가 있으므로 자신있게 코드를 개선할 수 있다
다중 에이전트 협업: 병렬 테스트 개발
Manager View의 진가
안티그래비티의 Manager View는 여러 에이전트를 동시에 관리할 수 있는 Mission Control이다. 이를 활용하면 대규모 프로젝트에서 테스트 개발을 극적으로 가속화할 수 있다.
예를 들어 10개의 React 컴포넌트에 대한 테스트를 작성해야 한다면, 순차적으로 하면 몇 시간이 걸릴 수 있다. 하지만 Manager View에서 5개의 에이전트를 병렬로 실행하면 시간을 1/5로 줄일 수 있다.
실전 예제: E-commerce 앱 테스트 스위트 생성
온라인 쇼핑몰 프로젝트가 있고, 다음 컴포넌트들에 대한 테스트가 필요하다고 가정하자:
- ProductCard
- ProductList
- ShoppingCart
- Checkout
- UserProfile
Manager View를 열고 다음과 같이 5개의 에이전트에게 작업을 할당한다:
Agent 1: ProductCard 테스트
1
2
3
ProductCard 컴포넌트에 대한 포괄적인 테스트를 작성해줘.
상품 이미지, 가격, "장바구니 추가" 버튼이 제대로 렌더링되는지 확인하고,
클릭 이벤트와 호버 상태도 테스트해줘.
Agent 2: ProductList 테스트
1
2
ProductList 컴포넌트를 테스트해줘.
빈 목록 상태, 로딩 상태, 에러 상태, 그리고 페이지네이션을 모두 커버해줘.
Agent 3: ShoppingCart 테스트
1
2
ShoppingCart 컴포넌트 테스트를 작성해줘.
아이템 추가/제거, 수량 변경, 총액 계산이 정확한지 검증해줘.
Agent 4: Checkout 테스트
1
2
Checkout 컴포넌트의 전체 플로우를 테스트해줘.
배송 정보 입력, 결제 수단 선택, 주문 확인까지 모든 단계를 커버해줘.
Agent 5: UserProfile 테스트
1
2
UserProfile 컴포넌트 테스트를 작성해줘.
프로필 정보 표시, 편집 모드, 저장 기능을 테스트해줘.
각 에이전트는 독립적으로 작업을 수행한다. Manager View에서는 실시간으로 각 에이전트의 상태를 확인할 수 있다:
1
2
3
4
5
Agent 1: 🟢 Planning - ProductCard 테스트 계획 작성 중
Agent 2: 🟡 Coding - ProductList.test.tsx 작성 중
Agent 3: 🔵 Testing - ShoppingCart 테스트 실행 중
Agent 4: 🟠 Waiting - 승인 대기 (Implementation Plan)
Agent 5: 🟢 Completed - UserProfile 테스트 완료 ✅
각 에이전트가 작업을 완료하면 Artifact를 생성한다. 개발자는 한 에이전트의 작업을 검토하는 동안 다른 에이전트들은 계속 작업을 진행한다. 이렇게 하면 대기 시간이 거의 없이 효율적으로 작업을 진행할 수 있다.
에이전트 간 협업
더 흥미로운 것은 에이전트들이 서로 협업할 수도 있다는 점이다. 예를 들어:
Agent 1이 API mock 데이터를 생성하고, Agent 2와 Agent 3이 그 mock 데이터를 공유하여 테스트에 사용할 수 있다. Manager View에서 다음과 같이 지시한다:
1
2
3
4
5
6
7
8
9
Agent 1: 프로젝트에 공통으로 사용할 테스트 mock 데이터를
src/__mocks__/fixtures.ts 파일에 생성해줘.
상품, 사용자, 주문 데이터를 포함해줘.
Agent 2: Agent 1이 생성한 fixtures를 사용해서
ProductList 컴포넌트 테스트를 작성해줘.
Agent 3: Agent 1이 생성한 fixtures를 사용해서
ShoppingCart 컴포넌트 테스트를 작성해줘.
Agent 2와 Agent 3는 Agent 1이 작업을 완료할 때까지 자동으로 대기하고, fixtures.ts 파일이 생성되면 즉시 작업을 시작한다.
통합 테스트 리포트
모든 에이전트가 작업을 완료하면, Manager View에서 통합 리포트를 생성할 수 있다:
1
2
전체 에이전트가 완료한 테스트들을 종합해서
프로젝트 전체의 테스트 커버리지 리포트를 생성해줘.
에이전트가 다음과 같은 종합 리포트를 제공한다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
## E-commerce 앱 테스트 스위트 완료 리포트
### 생성된 테스트 파일
- ProductCard.test.tsx: 12 테스트
- ProductList.test.tsx: 18 테스트
- ShoppingCart.test.tsx: 24 테스트
- Checkout.test.tsx: 31 테스트
- UserProfile.test.tsx: 15 테스트
### 전체 통계
- 총 테스트 케이스: 100개
- 실행 시간: 8.3초
- 통과: 100/100 (100%)
- 실패: 0
### 코드 커버리지
- 구문(Statements): 94.2%
- 분기(Branches): 89.7%
- 함수(Functions): 96.1%
- 라인(Lines): 93.8%
### 커버리지가 부족한 영역
1. ShoppingCart.tsx: 에러 처리 로직 (72번째 줄)
2. Checkout.tsx: 결제 실패 시나리오 (156번째 줄)
### 권장사항
- ShoppingCart의 네트워크 에러 테스트 추가
- Checkout의 결제 실패 엣지 케이스 테스트 추가
- 모든 컴포넌트에 접근성 테스트 추가 (현재 부분적)
이 리포트를 바탕으로 추가 테스트를 요청하거나, 커버리지가 부족한 부분을 보완할 수 있다.
베스트 프랙티스: 효과적인 AI 협업
명확한 컨텍스트 제공
에이전트가 좋은 결과를 만들어내려면 충분한 컨텍스트가 필요하다. 단순히 “테스트 작성해줘”보다는 다음과 같이 구체적으로 요청하는 것이 좋다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
UserProfileForm 컴포넌트의 테스트를 작성해줘.
컨텍스트:
- 이 컴포넌트는 React Hook Form을 사용한다
- 유효성 검증은 Zod 스키마로 정의되어 있다
- 저장 버튼은 React Query의 mutation을 호출한다
- 이미지 업로드는 별도의 useImageUpload 커스텀 훅을 사용한다
테스트해야 할 것:
- 폼 유효성 검증 (모든 필드가 필수)
- 이메일 형식 검증
- 저장 성공 시 토스트 메시지 표시
- 저장 실패 시 에러 메시지 표시
- 이미지 업로드 프로세스
- 로딩 상태 동안 폼 비활성화
단계적 접근
복잡한 컴포넌트의 테스트를 한 번에 작성하려고 하지 말고, 단계적으로 접근하는 것이 좋다:
1
2
3
4
5
6
7
8
9
10
11
12
13
1단계: ProductDetail 컴포넌트의 렌더링 테스트만 먼저 작성해줘
[검토 후]
2단계: 사용자 인터랙션 테스트 추가해줘 (장바구니 추가, 수량 변경)
[검토 후]
3단계: API 통합 테스트 추가해줘 (상품 정보 로딩, 에러 처리)
[검토 후]
4단계: 접근성 테스트 추가해줘
이렇게 하면 각 단계마다 결과를 검토하고 방향을 조정할 수 있다.
Deny List 활용
안티그래비티는 특정 파일이나 디렉토리를 에이전트가 접근하지 못하도록 막는 Deny List 기능을 제공한다. 테스트 작업 시에는 다음과 같이 설정하는 것을 권장한다:
1
2
3
4
5
6
Deny List:
- node_modules/*
- .env
- build/*
- dist/*
- .git/*
이렇게 하면 에이전트가 실수로 중요한 파일을 수정하거나 삭제하는 것을 방지할 수 있다.
코드 리뷰는 여전히 필수
에이전트가 생성한 테스트 코드도 반드시 검토해야 한다. 다음 사항들을 특히 주의깊게 확인한다:
- 테스트가 실제로 무엇을 검증하는가: 의미없는 assertion이 아닌지 확인
- 엣지 케이스 커버리지: 예외 상황들이 제대로 테스트되는지 확인
- Mock의 적절성: 과도한 mocking으로 테스트가 무의미해지지 않았는지 확인
- 테스트 가독성: 다른 개발자가 테스트를 읽고 이해할 수 있는지 확인
- 성능: 테스트가 불필요하게 느리지 않은지 확인
안티그래비티는 도구일 뿐이며, 최종 품질 책임은 여전히 개발자에게 있다.
지속적인 피드백
에이전트가 생성한 코드에 문제가 있다면, Artifact에 직접 코멘트를 남길 수 있다. 마치 Google Docs에 댓글을 다는 것처럼 특정 부분을 하이라이트하고 수정 요청을 작성한다:
1
2
이 테스트는 너무 구체적인 구현 세부사항을 테스트하고 있어.
대신 사용자가 보는 결과를 테스트하도록 수정해줘.
에이전트가 즉시 코드를 수정하고 업데이트된 버전을 제공한다.
실전 시나리오: 대규모 리팩토링 후 테스트 업데이트
시나리오 설명
회사에서 레거시 React 프로젝트를 클래스 컴포넌트에서 함수 컴포넌트와 Hooks로 마이그레이션하기로 결정했다. 기존에 작성된 테스트들도 모두 업데이트해야 한다. 50개 이상의 컴포넌트와 200개 이상의 테스트 파일이 있다면 수작업으로는 몇 주가 걸릴 수 있다.
안티그래비티 활용 전략
1단계: 마이그레이션 패턴 정의
먼저 Workflow를 만들어 마이그레이션 패턴을 정의한다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
name: migrate-class-component-tests
description: 클래스 컴포넌트 테스트를 함수 컴포넌트 + Hooks 테스트로 마이그레이션
# 변환 규칙
- wrapper.instance() 사용 제거 (구현 세부사항 테스트 제거)
- setState() Mock을 useState() Hook 테스트로 변환
- componentDidMount 테스트를 useEffect 테스트로 변환
- this.props를 직접 props로 변환
- enzyme의 shallow/mount를 RTL의 render로 변환
# 유지해야 할 것
- 기존 테스트의 의도와 검증 내용
- 테스트 커버리지 수준
- 엣지 케이스 시나리오
# 개선 기회
- 더 사용자 중심적인 테스트로 개선
- 불필요한 mock 제거
- 테스트 가독성 향상
2단계: 샘플 마이그레이션
한 두 개의 컴포넌트로 먼저 테스트해본다:
1
2
3
UserCard 컴포넌트와 테스트를 함수 컴포넌트로 마이그레이션해줘.
기존 enzyme 기반 테스트를 React Testing Library로 변환하고,
모든 테스트가 통과하는지 확인해줘.
에이전트가 변환을 수행하고 Before/After를 보여준다. 이를 검토하여 마이그레이션 품질이 만족스러운지 확인한다.
3단계: 대규모 병렬 마이그레이션
샘플이 검증되면 Manager View에서 5개 에이전트를 동시에 실행한다:
1
2
3
4
5
Agent 1: src/components/user/* 의 모든 컴포넌트와 테스트 마이그레이션
Agent 2: src/components/product/* 의 모든 컴포넌트와 테스트 마이그레이션
Agent 3: src/components/cart/* 의 모든 컴포넌트와 테스트 마이그레이션
Agent 4: src/components/checkout/* 의 모든 컴포넌트와 테스트 마이그레이션
Agent 5: src/components/common/* 의 모든 컴포넌트와 테스트 마이그레이션
4단계: 통합 검증
모든 마이그레이션이 완료되면:
1
2
3
전체 프로젝트의 테스트를 실행해서
모든 테스트가 통과하는지 확인하고,
마이그레이션 전후의 커버리지를 비교해줘.
에이전트가 종합 리포트를 제공한다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
## 마이그레이션 완료 리포트
### 변환 통계
- 마이그레이션된 컴포넌트: 53개
- 마이그레이션된 테스트 파일: 208개
- 총 테스트 케이스: 1,247개
### 테스트 실행 결과
- 통과: 1,238/1,247 (99.3%)
- 실패: 9 (0.7%)
### 실패한 테스트
1. CartSummary.test.tsx: timeout 이슈
2. CheckoutForm.test.tsx: 비동기 처리 문제
... (상세 내용)
### 커버리지 비교
마이그레이션 전: 87.3%
마이그레이션 후: 89.1% (+1.8%)
### 개선 사항
- 구현 세부사항 테스트 제거: 143개
- 더 사용자 중심적 테스트로 전환: 208개
- 불필요한 mock 제거: 87개
- 평균 테스트 실행 시간: 12.4초 → 8.7초 (30% 개선)
실패한 9개의 테스트에 대해서는 개별적으로 에이전트에게 수정을 요청한다. 이렇게 하면 몇 주가 걸릴 작업을 하루 만에 완료할 수 있다.
성능 최적화: 빠른 피드백 루프
Vitest의 Watch 모드 활용
개발 중에는 빠른 피드백이 중요하다. Vitest의 Watch 모드를 활용하면 파일을 저장할 때마다 관련된 테스트만 자동으로 실행된다.
1
2
3
4
package.json에 다음 스크립트를 추가해줘:
"test:watch": "vitest --watch"
그리고 Watch 모드로 테스트를 시작해줘.
에이전트가 스크립트를 추가하고 Watch 모드를 시작한다. 이제 컴포넌트를 수정하면 관련 테스트가 자동으로 실행되고, 터미널에 즉시 결과가 표시된다.
선택적 테스트 실행
전체 테스트 스위트를 매번 실행하면 시간이 오래 걸린다. 작업 중인 부분과 관련된 테스트만 실행하도록 설정할 수 있다:
1
2
현재 작업 중인 LoginForm 컴포넌트와 관련된 테스트만 실행해줘.
그리고 실행 시간을 측정해서 알려줘.
1
2
3
4
5
6
$ vitest run LoginForm
Test Files 1 passed (1)
Tests 14 passed (14)
Start at 14:23:05
Duration 342ms
병렬 실행 최적화
Vitest는 기본적으로 테스트를 병렬로 실행하지만, 설정을 조정하면 더 최적화할 수 있다:
1
2
3
vitest.config.ts를 수정해서
최대 워커 수를 CPU 코어 수에 맞게 설정하고,
테스트 타임아웃도 적절히 조정해줘.
에이전트가 다음과 같이 설정을 최적화한다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default defineConfig({
test: {
// CPU 코어 수만큼 병렬 실행
maxConcurrency: os.cpus().length,
// 개별 테스트 타임아웃 (너무 짧으면 비동기 테스트 실패)
testTimeout: 10000,
// 느린 테스트 경고 임계값
slowTestThreshold: 1000,
// 격리 모드 (각 테스트 파일이 독립적으로 실행)
isolate: true,
}
});
트러블슈팅: 일반적인 문제와 해결책
문제 1: 에이전트가 잘못된 테스트를 생성
증상: 생성된 테스트가 구현 세부사항을 테스트하거나, 의미없는 assertion을 포함한다.
해결책: Workflow에 더 명확한 지침을 추가하고, 구체적인 예시를 제공한다.
1
2
3
4
5
6
7
# 잘못된 테스트 예시 (피해야 할 것)
expect(component.state.count).toBe(5) // ❌ 구현 세부사항
expect(element.className).toContain('red') // ❌ 스타일 세부사항
# 올바른 테스트 예시 (권장)
expect(screen.getByText('5')).toBeInTheDocument() // ✅ 사용자가 보는 것
expect(screen.getByRole('button')).toHaveAttribute('aria-pressed', 'true') // ✅ 동작
문제 2: 테스트가 Flaky(불안정)함
증상: 같은 테스트가 때로는 통과하고 때로는 실패한다.
해결책: 비동기 처리를 명시적으로 대기하도록 수정 요청한다.
1
2
3
이 테스트가 불안정해.
비동기 처리를 모두 waitFor나 findBy로 감싸서
DOM 업데이트를 확실히 기다리도록 수정해줘.
에이전트가 다음과 같이 수정한다:
1
2
3
4
5
6
7
8
9
// Before (Flaky)
await user.click(submitButton);
expect(screen.getByText('성공')).toBeInTheDocument(); // 실패할 수 있음
// After (Stable)
await user.click(submitButton);
await waitFor(() => {
expect(screen.getByText('성공')).toBeInTheDocument();
});
문제 3: Mock이 제대로 작동하지 않음
증상: API 호출을 mock했는데도 실제 네트워크 요청이 발생한다.
해결책: Mock 설정을 명시적으로 요청한다.
1
2
3
API 호출을 MSW(Mock Service Worker)로 mock해줘.
그리고 각 테스트에서 필요한 응답을 쉽게 커스터마이즈할 수 있도록
핸들러를 재사용 가능하게 만들어줘.
에이전트가 MSW 설정과 재사용 가능한 핸들러를 생성한다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/__mocks__/handlers.ts
export const handlers = [
rest.get('/api/user', (req, res, ctx) => {
return res(ctx.json({ id: '1', name: 'Test User' }));
}),
];
// LoginForm.test.tsx
import { server } from '../__mocks__/server';
import { rest } from 'msw';
it('should handle API error', async () => {
// 이 테스트에서만 에러 응답으로 override
server.use(
rest.post('/api/login', (req, res, ctx) => {
return res(ctx.status(401));
})
);
// 테스트 계속...
});
문제 4: 테스트 커버리지가 낮음
증상: 에이전트가 생성한 테스트의 커버리지가 목표에 미달한다.
해결책: 커버리지 리포트를 보고 누락된 부분을 구체적으로 요청한다.
1
2
3
현재 테스트 커버리지 리포트를 생성해줘.
그리고 커버되지 않은 코드 경로를 분석해서
추가 테스트를 작성해줘.
에이전트가 커버리지 리포트를 분석하고 누락된 엣지 케이스들에 대한 테스트를 추가한다.
결론: AI 에이전트 시대의 테스팅
구글 안티그래비티는 UI 단위 테스트 작성 방식을 근본적으로 변화시키고 있다. 개발자는 더 이상 반복적인 테스트 코드 작성에 시간을 쓰지 않고, 대신 무엇을 테스트해야 하는지, 어떤 품질 기준을 적용할 것인지 같은 고차원적인 결정에 집중할 수 있다.
하지만 이것이 개발자의 역할을 축소시키는 것은 아니다. 오히려 개발자의 역할이 더 중요해진다. AI 에이전트가 생성한 테스트의 품질을 검증하고, 적절한 테스트 전략을 수립하고, 팀의 테스팅 표준을 정의하는 것은 여전히 개발자의 몫이다.
안티그래비티를 효과적으로 활용하는 핵심은 명확한 컨텍스트 제공, 단계적 접근, 그리고 지속적인 피드백이다. Workflow 시스템으로 팀의 테스팅 표준을 정의하고, Manager View로 여러 에이전트를 효율적으로 관리하고, Browser Agent로 실제 UI 동작을 검증하면, React 애플리케이션의 테스트 커버리지와 품질을 극적으로 향상시킬 수 있다.
2026년 현재 무료로 제공되는 이 강력한 도구를 활용하여, 더 견고하고 신뢰할 수 있는 React 애플리케이션을 만들어보자. 테스트 작성에 쓰던 시간을 더 중요한 문제를 해결하는 데 사용할 수 있을 것이다.
문서 작성 일자: 2026-01-22
추가 리소스
- Google Antigravity 공식 문서: antigravity.google
- Google Codelabs: Getting Started with Antigravity
- React Testing Library 공식 문서: testing-library.com/react
- Vitest 공식 문서: vitest.dev
- MSW(Mock Service Worker) 문서: mswjs.io
참고 사례
- MoneySense AI: 안티그래비티로 복잡한 금융 데이터 시각화 컴포넌트 테스트 자동화
- MenuGo: 전체 E-commerce 플로우 테스트를 2주에서 3일로 단축
- Sarfa: 24시간 만에 5,000줄 이상의 프로덕션 코드와 완전한 테스트 스위트 생성
커뮤니티
- Discord: Antigravity 개발자 커뮤니티
- GitHub Discussions: 실전 사례 공유 및 질문
- Reddit r/antigravity: 팁과 트릭 공유