첫 Flutter 앱의 범위를 “화면 여러 개”나 “패키지 여러 개”로 잡으면 무엇이 완성됐는지 확인하기 어렵습니다. 더 작은 기준은 입력 하나가 규칙에 따라 상태를 바꾸고, 그 규칙이 자동 테스트로 고정되며, 목표 플랫폼의 릴리스 산출물까지 만들어지는가입니다.
2026년 9월 30일 Flutter 3.47.5 stable과 Dart 3.13.4로 Android·Web 대상 빈 프로젝트를 만든 뒤, 할 일 입력·빈값/중복 차단·추가·완료 전환·삭제를 구현했습니다. controller 테스트 2개와 widget 테스트 1개를 작성하고 format, analyze, test, Web release build를 실행했습니다. 13개 기대 조건이 모두 통과했고 실제 buildweb 화면도 로컬 HTTP로 열어 확인했습니다.
완료 기준을 명령과 화면으로 먼저 고정했다
| 완료선 | 이번 프로젝트의 조건 | 관찰 결과 |
|---|---|---|
| 입력 경계 | trim, 빈값 거절, 같은 제목 중복 거절 | controller 테스트 통과 |
| 상태 전이 | 추가, 완료 반전, ID로 삭제 | controller 테스트 통과 |
| 사용자 경로 | 빈 입력 오류 표시 후 유효 입력으로 한 행 생성 | widget 테스트 통과 |
| 정적 품질 | flutter analyze 오류·경고 없음 |
No issues found! |
| 릴리스 산출물 | Web release 폴더 생성 | √ Built buildweb |

테스트 출력은 +3: All tests passed!였고, analyze는 약 10.2초, Web release 빌드는 약 31.3초 걸렸습니다. 시간은 이 PC와 캐시 상태에 한정된 관찰값이며 성능 비교 자료가 아닙니다.
1단계: 화면보다 입력 규칙을 함수로 만든다
첫 구현은 TextField가 아니라 입력을 받아 상태를 바꾸는 규칙이었습니다. 공백 제거 후 빈 문자열과 중복 제목을 거절하고, 성공한 값만 목록에 넣도록 했습니다.
String? add(String rawTitle) {
final title = rawTitle.trim();
if (title.isEmpty) return '할 일을 입력하세요.';
if (_items.any((item) => item.title == title)) {
return '이미 있는 할 일입니다.';
}
_items.add(TaskItem(id: _nextId++, title: title));
notifyListeners();
return null;
}
오류 문자열을 UI에서 만드는 대신 controller가 반환하게 두면 같은 규칙을 버튼, 키보드 제출, 나중의 다른 화면에서도 재사용할 수 있습니다. 외부에는 UnmodifiableListView를 제공해 화면 코드가 목록을 우회 수정하지 못하게 했습니다. 이 구조가 모든 앱의 정답이라는 뜻은 아니지만, 이번 기능의 상태 변경 경로를 한 곳에서 관찰할 수 있게 만들었습니다.
2단계: 정상 입력보다 실패 입력을 먼저 테스트한다
테스트 세 개는 UI 모양을 복제하지 않고 기능 계약을 고정했습니다.
- 공백만 넣으면 오류가 나고, 앞뒤 공백은 제거되며, 같은 제목은 두 번 들어가지 않는다.
- 항목 ID로 완료 상태를 반전하고 삭제할 수 있다.
- 실제 위젯에서 빈 입력 오류가 보이고, 다음 유효 입력은 한 행으로 추가된다.
첫 앱에서 테스트 개수를 늘리는 것보다 “깨졌을 때 사용자에게 보이는 규칙”을 하나씩 고정하는 편이 낫습니다. 계산, 파싱, 검증처럼 UI와 분리된 규칙은 단위 테스트로 빠르게 돌리고, 입력→버튼→화면 변화처럼 연결이 중요한 경로만 widget 테스트로 남겼습니다.
3단계: 각 성공 뒤에 다음 계층을 하나만 추가한다
| 현재 통과 상태 | 다음에 하나만 추가할 것 | 다시 확인할 것 |
|---|---|---|
| 메모리 목록 | 로컬 저장소 | 재실행 후 복원, 손상 데이터, 마이그레이션 |
| 로컬 저장 | HTTP API | loading, timeout, 오류, 빈 결과 |
| 단일 화면 | 두 번째 route | 뒤로 가기와 입력 상태 보존 |
| widget 테스트 | 통합 테스트 | 실제 플러그인·플랫폼 경계 |
| Web release | Android release | JDK·서명·권한·실기기 설치 |
저장소, API, route, 상태관리 패키지를 동시에 추가하면 실패 원인이 다섯 곳으로 늘어납니다. 이번 앱은 패키지를 추가하지 않은 상태에서 입력과 상태 전이부터 고정했습니다. 다음 단계에서는 로컬 저장 하나만 붙이고 테스트를 다시 통과시키는 식으로 성공 범위를 넓힐 수 있습니다.
4단계: analyze와 test를 릴리스 빌드 앞에 둔다
dart format --output none --set-exit-if-changed lib test
flutter analyze
flutter test --reporter expanded
flutter build web --release
format·analyze·test가 먼저 실패하면 릴리스 빌드 시간을 쓰기 전에 코드 품질 문제를 확인할 수 있습니다. 이번 실행에서는 두 Dart 파일의 포맷 변경이 0개였고, analyze와 테스트가 모두 성공한 뒤 Web release 폴더가 생성됐습니다.
빌드 성공만으로 배포 완료라고 부르지는 않았습니다. 실제 서비스라면 base URL, 캐시, 서버의 SPA fallback, 접근성, 브라우저 호환, 오류 수집을 별도 확인해야 합니다. 이 글의 완료선은 로컬에서 확인한 release 산출물까지입니다.
Android 목표라면 같은 완료선을 기기까지 연장한다
프로젝트에는 Android 폴더도 생성됐지만 이 PC의 flutter doctor -v는 Android SDK를 찾은 뒤 No Java Development Kit (JDK) found를 보고했습니다. Android 기기도 연결되지 않았습니다. 따라서 APK·AAB 빌드와 기기 실행은 수행하지 않았고 성공으로 기록하지 않았습니다.
Android 앱을 목표로 할 때는 다음 조건이 추가로 모두 필요합니다.
- doctor의 Android toolchain에서 JDK·SDK·licenses 문제를 해소한다.
flutter devices에 사용할 emulator 또는 실기기가 나타난다.- 빈 프로젝트와 현재 앱을 모두 실제 기기에서 실행한다.
- release 서명 후 APK 또는 AAB를 만들고 설치·업데이트 경로를 시험한다.
- 권한 거절, 백그라운드 복귀, 네트워크 끊김처럼 플랫폼 상태를 확인한다.
SDK 준비 과정과 doctor 해석은 Windows Flutter 설치 직접 검증에 해시·명령 결과와 함께 분리했습니다. 로드맵 글에서 같은 설치 절차를 반복하지 않는 이유입니다.
첫 앱의 범위를 줄이는 결정표
| 넣으려는 기능 | 첫 버전에 남길 최소 범위 | 뒤로 미룰 수 있는 것 |
|---|---|---|
| 로그인 | 로컬 가짜 사용자와 화면 전환 | 소셜 로그인·토큰 갱신 |
| 목록 | 추가·완료·삭제와 빈 상태 | 검색·필터·드래그 정렬 |
| 저장 | 한 모델의 저장·복원·손상 처리 | 동기화·충돌 해결 |
| 네트워크 | 한 endpoint의 loading·성공·실패 | 재시도 큐·오프라인 캐시 |
| 배포 | 한 목표 플랫폼의 release 산출물 | 모든 플랫폼 동시 출시 |
이번 실행으로 확인하지 않은 범위
- 로컬 데이터베이스, HTTP 요청, 로그인, 푸시 알림 패키지는 추가하지 않았습니다.
- Web release 산출물은 로컬 HTTP에서 열었으며 실제 운영 호스팅에는 배포하지 않았습니다.
- Android JDK와 기기가 없어 Android 빌드·설치·성능을 확인하지 않았습니다.
- iOS는 macOS·Xcode·서명 환경이 없어 대상에 포함하지 않았습니다.
- 세 테스트는 이번 최소 기능의 계약만 확인하며 앱 전체 품질을 보장하지 않습니다.