본문으로 바로가기
TaeyoungKim.dev

Dart var와 dynamic 차이: 타입 추론과 런타임 오류는 언제 생길까?

앱작성 약 4분 읽기TaeyoungKim
LinkedInX

앱이 받은 JSON의 label이 늘 문자열일 거라 생각하고 대문자로 바꿨다. 테스트 데이터에서는 잘 됐는데, 숫자 404가 들어온 순간 실행 중 오류가 난다. var로 썼을 때는 코드를 작성하는 단계에서 막히던 일이 왜 dynamic에서는 실행할 때까지 숨어 있을까?

둘 다 변수 앞에 붙지만 맡는 일이 다르다. var는 타입을 생략해 추론하도록 하고, dynamic은 그 값에 대한 정적 검사를 느슨하게 한다. 같은 label로 차이를 확인해 보자.

Dart var는 타입을 바꾸는 변수일까?

초기값이 있으면 Dart는 그 값으로 변수의 정적 타입을 추론한다. 아래 title은 String으로 추론된다. 변수에 다른 문자열을 다시 넣을 수는 있지만, 정수를 넣을 수는 없다.

dart
var title = 'Ready'; // String으로 추론
title = 'Sent';      // 가능: 여전히 String
// title = 404;      // 분석 오류: int를 String 변수에 넣을 수 없음

주석 처리한 마지막 줄을 실제 코드로 바꿔 정적 분석을 실행하면 invalid_assignment가 나온다. var가 '무슨 값이든 받는다'는 뜻은 아니다. 이름만 보고 타입을 찾아야 하는 부담을 컴파일러에 맡긴 셈이다.

다만 초기값도 없고 타입을 알 다른 문맥도 없다면 var pending;는 dynamic이 된다. var라는 철자만으로 타입 안전성이 생기는 것은 아니다. 가능하면 초기값이나 명시적인 타입으로 의도를 드러내자.

dynamic에서는 왜 숫자에 문자열 메서드를 호출할까?

JSON은 앱 밖에서 들어온 값이다. 다음 예제는 label이 문자열인 응답과 숫자인 응답을 차례로 흉내 낸다. 둘 다 같은 코드로 읽는다.

도식처럼 var의 타입 불일치는 실행 전에 드러나지만, dynamic에 들어온 값의 잘못된 메서드 호출은 해당 경로가 실행될 때까지 미뤄진다. 같은 JSON 입력을 두 방식으로 읽어 차이를 확인해 보자.

dart
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로 실행하면 다음과 같이 나온다.

text
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?로 받아 타입을 확인하고, 확인한 값만 앱 로직에 넘기는 편이 오류를 이해하고 고치기 쉽다.

작성자

TaeyoungKim

기초 개념을 구현과 검증, 실제 운영 판단까지 연결해 기록합니다.

#Dart#var#dynamic#타입 추론#런타임 오류

함께 읽으면 좋은 글