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 |

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 옵션에 한정됩니다.
댓글은 닫혔습니다.