Widget Previewer를 Human/AI 친화적으로 만든 CLI 도구, shutter
요약
Flutter 위젯의 스크린샷을 PNG로 추출하고 비교하는 CLI 도구 'shutter'가 소개되었다. 이 도구는 코드 변경에 따른 UI 변화를 시각적으로 포착하며, 특히 AI 에이전트가 생성한 코드의 검증 및 디버깅 과정에서 강력한 기준 자료(diff image)를 제공한다.
핵심 포인트
- Flutter 위젯을 PNG로 추출하고 비교하는 CLI 도구입니다.
- 코드 변경에 따른 UI 변화를 시각적으로 포착하여 버그 발견에 도움을 줍니다.
- AI 에이전트가 생성한 코드의 검증 기준 자료(diff image)를 제공합니다.
- 스크롤이나 기기 조건 없이 위젯 단독으로 촬영 및 비교가 가능합니다.
서론
Flutter 위젯을 원하는 시점에 PNG로 추출하는 CLI 도구를 만들었습니다.
외관 변경은 외관으로만 확인할 수 있다
Flutter를 채택한 프로젝트에서는 많은 경우 PR(또는 MR) 템플릿에 스크린샷 첨부란이 있을 것입니다. 저는 다소 게으른 편이라, 귀찮다는 생각으로 iPhone 시뮬레이터를 실행하고 Command+S로 스크린샷을 찍습니다.
하지만 스크린샷의 힘은 과소평가할 수 없습니다. 코드를 작성할 때는 완벽하다고 생각했던 것이 실제로 이미지를 비교해 보면 전혀 그렇지 않은 경우가 많습니다. Flutter에서는 아무 플랫폼에서 확인하면 대부분 괜찮지만, 이 단계의 중요성은 변하지 않습니다.
또한 AI 에이전트에게 코드 작성을 맡기게 되어도 이 흐름은 바뀌지 않습니다. AI가 만든 코드를
에서는 run이라고 부르며, 이것이 비교의 기준이 됩니다.
$ shutter shot lib/preview/button_preview.dart
# shutter ai-report v1
run: /path/to/example/.dart_tool/shutter/runs/20260920T105416Z
...
PrimaryButton의 padding을 horizontal: 24에서 horizontal: 40으로 변경하고, 다시 촬영합니다. 그리고 두 개의 run을 비교합니다.
$ shutter diff latest~1 latest --images
# shutter ai-report v1
diff: /path/to/example/.dart_tool/shutter/diffs/20260920T105442Z
...
버튼의 세 가지 프리뷰가 changed 상태가 되고, 너비가 66.69에서 98.69로 확장된 것이 숫자로 나옵니다. 반면, 네 번째 LoginForm은 unchanged였습니다. 이 화면 역시 PrimaryButton을 사용하고 있지만, CrossAxisAlignment.stretch로 가로 폭 전체에 늘여 놓았기 때문에, 가로 padding을 바꿔도 렌더링은 변하지 않습니다. 코드를 따라가기만 하면 '사용했으니까 바뀔 것이다'라고 판단해 버리는 경우입니다.
--images를 붙이면, 차이점(diff) 픽셀을 빨간색으로 칠한 이미지도 출력됩니다. 이미지나 run의 경로는 절대 경로로 출력되므로, 그대로 이미지 뷰어나 PR에 전달할 수 있습니다.
실제 기기의 스크린샷과 다른 점은, 맞춰야 할 조건 자체가 없다는 것입니다.
스크롤하지 않으면 보이지 않는 Widget의 경우, 실제 기기에서는 이전/이후 스크롤량을 일치시키지 않으면 비교가 불가능합니다. shutter는 Widget을 단독으로도 촬영할 수 있기 때문에, 스크롤해서 화면에 띄우는 과정이 필요 없습니다. 크기도 @Preview의 size로 결정되므로, 기기 차이도 발생하지 않습니다. 의도했던 것은 아니었지만, 편리하게 느껴집니다.
프리뷰 파일을 작성하지 않고 촬영하기
'이 Widget을 여기서 사용하면 어떻게 보일까'를 확인하고 싶을 때만은, 커맨드라인에 식(expression)을 직접 전달할 수 있습니다. 사람에게는 다소 어려운 조작이지만, AI에게는 쉬울 것 같아서 추가해 보았습니다.[1]
shutter shot --widget 'PrimaryButton(label:
AI 에이전트에게 맡긴다는 관점에서도, 기준 이미지가 없는 설계에는 의미가 있습니다. golden test를 AI가 처리하게 하면, 차이가 발생했을 때 '기준 이미지를 업데이트했습니다'로 끝내버리는 경우가 있습니다. `shutter`는 업데이트할 기준 자체가 없기 때문에, 이러한 속임수의 경로가 없습니다. 대신 차이 이미지(diff image)가 남게 되므로, 판단의 근거 자료는 사람에게 돌아오게 됩니다.
## AI 에이전트에게 전달하기
`shutter`는 AI 에이전트가 사용한다는 전제하에 설계되었습니다.
### 플레이북을 바이너리에 포함
`shutter agent`를 실행하면, 절차서(playbook)가 출력됩니다. 피사체를 정하고, 편집 전에 촬영하고, 편집한 후에 다시 촬영하며, `diff`를 읽는 흐름입니다. `shutter manual`은 개념적인 참고 자료입니다.
문서를 바이너리에 포함하는 이유는 설치된 버전과 절차서의 내용이 어긋나지 않도록 하기 위함입니다. 그리고 또 하나, AI가 사용법을 모른다고 떼쓰는 상황을 피하기 위해서이기도 합니다. AI 입장에서 `shutter`는 미지의 패키지이기 때문에, 틀림없이 '저는 사용법을 모릅니다'라며 헤매게 됩니다. 이를 방어하는 장치입니다.
패키지에는 agent skill인 `shutter-visual-check`도 포함되어 있습니다.
만드는 과정에서 Flutter SDK 측에 느낀 점을 적어두겠습니다.
현재 v1은 Widget Previewer가 이미지를 출력할 수 없다는 전제하에, `flutter test`를 경유하여 렌더링하고 있습니다. 이는 임시방편의 축적일 뿐, 이상적인 설계는 아닙니다. Font 문제, `flutter/material.dart`가 `material_ui/material_ui.dart`로 마이그레이션되는 문제 등 여러 문제를 거치고 있습니다. 또한, Widget Previewer가 `lib/` 디렉토리 아래만 볼 수 있다는 제약 등의 영향도 받고 있습니다.
이러한 문제들의 대부분은 `flutter widget-preview`가 `ui.Image`를 출력해 준다면 해결될 것입니다. `shutter`의 내부 구조는 엔진 계층을 교체할 수 있도록 분리되어 있기 때문에, SDK 측 개선이 이루어지면 run이나 diff의 설계를 변경하지 않고도 대체할 수 있습니다.[3]
세상이 AI에 의한 구현을 지원하는 방향으로 가고 있기 때문에, 지식을 쌓아간다면 좋은 방향으로 나아갈 것 같은 느낌입니다. Widget Previewer는 흥미로운 메커니즘이므로, 관심 있는 분들은 꼭 사용해 보세요.
## 맺음말
`shutter`는 Flutter 개발 루프에서 사람의 손에 남기 쉬웠던 '시각적 확인'을 이미지라는 형태로 기계에게 전달하려는 시도입니다. 사람이 직접 테스트해도 편리하고, AI 에이전트에게 전달해도 동일하게 작동합니다. 외관과 관련된 변경 사항을 AI에게 맡기고 계신 분들은 꼭 사용해 보세요.
agent skill의 사용 편의성이나 PR에 붙여넣는 기능에 대해서는 필자도 아직 감을 잡지 못했습니다. 사용해보시고 느낀 점이나 요청사항이 있다면 꼭 보내주세요. 기다리겠습니다!
### 토론

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