앱이 받은 JSON의 label이 늘 문자열일 거라 생각하고 대문자로 바꿨다. 테스트 데이터에서는 잘 됐는데, 숫자 404가 들어온 순간 실행 중 오류가 난다. var로 썼을 때는 코드를 작성하는 단계에서 막히던 일이 왜 dynamic에서는 실행할 때까지 숨어 있을까?
둘 다 변수 앞에 붙지만 맡는 일이 다르다. var는 타입을 생략해 추론하도록 하고, dynamic은 그 값에 대한 정적 검사를 느슨하게 한다. 같은 label로 차이를 확인해 보자.
Dart var는 타입을 바꾸는 변수일까?
초기값이 있으면 Dart는 그 값으로 변수의 정적 타입을 추론한다. 아래 title은 String으로 추론된다. 변수에 다른 문자열을 다시 넣을 수는 있지만, 정수를 넣을 수는 없다.
var title = 'Ready'; // String으로 추론
title = 'Sent'; // 가능: 여전히 String
// title = 404; // 분석 오류: int를 String 변수에 넣을 수 없음주석 처리한 마지막 줄을 실제 코드로 바꿔 정적 분석을 실행하면 invalid_assignment가 나온다. var가 '무슨 값이든 받는다'는 뜻은 아니다. 이름만 보고 타입을 찾아야 하는 부담을 컴파일러에 맡긴 셈이다.
다만 초기값도 없고 타입을 알 다른 문맥도 없다면 var pending;는 dynamic이 된다. var라는 철자만으로 타입 안전성이 생기는 것은 아니다. 가능하면 초기값이나 명시적인 타입으로 의도를 드러내자.
dynamic에서는 왜 숫자에 문자열 메서드를 호출할까?
JSON은 앱 밖에서 들어온 값이다. 다음 예제는 label이 문자열인 응답과 숫자인 응답을 차례로 흉내 낸다. 둘 다 같은 코드로 읽는다.
도식처럼 var의 타입 불일치는 실행 전에 드러나지만, dynamic에 들어온 값의 잘못된 메서드 호출은 해당 경로가 실행될 때까지 미뤄진다. 같은 JSON 입력을 두 방식으로 읽어 차이를 확인해 보자.
import 'dart:convert';
void main() {
var title = 'Ready';
title = 'Sent';
print('var: ${title.toUpperCase()}');
for (final json in ['{"label":"Ready"}', '{"label":404}']) {
final payload = jsonDecode(json) as Map<String, dynamic>;
dynamic unchecked = payload['label'];
try {
print('dynamic: ${unchecked.toUpperCase()}');
} on NoSuchMethodError {
print('dynamic: method failed (${unchecked.runtimeType})');
}
Object? checked = payload['label'];
if (checked is String) {
print('checked: ${checked.toUpperCase()}');
} else {
print('checked: reject (${checked.runtimeType})');
}
}
}파일을 main.dart로 저장해 dart run main.dart로 실행하면 다음과 같이 나온다.
var: SENT
dynamic: READY
checked: READY
dynamic: method failed (int)
checked: reject (int)dynamic의 값 자체가 마법처럼 타입을 바꾼 게 아니다. 첫 응답에서는 String 객체, 둘째 응답에서는 int 객체가 같은 변수에 담겼다. dynamic이므로 분석기는 toUpperCase() 호출을 미리 막지 않았고, 숫자를 받은 뒤에야 해당 메서드가 없어 실패했다. 테스트에서 문자열만 넣으면 이 오류가 조용히 숨어 있는 이유다.
예제의 try/catch는 오류가 나는 시점을 보여 주기 위한 장치다. 실제 입력 검증을 메서드 오류 잡기로 대신하지는 말자.
JSON처럼 외부에서 온 값은 어떻게 검사할까?
위 코드의 Object? checked는 값의 타입을 먼저 모른다는 사실을 드러낸다. checked is String을 통과한 블록에서만 문자열 메서드를 호출한다. 숫자가 들어오면 막연한 메서드 오류 대신 예상하지 않은 입력으로 처리할 수 있다.
| 선언 | 이 예제에서 확인한 일 | 오류를 발견하는 시점 |
|---|---|---|
var title = 'Ready' | String으로 추론돼 404 대입이 거부됨 | 정적 분석 |
dynamic unchecked | 숫자 대입과 메서드 호출을 허용함 | 잘못된 호출을 실행할 때 |
Object? checked + is String | 타입을 확인한 뒤 문자열 메서드를 사용함 | 입력을 검사할 때 |
앱 화면의 표시 문구가 반드시 문자열이어야 한다면 숫자를 임의로 문자열로 바꾸기 전에 그 입력이 정상인지 결정해야 한다. 404가 의도된 상태 코드인지, 서버 응답 형식이 깨진 것인지에 따라 처리 방법이 달라진다. dynamic을 전부 금지할 필요는 없지만, JSON처럼 경계에서 들어온 값을 앱 내부까지 그대로 흘려보내면 오류가 뒤늦게 나타난다.
핵심 요약
초기값이 있는 var는 타입을 추론할 뿐, 아무 타입이나 받는 변수가 아니다. dynamic은 정적 검사를 건너뛰므로 잘못된 메서드 호출이 실행 중에 드러날 수 있다. 외부 입력이라면 Object?로 받아 타입을 확인하고, 확인한 값만 앱 로직에 넘기는 편이 오류를 이해하고 고치기 쉽다.

