입력 검증에서 if는 “왜 거부했는가”를 결정하고, while은 “다음 입력을 받을 것인가”를 결정합니다. 두 역할을 한 함수에 묶어 무한 반복시키면 UI, 자동 테스트, 입력 종료를 다루기 어려워집니다.
2026년 10월 4일 Dart 3.13.4에서 나이 입력 12종, 가입 경로 4종, 재시도 종료 3종을 실행했습니다. 빈 값·형식 오류·범위 오류를 서로 다른 결과로 보존하고, 성공·최대 시도 도달·입력 소진을 구분한 19개 조건이 모두 통과했습니다.
검증 전에 입력 계약을 한 줄씩 정한다
| 항목 | 이번 예제의 계약 | 거부 결과 |
|---|---|---|
| 공백 | 앞뒤 공백은 제거 | 제거 후 비면 empty |
| 문자 집합 | ASCII 숫자 0~9만 허용 | nonDecimal |
| 범위 | 0~120 포함 | outOfRange |
| 선택지 | 1·2·3만 허용 | null |
| 재시도 | 호출자가 최대 횟수 지정 | attemptLimit |
int.tryParse()는 부호가 있는 정수 리터럴도 받을 수 있습니다. 이 예제에서는 나이에 +18이나 -1을 허용하지 않는 제품 규칙을 먼저 적용하려고 정규식으로 ASCII 숫자만 확인한 뒤 파싱했습니다. 파서가 받아들이는 문법과 제품이 허용하는 입력은 같지 않을 수 있습니다.

문자열 대신 성공과 실패 타입을 반환한다
Validation<int> validateAge(String raw) {
final value = raw.trim();
if (value.isEmpty) return Invalid(AgeError.empty);
if (!RegExp(r'^[0-9]+$').hasMatch(value)) {
return Invalid(AgeError.nonDecimal);
}
final age = int.tryParse(value);
if (age == null || age < 0 || age > 120) {
return Invalid(AgeError.outOfRange);
}
return Valid(age);
}
오류 문구를 검증 함수에서 바로 만들지 않았습니다. 도메인 코드는 AgeError만 반환하고, 화면 계층이 언어와 상황에 맞는 문구를 선택합니다. 이렇게 하면 다국어 문구 변경이 검증 로직을 바꾸지 않고, 테스트도 문자열 전체가 아닌 판정값을 확인할 수 있습니다.
12개 입력에서 경계를 일부러 흔들었다
| 입력 | 결과 | 확인한 경계 |
|---|---|---|
"", 공백 |
empty | trim 후 빈 값 |
abc, -1, +18 |
nonDecimal | 부호와 문자를 허용하지 않음 |
18.0, 전각 12 |
nonDecimal | 소수와 비 ASCII 숫자 |
121, 매우 긴 숫자 |
outOfRange | 상한과 파싱 실패 |
0, 120 |
Valid | 포함 경계 |
" 018 " |
Valid(18) | 공백 제거와 선행 0 |
어떤 입력을 허용할지는 서비스에 따라 다릅니다. 국제화된 숫자 입력을 지원해야 한다면 전각 숫자를 무조건 거부할 것이 아니라 입력 정규화 정책과 로케일을 별도로 설계해야 합니다.
while은 무한 반복이 아니라 세 종료 조건을 가진다
검증 함수와 별개로 재시도 제어를 만들고 세 경로를 실행했습니다.
x → 121 → 20: 세 번째 입력에서accepted(20)- 같은 입력을 최대 2회로 제한:
attemptLimit x하나만 제공: 한 번 거부 후endOfInput
while (attempts < maxAttempts) {
if (!iterator.moveNext()) return endOfInput(attempts);
attempts++;
final result = validateAge(iterator.current);
if (result case Valid<int>(:final value)) {
return accepted(value, attempts);
}
}
return attemptLimit(attempts);
while (true)가 항상 잘못된 것은 아니지만, 종료가 사용자 입력 하나에만 의존하면 자동 테스트와 취소 처리가 어려워집니다. 최대 횟수, 입력 종료, 취소 신호 중 무엇이 루프를 끝내는지 반환값으로 구분합니다.
고정 선택지는 switch가 누락을 드러낸다
SignUpChannel? parseChannel(String raw) => switch (raw.trim()) {
'1' => SignUpChannel.email,
'2' => SignUpChannel.kakao,
'3' => SignUpChannel.naver,
_ => null,
};
1·2·3은 각각 enum으로 변환되고 4는 null이 됐습니다. 숫자 선택지를 앱 내부까지 문자열로 들고 가지 않으면 이후 분기에서 오타가 줄고, enum을 switch할 때 빠진 경우를 컴파일러의 완전성 검사로 찾을 수 있습니다.
CLI 반복문을 Flutter 화면에 그대로 옮기지 않는다
콘솔은 다음 줄을 기다릴 수 있지만 Flutter UI는 이벤트마다 다시 호출됩니다. TextField 제출 시 한 번 검증하고 오류 상태를 화면에 표시하면 됩니다. 원격 중복 확인처럼 비동기 검증이 필요하면 진행 상태와 취소·최신 요청 판정을 따로 둡니다. UI 스레드에서 입력을 기다리는 while을 돌리면 화면이 멈출 수 있습니다.
| 환경 | 반복의 주체 | 종료 신호 |
|---|---|---|
| CLI | 코드의 while | 정상 입력·EOF·시도 제한 |
| Flutter 폼 | 사용자 이벤트 | 검증 통과·취소·화면 이탈 |
| 서버 요청 | 재시도 정책 | 성공·비재시도 오류·횟수/시간 한도 |
이번 실행의 한계
- Dart VM에서 순수 검증 함수만 실행했고 실제 Flutter TextField나 키보드는 자동화하지 않았습니다.
- 나이 0~120과 ASCII 숫자는 예제 정책이며 법적 연령 확인이나 신원 검증 기준이 아닙니다.
- 비밀번호 강도, 서버 중복 확인, 네트워크 재시도는 다루지 않았습니다.
- 오류 문구의 접근성·현지화 품질은 별도 사용자 테스트가 필요합니다.
분기 설계와 UI 입력으로 이어가기
복합 권한 조건을 표로 검증하는 방법은 앱 조건문을 안전하게 만드는 불리언 대수에서, Flutter 컨트롤러 생명주기와 폼 입력은 Flutter TextField 입력값 받기에서 분리해 다룹니다.