본문으로 건너뛰기

© 2026 Molayo

Zenn헤드라인2026. 06. 24. 21:27

【Flutter】AI와 결합하여 금융 앱의 VRT를 운영해 본 경험

요약

금융 앱 개발 과정에서 Flutter의 Golden Test를 활용해 시각적 회귀 테스트(VRT)를 구축하고 운영한 경험을 공유합니다. GitHub Actions와 easy-vrt를 연동하여 PR 단계에서 UI 변화를 자동으로 감지하는 CI 워크플로우를 소개합니다.

핵심 포인트

  • Flutter의 Golden Test를 이용한 위젯 기반 VRT 구현
  • GitHub Actions와 easy-vrt를 활용한 자동화된 CI 워크플로우 구축
  • 금융 앱 특유의 미세한 레이아웃 변화를 방지하기 위한 시각적 검증
  • PR 생성 시 Before/After 이미지 비교 및 자동 코멘트 기능 활용

서론

안녕하세요!

블루모 증권 주식회사(BlueMo Securities Co., Ltd.)에서 Flutter 팀 리드를 맡고 있는 oke입니다.

모바일 앱을 개발하다 보면 "공통 컴포넌트를 조금 수정했더니, 다른 화면의 모습이 예상치 못하게 바뀌어 버렸다!" 같은 일이 발생하곤 하죠?

특히 금융 계열 앱은 금액, 비율, 주석 등의 정보가 화면에 밀집되어 있기 때문에, 아주 미세한 레이아웃 변화만으로도 정보가 잘려 나가 누락되는 상황이 발생하기도 합니다 (땀).

이러한 "의도하지 않은 시각적 변화"를 매번 사람의 눈으로만 체크하는 것은 번거로운 일입니다. 그래서 화면의 모습을 이미지로 비교하여 변화를 자동으로 감지하는 메커니즘——VRT(Visual Regression Test)를 도입했습니다.

본 기사에서는 VRT가 어떤 원리로 작동하는지 차례대로 설명하면서, 실제로 운영하는 과정에서 유용했던 Tips를 AI 활용과 함께 소개하겠습니다.

애초에 VRT란 무엇인가

VRT는 간단히 말해 "화면의 스크린샷을 찍어서 기준이 되는 이미지와 비교하는" 테스트입니다.

그리고 차이(diff)가 있다면 "모습이 변했다"라고 감지할 수 있습니다.

차이를 감지하고 있는 모습

Flutter에는 이를 실현하기 위한 메커니즘으로 "Golden Test"가 표준으로 준비되어 있습니다.

위젯(Widget)을 렌더링하여 이미지로 저장하고, 다음부터는 그 이미지와 일치하는지를 비교하는 방식입니다.

Golden Test의 기본

Golden Test의 작성은 간단하며, 아래 코드와 같이 Widget을 렌더링하여 matchesGoldenFile로 지정한 Golden 이미지와 대조하기만 하면 됩니다.

testWidgets('HomePage golden', (tester) async {
await tester.pumpWidget(const MaterialApp(home: HomePage()));
await expectLater(
...

단, 처음에는 기준이 되는 이미지가 존재하지 않기 때문에, --update-goldens를 붙여 실행하여 이미지를 생성합니다.

flutter test --update-goldens

이후에는 flutter test를 실행하면, 저장된 이미지와 일치하는지 비교하며 차이가 있으면 테스트가 실패합니다.

VRT가 동작하는 흐름

위의 Golden Test 방식은 사전에 작성해 두었던 기준 이미지와 대조하여 비교하지만, 이를 PR(Pull Request) 생성 시에 머지 대상(merge target)과 작업 대상(source)의 UI 차이를 알기 쉽게 감지할 수 있다면 좋겠지요.

그렇기 때문에 PR이 생성될 때마다 GitHub Actions를 통해 다음과 같은 흐름을 자동으로 실행하고 있습니다.

  • PR이 생성/업데이트되면 VRT용 GitHub Actions가 기동
  • 머지 대상(before)과 작업 대상(after) 모두에 대해 전 화면을 렌더링
  • before / after 이미지를 비교
  • 차이가 발생한 화면만 PR에 코멘트로 제시

CI에는 VRT용 Workflow를 준비하였고, easy-vrt를 이용하고 있습니다. 기재된 대로 CI를 셋업하면 머지 대상·작업 대상으로부터 이미지를 생성하고, 이를 reg-cli라는 도구를 사용하여 차이가 있으면 PR에 코멘트 및 이미지 차이를 보기 쉬운 HTML 형식으로 출력하여 다운로드할 수 있게 하는 부분까지 자동으로 수행해 줍니다.

PR에 코멘트가 달려 있는 모습

위의 정보만 있다면 일단 VRT를 구축할 수 있을 것 같지만... 실제 운영에서는 고민되는 점이 몇 가지 생겨났기에, 그것들을 Tips로서 소개합니다!

Tips 1. Golden 이미지를 어떻게 찍을 것인가

세로로 긴 화면은 높이를 자동 계산한다

스마트폰의 한 화면에 담기지 않는 세로로 긴 페이지의 경우, 높이를 수동으로 지정하게 되기 쉽습니다.

지정이 짧으면 콘텐츠가 중간에 잘리고, 길면 여백이 생깁니다. 이를 화면마다 수동으로 조정하는 것은 번거로운 일입니다.

그래서 Scrollable의 maxScrollExtent로부터 콘텐츠 전체의 높이를 자동 계산하는 메커니즘을 준비했습니다. 한 번 스크롤하여 전체 높이를 측정하고, 그 높이로 리사이즈한 후 촬영합니다.

// Scrollable을 끝까지 스크롤하여 전체 콘텐츠를 레이아웃하고, 전체 높이를 산출함
await _scrollToEndAndBack(tester);
var totalHeight = _measureTotalHeight(tester, viewportHeight);
...

리사이즈 후에 이미지 로딩이 완료되어 레이아웃이 더 늘어나는 경우도 있기 때문에, 높이가 안정될 때까지 몇 번만 재보정을 수행하고 있습니다.

이를 통해 테스트 측에서 화면별 높이를 하드코딩(Hard-code)할 필요가 없어졌습니다.

작은 단말기에서도 촬영하여 잘림 방지

금융 앱에서는 수치 등이 어중간하게 잘리는 현상은 오인식으로 이어질 수 있어 피해야 하지만, 수동 확인 시에는 아무래도 평소 사용하는 일반 사이즈의 단말기에 치우치기 마련이라 작은 단말기에서의 레이아웃 깨짐은 놓치기 쉽습니다.

그래서 각 화면을 작은 단말기와 일반 사이즈의 2가지 패턴으로 렌더링(Rendering)하고 있습니다.

const goldenDevices = [
GoldenDevice(name: 'iPhoneSE_zoomed', width: 320, viewportHeight: 568),
GoldenDevice(name: 'iPhone17', width: 402, viewportHeight: 874),
...

iPhoneSE_zoomed

(가로 320pt)의 320pt는 현재 iOS가 지원하는 단말기 중 가장 좁은 폭입니다.

4.7인치 기기(iPhone SE 2/3세대 등)에서 디스플레이 줌(Display Zoom, 확대 표시)을 활성화하면 320×568pt로 그려지며, 명칭의 zoomed는 이 케이스를 가리킵니다. 즉, iOS에서 실제로 도달할 수 있는 최소 폭까지 검증하고 있습니다.

두 단말기의 이미지는 한 장에 가로로 나란히 합성하여 출력하므로, "일반 단말기에서는 문제가 없지만, 작은 단말기에서는 줄바꿈이 되어 버튼과 겹쳐 있다"와 같은 케이스도 좌우를 비교하는 것만으로 파악할 수 있습니다.

작은 단말기와 표준 크기를 동시에 검증

글자는 실제 폰트로 렌더링한다

Flutter의 골든 테스트(Golden Test)는 기본 설정 상태에서 실제 폰트를 불러오지 않고 글자가 모두 동일한 폭의 사각형으로 그려질 수 있어, 작은 단말기에서의 글자 잘림을 감지하지 못할 때가 있습니다.

그래서 앱에서 사용하는 폰트를 FontLoader로 불러와 실제 글자 모양으로 렌더링하고 있습니다.

Future<void> loadAppFonts() async {
await _loadFont('Noto Sans JP', ['assets/fonts/NotoSansJP-Regular.ttf', /* ... */]);
}

"실제 폰트를 그리면 환경 차이로 인해 렌더링이 미세하게 흔들려 골든(Golden)이 플래키(Flaky)해진다"라는 우려 때문에, 일부러 폰트를 그리지 않는 구성으로 만드는 사례도 있을 것입니다. 골든 이미지는 렌더링 환경에 따라 미세하게 달라지며, 로컬에서 만든 기준 이미지를 CI와 비교할 때 차분이 발생하기 쉽습니다. 이러한 동작은 공식 matchesGoldenFile 문서에도 "폰트는 플랫폼이나 Flutter 버전에 따라 렌더링이 달라질 수 있다"라고 명시되어 있습니다.

이번에는 이를 easy-vrt가 Before / After 양쪽 모두를 CI(항상 Linux 러너 사용)에서 렌더링하여 대조함으로써 회피하고 있으며, 폰트 기인으로 인한 플래키(Flaky)한 차분은 현재까지 발생하지 않고 있으며 실제 폰트 표시를 바탕으로 한 확인이 가능합니다.

네트워크 이미지는 더미(Dummy)로 고정한다

화면에는 CDN 등에서 가져오는 네트워크 이미지(CachedNetworkImage)가 포함됩니다. 테스트 환경에서는 실제 통신을 할 수 없기 때문에 그대로 두면 이미지가 에러로 표시됩니다.

그래서 HttpOverrides를 교체하여 네트워크 요청에 고정된 더미 이미지(1×1 회색 PNG)를 반환하도록 하고 있습니다.

class MockHttpOverrides extends HttpOverrides {
@override
HttpClient createHttpClient(SecurityContext? context) => _MockHttpClient();
...

이렇게 하면 네트워크에 의존하지 않고 항상 동일한 이미지로 렌더링할 수 있으며, 이미지 부분이 차분의 노이즈가 되는 일도 없습니다.

Tips 2. 비교 메커니즘

비교 방법은 easy-vrt로 완결시킨다

VRT를 시작할 때 고민되는 점이 "촬영한 이미지를 어디에 두고 어떻게 차분을 추출할 것인가"입니다.

전형적인 방법은 reg-suit라고 생각하며, 커밋별 이미지와 비교 리포트를 클라우드(S3나 Google Cloud Storage)에 저장하고, PR 생성 시 클라우드의 이미지와 대조하는 구성입니다.

검증된 선택지이지만, 이미지와 HTML 리포트를 저장할 버킷(Bucket)의 운영이 필요합니다. 예를 들어 reg-publish-s3-plugin은 기본 ACL(Access Control List)이 public-read로 설정되어 있어, 즉 리포트와 이미지가 공개 상태가 됩니다. 외부로 유출되면 안 되는 화면을 포함하는 앱이라면 이 점을 무시할 수 없으며, 공개하고 싶지 않다면 별도의 액세스 제어(Access Control)를 직접 구현해야 합니다.

이번에는 우선 도입하여 운영하며 효과를 검증하는 것을 우선시하여 easy-vrt를 선택했습니다.

작가의 기사에 나와 있듯이 "외부 스토리지(External Storage)를 이용하지 않고 GitHub Actions만으로 완결"되는 구성으로, 클라우드의 인증 정보나 버킷을 준비할 필요 없이 리포지토리(Repository)에 Workflow를 추가하는 것만으로 바로 동작하기 때문에 편리하며, VRT 도입에 안성맞춤이었습니다.

병렬 실행과 opt-in으로 실행 시간을 억제하기

대상 화면이 늘어나면 이번에는 실행 시간과 조합 관리가 과제가 됩니다. 이에 대해 두 가지 방법으로 대응했습니다.

병렬 실행으로 실행 시간을 억제하기

화면 수의 증가에 따라 테스트 시간도 늘어나기 때문에, --concurrency=4를 사용하여 병렬 실행하고 있습니다.

다만 병렬화를 하면, CachedNetworkImage가 내부적으로 사용하는 flutter_cache_manager가 동일한 캐시 디렉터리에 동시에 쓰기를 시도하여 파일이 손상되는 문제가 있었습니다. 이 캐시 경로는 path_provider로부터 가져오기 때문에, path_provider를 모킹(Mock)하여 프로세스마다 고유한 임시 디렉터리를 반환하도록 설정함으로써 충돌을 회피하고 있습니다.

FeatureFlag는 opt-in 방식으로 조합을 제한하기

FeatureFlag(기능 플래그)를 통해 기능의 노출 여부를 분기하는 경우가 있습니다. 이 경우 "모든 플래그 × 모든 화면"에 대해 ON/OFF 양쪽을 촬영하면 조합이 방대해집니다. 따라서 기본적으로는 전개하지 않고, 플래그에 따라 모습이 변하는 화면만 variantFlags를 통해 명시적으로 opt-in(선택적 참여)하는 방식으로 했습니다.

// 이 페이지는 sample의 ON/OFF에 따라 표시가 달라지므로 opt-in 한다
variantFlags: const {FeatureFlag.sample},

onTap 내부의 분기처럼, 조작하지 않으면 픽셀이 변하지 않는 것은 opt-in 하지 않는 식으로 구분하여 필요한 비교 대상만으로 압축하고 있습니다.

Tips 3. AI와 결합하기

테스트는 AI에게 작성하게 한다

VRT는 대상 화면을 늘릴수록 효과가 높아지지만, 그만큼 테스트를 작성하는 비용도 발생합니다.

화면마다 테스트를 준비하고 필요한 Provider의 상태를 맞추는 수고는 화면 수가 늘어나면 무시할 수 없습니다.

여기서 효과를 본 것이 AI의 활용입니다. GoldenTest 구현을 위한 Claude Code의 Skills를 준비해 두어, 대상 페이지를 전달하는 것만으로 테스트를 만들 수 있도록 하고 있습니다. 스킬에는 대략 다음과 같은 절차를 기재해 두었습니다.

  • 대상 페이지가 사용 중인 Provider를 조사한다
  • 계좌 개설 완료/미개설 등의 상태(Provider overrides)를 기존 것을 사용할지, 새로 준비할지 판단한다
  • 상태에 맞춰 GoldenTest를 작성한다
  • Golden 이미지를 생성하고, 깨짐이 없는지 품질을 확인한다

판단이 모호한 점(작성할 패턴 수나 Fake 데이터의 값 등)은 Claude Code의 AskUserQuestions를 통해 AI가 그 자리에서 질문하도록 설계했습니다.

작성할 패턴에 대해서는 현재 페이지 단위의 대표적인 표시 패턴이 중심이며, 세부적인 UI 변형은 컴포넌트 단위의 VRT로 담보하는 형태로 확장해 나가고 싶습니다.

goldenTestWithFeatureFlags(
groupName: 'Home_계좌 개설 완료',
child: const HomePage(),
...

또한, GoldenTest의 구현 코드는 깊게 확인할 필요 없이 결과물인 이미지로 판단하도록 하고 있어, 리뷰 비용도 낮게 운영할 수 있습니다.

Golden 이미지를 AI의 피드백 루프로 만들기

VRT를 도입하여 부수적으로 얻은 효과는, 생성된 Golden 이미지가 그대로 AI의 "눈"이 된다는 점입니다.

OpenAI가 공개한 Harness engineering에서도 스크린샷 등 렌더링 결과를 전달하여 UI 동작 등의 분석을 에이전트(Agent) 스스로 수행하게 하는 시도가 소개되고 있습니다.

시뮬레이터를 에이전트(Agent)가 조작하게 하는 방법도 있지만, 화면 전환과 상태(State) 구축, 그리고 매번 빌드가 필요합니다. GoldenTest의 경우에는 굳이 촬영 환경을 구축하지 않아도, AI에게 「구현 코드」, 「렌더링 결과 이미지」, 「Figma 디자인」을 한꺼번에 전달하여,

  • 디자인과 구현의 차이(Diff)를 인식시키기
  • 수정 → Golden 재생성 → 재확인, 이라는 루프를 이미지 기반으로 돌리기

와 같은 작업을 추가 비용 없이 수행할 수 있습니다.

FigmaMCP를 사용하여 컨텍스트(Context)를 전달하면서 실제 색상, 글자 크기, 여백을 대조하면 좋은 결과물을 얻을 수 있기 때문에, 구현 정밀도를 높이는 용도로도 효과적입니다.

PR의 개요란에 before/after 이미지를 자동으로 첨부하기

PR(Pull Request)을 생성할 때, 차이가 있는 화면이나 새로 생성한 화면이 있으면 개요란에 이미지를 올리곤 하는데, 매번 이미지를 촬영해서 붙여넣는 것이 번거로웠습니다.

그래서 PR을 생성하는 Skill을 만들어, 그곳에 신규 생성되거나 차이가 있는 화면이 있으면 Playwright MCP를 사용하여 자동으로 첨부되도록 하고 있습니다. 덕분에 PR 생성 작업이 상당히 편해졌습니다.

운용을 통해 얻은 효과

한동안 운용해 보면서 특히 효과를 느낀 점은 다음과 같습니다.

  • 소형 단말기에서의 레이아웃 깨짐을 발견할 수 있음
    일반적인 단말기에서는 문제가 없더라도, 소형 단말기에서는 글자가 줄바꿈되어 레이아웃이 깨지는 등의 케이스를 VRT가 잡아주었습니다. 수동 확인은 아무래도 일반적인 단말기에 치우치기 마련이므로, 이러한 놓치기 쉬운 부분을 보완할 수 있어 도움이 됩니다.

  • 수정한 화면 이외의 영향까지 확인할 수 있음
    공통 위젯(Widget)을 변경했을 때, 내가 수정한 화면뿐만 아니라 다른 화면에 미치는 영향 유무까지 차이(Diff) 개수로 파악할 수 있습니다. "의도하지 않은 변화가 일어나지 않았다"는 것을 확인할 수 있는 것은 생각 이상으로 안심이 되었습니다.

  • 공수 대비 효과가 큼
    AI 활용으로 테스트 구현 비용이 낮아지고, 비교는 CI가 알아서 해줍니다. 또한 AI에게 시각적 정보를 쉽게 전달할 수 있고, PR 생성 시간도 단축할 수 있었습니다. 들인 노력에 비해 얻는 효과가 상당히 크다고 느끼고 있습니다.

반대로 과제로는 "테스트를 늘릴 때마다 CI 실행 시간이 길어진다", "easy-vrt는 비교 이미지를 다운로드하는 것이 번거롭다" 등이 있으며, 이 부분은 계속해서 검토 및 개선해 나갈 예정입니다.

요약

본 기사에서는 VRT의 메커니즘을 차례대로 설명하면서, 운용 과정에서 효과적이었던 팁(Tips)을 소개했습니다.

유사한 시도를 하고 계신 분들의 지견이나, 검증 단말기 및 비교 도구에 관한 노하우가 있다면 꼭 공유해 주시면 감사하겠습니다.

We are hiring!

블루모 증권에서는 엔지니어를 적극적으로 채용하고 있습니다!

조금이라도 관심이 생기신 분은 부담 없이 아래 채용 페이지를 통해 연락해 주세요!

Discussion

AI 자동 생성 콘텐츠

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

원문 바로가기
0

댓글

0