Dart var·final·const 차이: 변수 선언 기준과 예제

var, final, const는 단순히 “변경 가능·불가능” 한 줄로 나누면 실수하기 쉽습니다. 선언한 변수가 다른 객체를 다시 가리킬 수 있는지, 그 객체 내부를 바꿀 수 있는지, 초깃값이 런타임에 정해지는지, 같은 상수 값이 canonical instance로 합쳐지는지를 따로 봐야 합니다.

2026년 9월 29일 Windows의 Dart SDK 3.13.4 stable에서 성공 예제 한 개와 실패 예제 세 개를 dart analyze --format machine과 dart run으로 실행했습니다. 입력값은 명령줄에서 받은 문자열 41, List는 [1, 2], const constructor 값은 7로 고정했습니다.

세 키워드를 네 가지 질문으로 실행했다

질문 실행 결과 의미
var에 같은 타입을 다시 넣을 수 있나? 1 → 2, 분석·실행 성공 변수 재할당 가능
추론된 var int에 String을 넣을 수 있나? INVALID_ASSIGNMENT var는 dynamic이 아님
final List에 add할 수 있나? [1, 2] → [1, 2, 3] 참조 고정과 객체 불변은 다름
명령줄 입력을 const로 parse할 수 있나? CONST_EVAL_METHOD_INVOCATION 런타임 계산은 const 초깃값이 아님
같은 const List·Point는 같은 객체인가? identical == true 동일 상수 값 canonicalization
Dart var final const의 재할당 객체 변경 컴파일 타임 상수 차이를 analyzer와 VM으로 검증한 결과
2026년 9월 29일 Dart SDK 3.13.4에서 재할당, final List 변경, const 런타임 제한과 canonicalization을 analyzer와 VM으로 실행한 결과입니다.

13개 기대 조건이 모두 통과했습니다. 성공 파일은 analyzer 종료 코드 0과 VM 종료 코드 0을 기록했습니다. 실패 파일은 의도한 줄에서 각각 INVALID_ASSIGNMENT, ASSIGNMENT_TO_FINAL_LOCAL, CONST_EVAL_METHOD_INVOCATION을 냈습니다.

var retryCount = 1;
retryCount = 2;              // 성공: 같은 int 재할당
// retryCount = 'two';       // INVALID_ASSIGNMENT

final parsed = int.parse(arguments.single); // 성공: 런타임 값
final values = <int>[1, 2];
values.add(3);               // 성공: List 내부 변경
// values = [4, 5];          // final 참조 재할당은 실패

const fixed = <int>[1, 2];
// fixed.add(3);             // 실행하면 UnsupportedError
// const parsed = int.parse(input); // const 평가에서 메서드 호출 실패

var는 타입을 생략하는 문법이지 타입 검사를 끄지 않는다

var retryCount = 1에서 정적 타입은 int로 추론됐습니다. 값을 2로 바꾼 파일은 정상 실행됐지만 'two'를 대입한 별도 파일은 analyzer가 컴파일 타임 오류로 막았습니다. 값이 바뀔 수 있다는 것과 값의 타입이 아무거나 될 수 있다는 것은 다른 조건입니다.

로컬 변수에서 초깃값이 타입을 명확히 보여 준다면 var를 쓸 수 있습니다. API 반환형·매개변수·빈 컬렉션처럼 계약을 읽는 사람이 알아야 하는 경계에서는 타입을 명시하는 판단이 따로 필요합니다. 이 위치별 판단은 Dart에서 var와 명시 타입을 선택하는 기준의 analyzer 사례와 이어집니다. 외부 데이터 경계에서 dynamic과 Object?의 실패 시점을 구분하려면 var·dynamic·Object? 실행 비교를 함께 볼 수 있습니다.

final은 참조 재할당을 막지만 객체 내부까지 얼리지 않는다

final requestId에 두 번째 값을 넣은 파일은 ASSIGNMENT_TO_FINAL_LOCAL로 실패했습니다. 하지만 final values = [1, 2]가 가리키는 List에 add(3)을 호출한 파일은 [1, 2, 3]을 출력했습니다.

즉 final의 질문은 “이 변수가 다른 객체를 다시 가리킬 수 있는가?”입니다. “그 객체의 메서드가 내부 상태를 바꾸는가?”는 타입의 API와 구현을 따로 확인해야 합니다. 변경 불가능한 컬렉션이 필요하다면 단순히 변수에 final을 붙이는 것만으로는 요구 사항을 충족하지 않습니다.

코드 변수 재할당 List 내부 변경
var a = <int>[] 가능 가능
final a = <int>[] 불가 가능
const a = <int>[] 불가 실행 시 UnsupportedError

const는 선언 위치보다 값이 상수 컨텍스트인지 본다

const 변수는 컴파일 타임 상수로 초기화해야 합니다. 이번에 명령줄 인자를 int.parse로 바꾼 값은 실행 때 정해지므로 final에는 넣을 수 있었지만 const에는 넣을 수 없었습니다. 반면 숫자·문자열 literal, 다른 const 값, 허용되는 상수 연산, const constructor 결과는 상수 컨텍스트를 만들 수 있습니다.

const constructor가 있다고 모든 호출이 상수가 되는 것도 아닙니다. 같은 Point constructor를 런타임 값으로 두 번 호출했을 때 identical은 false였고, const 컨텍스트의 동일한 Point(7) 두 개는 true였습니다. 동일한 const List 두 개도 true였습니다. const 생성자·컬렉션·canonicalization에서 객체 설계 조건을 더 자세히 다룹니다.

이 결과만으로 Flutter에서 const 위젯이 몇 프레임을 절약한다고 말할 수는 없습니다. 이번 실행은 객체 동일성과 언어 오류만 확인했으며 위젯 rebuild 횟수·메모리·프레임 시간은 측정하지 않았습니다.

값의 출처와 변경 범위로 선언을 고르는 표

실제 요구 선언 후보 코드 리뷰에서 확인할 반례
같은 타입의 값으로 다시 할당해야 함 var 또는 명시 타입 정말 상태 변경이 필요한가?
API·입력·현재 시각 결과를 한 번만 보관 final 객체 내부 변경도 금지해야 하는가?
빌드 전에 값이 완전히 결정됨 const 메서드 호출·입력·환경값이 섞였는가?
변경 불가능한 런타임 데이터 final + 불변 타입/방어적 복사 final List만으로 충분하다고 오해했는가?
나중에 한 번 초기화되는 non-null 값 제어 흐름 또는 신중한 late final 읽기 전에 초기화되지 않을 경로가 있는가?

late와 nullable 타입은 초기화 시점·null 가능성이라는 또 다른 축입니다. 해당 경계는 Dart null safety에서 ?, !, late를 고르는 기준과 대조할 수 있습니다.

이번 실행의 범위 밖

  • Dart 명령줄 analyzer와 단일 isolate VM만 실행했으며 Flutter 앱이나 위젯 트리를 만들지 않았습니다.
  • const 사용에 따른 rebuild·메모리·프레임 성능은 측정하지 않았습니다.
  • canonicalization은 같은 isolate 안의 동일한 const List와 const constructor 결과를 identical로 비교했습니다.
  • final 객체 변경은 List의 add만 실행했으며 모든 클래스가 변경 가능하다고 일반화하지 않습니다.
  • 진단명과 결과는 Dart SDK 3.13.4 및 기록한 strict analyzer 옵션에 한정됩니다.

실험 결과와 대조한 Dart 공식 자료

댓글은 닫혔습니다.