AI 앱 복제: 화면과 동작의 격차

AI가 만든 앱의 첫 화면이 원본과 비슷하다. 버튼과 입력칸도 있다. 여기까지 확인하고 끝내면, 숫자를 바꿨을 때 합계가 그대로이거나 저장한 내용이 새로고침 뒤 사라지는 문제를 놓칠 수 있다.

2026년 9월 18일 공개되고 21일 개정된 RecreationWorld 논문은 이 차이를 살펴볼 자료다. 앱 재구현 과제 250개를 평가한 RecreationBench에서 GPT-6 Astra의 종합 점수는 58.1%였다. 모든 프로그램 검사를 통과한 과제 비율은 2.8%였다. 평균적으로 받은 점수와, 필요한 검사를 전부 통과한 앱의 비율은 다르다. 논문과 공개 이력

이번 글에서는 이 숫자의 의미를 확인하고, 작은 계산 예제를 따로 만들어 검사했다. 논문의 모델 성능을 재현한 실험은 아니다. 무엇을 검사하느냐에 따라 같은 앱에 대한 판단이 어떻게 달라지는지 확인하는 예제다.

58.1%와 2.8%가 세는 것

RecreationBench는 실행 중인 기준 앱을 AI에게 보여 주고 그 동작을 재구현하게 한다. 대상은 Ubuntu, macOS, Windows, Android, 웹이다. 제출물은 화면뿐 아니라 실행 가능한 구현이다.

종합 점수는 프로그램으로 읽은 동작과 화면 평가를 집계한 값이다. 2.8%는 프로그램 검사 전체를 통과한 과제의 비율이다. 화면 평가까지 모두 만점을 받았다는 뜻도, 일반적인 AI 앱 개발의 성공률이라는 뜻도 아니다. 평가 방식과 지표

가령 앱 열 개가 각각 검사 열 개 중 아홉 개를 통과했다고 하자. 이 설명용 가정에서는 평균 통과율이 90%지만, 열 개를 모두 통과한 앱은 없다. 빠진 검사가 데이터 저장이라면 사용자는 남은 90%보다 그 실패를 더 크게 겪을 수 있다. 검사 항목의 평균과 사용할 수 있는 결과물의 수를 같이 봐야 하는 이유다.

같은 첫 화면으로 만든 두 계산기

차이를 분리해서 보기 위해 AI로 금액 계산 예제 두 버전을 만들었다. 단가는 10,000원, 처음 수량은 2개, 할인율은 0%다. 한쪽에는 계산 결과를 20,000원으로 고정하고 저장을 생략하는 오류를 일부러 넣었다. 다른 쪽은 입력값으로 계산하고 브라우저에 저장하도록 했다.

두 버전을 같은 Chromium 환경과 화면 크기로 열어 첫 화면을 캡처했다. 이미지 파일의 SHA-256 값이 같았다. 이 값은 파일 내용으로 계산한 식별값이며, 여기서는 두 캡처가 바이트 단위로 같았다는 뜻이다. 앱의 이후 동작까지 같다는 뜻은 아니다.

이어서 수량, 할인율, 새로고침 순서로 검사했다.

검사 기대한 결과 오류를 넣은 버전 정상 버전
처음 열기 20,000원 20,000원 20,000원
수량을 3개로 바꾸고 계산 30,000원 20,000원 30,000원
할인율을 10%로 바꾸고 계산 27,000원 20,000원 27,000원
새로고침 수량 3개·할인 10%·27,000원 유지 수량 2개·할인 0%로 초기화 기대한 값 유지

같은 첫 화면에서 시작한 두 계산기가 수량과 할인율을 바꾼 뒤 서로 다른 합계를 표시한 검사 결과

이 글을 위해 만든 예제의 실행 결과다. 오류가 있는 버전은 첫 검사만 통과했고 정상 버전은 네 검사를 통과했다. 특정 AI 모델의 오류율을 측정한 자료가 아니다.

AI는 예제 코드 작성과 브라우저 조작·값 읽기·캡처 대조를 수행했다. 기대값은 구현 결과를 따라 정하지 않고 단가 × 수량 × (1 − 할인율 / 100)으로 먼저 정했다. 구현한 코드에 틀린 공식이 들어 있어도, 그 공식을 그대로 검사에 복사하면 같은 오류를 두 번 확인하게 된다.

클릭 여부보다 클릭 뒤의 값

“버튼을 눌러 봤다”는 보고에는 결과가 빠져 있다. 클릭에 반응해 화면이 바뀌어도, 바뀐 값이 맞는지는 별도 질문이다. 예제의 검사는 다음처럼 입력과 기대값을 함께 적었다.

await page.locator('#qty').fill('3');
await page.getByRole('button').click();
await expect(page.locator('output')).toHaveText('30,000원');

await page.locator('#discount').fill('10');
await page.getByRole('button').click();
await expect(page.locator('output')).toHaveText('27,000원');

await page.reload();
await expect(page.locator('#qty')).toHaveValue('3');
await expect(page.locator('#discount')).toHaveValue('10');
await expect(page.locator('output')).toHaveText('27,000원');

위 코드는 같은 검사 흐름을 Playwright의 expect 문법으로 옮긴 예시다. 실제 실행 기록은 각 단계의 기대값과 관측값을 저장해 비교했다.

첫 화면 검사는 배치·글자·색을 확인하는 데 필요하다. 입력 검사는 규칙을, 새로고침 검사는 값이 유지되는지를 확인한다. 서로 대신할 수 있는 검사가 아니다. 계산기의 합계가 맞아도 글자가 화면 밖으로 잘리면 사용하기 어렵고, 화면이 정확해도 저장이 안 되면 결과를 잃는다.

AI에게 줄 작업 단위를 상태 변화로 나누기

이 방식을 실제 개발에 적용한다면 요청을 “계산기를 완성해 줘”에서 한 단계 더 구체화할 수 있다. 아래는 이번 예제에서 정리한 요청안이다.

구현 전에 입력, 동작, 기대 결과를 표로 정리한다.
정상 입력에서 바꿀 값을 하나씩 정하고 예상 결과를 계산한다.
각 항목에 대해 화면의 실제 값과 기대값을 비교한다.
저장 기능은 저장 직후와 새로고침 뒤를 모두 확인한다.
실패했거나 실행하지 못한 검사는 목록에서 빼지 않는다.
코드를 마지막으로 수정한 뒤 영향을 받는 검사를 다시 실행한다.

수량과 할인율을 한 번에 바꾸면 실패했을 때 어느 계산이 틀렸는지 바로 알기 어렵다. 먼저 수량만 바꾸고, 그다음 할인율을 바꾸면 원인을 좁힐 수 있다. 반대로 여러 단계를 이어야만 보이는 문제도 있다. 저장한 뒤 창을 다시 열어 보는 순서가 그 예다.

검사 목록은 AI가 어떤 코드를 만들었는지에만 맞춰 정하지 않는다. 앱에 필요한 동작에서 출발해야 한다. 요구한 저장 기능을 구현하지 않은 뒤 저장 검사를 빼 버리면, 목록의 통과율만 올라갈 수 있다. “해당 없음”으로 바꿀 때도 요구사항이 바뀐 것인지 먼저 확인해야 한다.

마지막 수정 뒤의 검사

이번 블로그도 글이 브라우저에서 열린다는 사실과 발행할 파일이 맞다는 사실을 따로 확인했다. 글 화면에서는 이미지·언어 전환·넘침을 검사했고, 배포에서는 검증한 파일과 서버 파일의 해시를 대조했다. 해시 일치는 배포 파일의 동일성을 확인한다. 글 내용의 사실성이나 사용자가 밟을 모든 경로를 보장하지는 않는다.

계산기를 고친 뒤 이전 캡처만 제출하는 경우에도 같은 문제가 생긴다. 캡처가 증명하는 것은 수정 전 화면일 수 있다. 그래서 완료 보고에는 최종 수정 이후 실행한 검사와 남은 확인이 같이 있어야 한다. 검사 수를 늘리는 것보다, 제출한 결과물에 대한 검사인지부터 맞추는 편이 먼저다.

AI 개발팀의 연결 검사에서도 개별 테스트가 통과한 뒤 실제 응답을 연결하면서 문제가 드러났다. 이번 예제는 그 구분을 작은 화면 하나로 줄여 본 것이다.

이 결과로 말할 수 있는 범위

RecreationBench는 고정된 환경과 정해진 검사 집합을 사용한다. 실시간 서비스, 외부 상태 변화, 실제 사용자의 모든 동작을 대표하지 않는다. 논문도 검사 통과가 모든 상태에서 원본과 같음을 증명하지는 않는다고 밝힌다. 연구의 한계

여기서 만든 계산 예제도 입력 네 단계만 확인했다. 잘못된 입력, 다른 탭과의 충돌, 저장 공간 오류까지 검증한 앱은 아니다. 확인한 결과는 좁고 분명하다. 첫 화면이 정확히 같았던 두 버전도 입력과 저장 검사에서는 달랐다. AI에게 맡길 검사에는 처음 보이는 화면과 그 뒤에 일어날 상태 변화가 모두 들어가야 한다.

다음 글인 코딩 AI의 계획 기능과 비용에서는 이 검사 과정을 어디까지 진행하고 언제 멈출지 정하는 문제를 다룬다.

자료 확인·예제 실행: 2026년 9월 21일. 논문은 공개된 미심사 원고 v2를 사용했다. 별도 모델 API를 호출하거나 RecreationBench 전체를 실행하지 않았다.

이 글의 목차