const를 “값을 바꾸지 못하게 하는 키워드”로만 이해하면 생성자, 컬렉션, Flutter 위젯에서 서로 다른 현상을 한데 묶게 됩니다. Dart에서 const는 컴파일 시점에 평가되는 상수 객체를 만들고, 같은 상수 표현식은 하나의 canonical instance로 정규화합니다. final은 변수의 재할당만 막기 때문에 객체 내부 변경과 정규화를 보장하지 않습니다.
2026년 10월 1일 Windows 10 x64에서 Dart 3.13.4와 Flutter 3.47.5로 이 차이를 실행했습니다. const Point, 중첩 Map·List, 일반 생성 객체, 가변·불변 List를 비교하고 잘못된 const 코드에는 analyzer를 실행했습니다. Flutter에서는 기본 디버그 테스트와 --no-track-widget-creation 테스트를 따로 돌려 const 위젯 정체성이 왜 다르게 관찰되는지도 확인했습니다.
실행 결과부터 보면 const의 역할이 세 갈래로 나뉜다
| 실험 | 관찰 결과 | 해석 |
|---|---|---|
같은 const Point(1, 2) 두 개 |
identical == true |
같은 상수 표현식이 정규화됨 |
같은 일반 Point(1, 2) 두 개 |
identical == false |
값이 같아도 별도 객체 |
| 같은 const Map과 내부 List | 둘 다 true |
상수 컨텍스트가 중첩 객체 그래프에 적용됨 |
final List에 add |
[1, 2, 3] |
재할당만 금지, 내부는 가변 |
const List에 add |
UnsupportedError |
상수 컬렉션 자체가 불변 |

이 결과에서 메모리를 몇 바이트 절약했는지는 측정하지 않았습니다. 정규화 여부는 identical로 확인했지만, 실제 앱의 할당량과 프레임 시간은 프로파일러 측정 없이 단정할 수 없습니다.
const 생성자는 상수 객체를 만들 수 있는 자격이다
class Point {
const Point(this.x, this.y);
final int x;
final int y;
}
const generative constructor를 선언하려면 인스턴스 필드가 final이어야 합니다. 이번에 가변 int x를 가진 클래스에 const 생성자를 선언하자 analyzer가 CONST_CONSTRUCTOR_WITH_NON_FINAL_FIELD로 차단했습니다.
생성자가 const라고 모든 호출이 상수가 되는 것은 아닙니다.
const a = Point(1, 2); // 상수 객체
const b = Point(1, 2); // a와 identical
final c = Point(1, 2); // const 컨텍스트가 아닌 일반 호출
final d = Point(1, 2); // c와 identical하지 않음
const Point는 클래스가 상수 객체를 만들 수 있다는 선언이고, 호출 위치의 const 컨텍스트가 실제 상수화를 결정합니다. 변수 이름에 final을 붙이는 것과 객체 생성 표현식을 const로 평가하는 것은 별개의 선택입니다.
canonicalization은 값 비교가 아니라 객체 정체성 규칙이다
a == b는 클래스가 정의한 값 비교일 수 있지만, identical(a, b)는 두 참조가 같은 객체인지 확인합니다. Dart 실행에서 동일한 const Point는 별도의 operator == 구현 없이도 identical == true였습니다. 일반 Point는 필드 값이 같아도 false였습니다.
중첩 const 객체 그래프도 확인했습니다.
const graphA = <String, Object>{
'label': 'origin',
'points': <Point>[Point(0, 0), Point(1, 2)],
};
const graphB = <String, Object>{
'label': 'origin',
'points': <Point>[Point(0, 0), Point(1, 2)],
};
Map뿐 아니라 graphA['points']와 graphB['points']도 동일한 인스턴스로 확인됐습니다. 바깥 const Map이 내부 List와 Point 생성자를 상수 컨텍스트로 만들기 때문에 내부의 const 표기는 생략할 수 있습니다.
analyzer가 막은 두 종류의 잘못된 const
아래 파일은 analyzer 종료 코드 3과 세 개의 진단을 냈습니다.
class MutablePoint {
const MutablePoint(this.x); // 가변 필드 때문에 오류
int x;
}
const runtimeValue = DateTime.now(); // 런타임 값이라 오류
| 진단 코드 | 의미 | 수정 방향 |
|---|---|---|
CONST_CONSTRUCTOR_WITH_NON_FINAL_FIELD |
const 객체가 가변 필드를 가짐 | 필드를 final로 만들거나 생성자에서 const 제거 |
CONST_INITIALIZED_WITH_NON_CONSTANT_VALUE |
const 변수의 초기값이 컴파일 타임 상수가 아님 | 런타임 값이면 final 사용 |
CONST_WITH_NON_CONST |
호출한 생성자가 const 생성자가 아님 | API 계약을 확인하고 일반 생성으로 변경 |
현재 시간, 네트워크 응답, 파일 내용, 사용자 입력은 실행 시점에 정해지므로 const가 아니라 보통 final 변수에 담습니다. “나중에 바꾸지 않는다”와 “컴파일 전에 값을 안다”를 구분하면 대부분의 const 진단을 설명할 수 있습니다.
final 컬렉션과 const 컬렉션은 실패 지점이 다르다
final mutable = <int>[1, 2];
mutable.add(3); // 성공: [1, 2, 3]
const immutable = <int>[1, 2];
immutable.add(3); // 실행 시 UnsupportedError
final은 mutable = 다른List 같은 재할당을 막습니다. add, remove, 인덱스 대입처럼 같은 List 내부를 바꾸는 동작은 허용합니다. 반대로 const List는 객체 자체가 불변이므로 변경 메서드가 런타임에서 실패합니다.
외부에 읽기 전용 컬렉션을 제공하려는 API라면 const literal만으로는 충분하지 않을 수 있습니다. 입력이 런타임에 들어오면 const로 만들 수 없기 때문입니다. 그때는 복사와 읽기 전용 뷰 같은 별도 설계가 필요하며, 이 글의 실험은 해당 API의 성능과 별칭 효과를 측정하지 않았습니다.
Flutter 디버그에서 같은 const 위젯이 identical하지 않았던 이유
기본 flutter test에서 서로 다른 줄에 선언한 const Text('정적') 두 개와 같은 const Row 두 개는 모두 identical == false였습니다. 같은 테스트를 --no-track-widget-creation으로 실행하자 둘 다 true가 됐습니다.
Flutter 디버그 모드는 Inspector가 위젯 생성 위치를 보여 주도록 widget creation tracking을 기본 활성화합니다. 공식 Inspector 문서도 이 계측이 otherwise-identical const Widget을 디버그 빌드에서 같지 않게 만들 수 있다고 설명합니다. 따라서 디버그 테스트의 identical 결과 하나만으로 release 빌드의 const 정규화가 깨졌다고 판단하면 안 됩니다.
| 실행 방식 | const Text | const Row | 사용 목적 |
|---|---|---|---|
기본 flutter test |
false | false | 생성 위치 추적이 켜진 디버그 검사 |
flutter test --no-track-widget-creation |
true | true | 위치 계측을 제외한 정체성 확인 |
두 테스트는 각각 1개씩 통과했고 Flutter analyzer는 문제 없음, Web release 빌드는 성공했습니다. 이 비교는 const를 성능 수치로 환산한 벤치마크가 아니라, 디버그 계측이 관찰 결과에 끼어드는 조건을 분리한 실험입니다.
Flutter에서 const가 해 주는 것과 해 주지 않는 것
- 같은 인수의 const 위젯을 자동 캐싱할 수 있게 해 동일 인스턴스를 재사용하는 경로를 만듭니다.
- Element가 이전 자식과 같은 위젯 인스턴스를 다시 만나면 불필요한 하위 업데이트를 줄일 수 있습니다.
- 부모의
build호출 자체를 금지하지 않습니다. - 상태가 바뀌는 Text나 런타임 입력을 억지로 const로 만들 수는 없습니다.
- 렌더링 병목, 레이아웃 비용, 이미지 디코딩 비용을 const 하나로 해결하지는 않습니다.
정적인 아이콘·간격·라벨은 const 후보입니다. 반대로 사용자 이름, 카운터, MediaQuery 값처럼 실행 중 달라지는 인수는 일반 위젯으로 남겨야 합니다. 가장 작은 const 개수를 세는 것보다 변경되는 데이터의 경계를 명확히 나누는 편이 유지보수와 최적화 모두에 유리합니다.
코드 리뷰에서는 네 질문으로 결정한다
- 값이 컴파일 전에 확정되는가? 아니라면 const가 아니라 final 또는 일반 변수입니다.
- 객체 내부까지 바뀌면 안 되는가? 재할당만 막으면 된다면 final로 충분할 수 있습니다.
- 같은 인수의 객체를 반복 생성하는가? 그렇다면 const 생성자와 const 컨텍스트가 정규화 기회를 줍니다.
- 성능이 실제 문제인가? DevTools 프로파일링 없이 “const라서 빨라졌다”고 결론 내리지 않습니다.
이번 실험에서 확인하지 않은 범위
- Android·iOS 실기기의 메모리 사용량과 프레임 시간은 측정하지 않았습니다.
- release Web 앱 내부의 객체 수를 힙 스냅샷으로 비교하지 않았습니다.
- freezed, built_value 같은 불변 모델 도구와 상태 관리 라이브러리는 다루지 않았습니다.
- factory 생성자의 캐시와 const canonicalization은 다른 메커니즘이며 이 실험에서는 비교하지 않았습니다.
기초 문법과 컬렉션 수정은 별도 글에서 이어서 보기
이 글은 const 생성자와 객체 정규화에만 집중합니다. 변수 재할당과 var·final·const의 기본 선택은 Dart var·final·const 실행 비교에서 확인할 수 있습니다. List·Map·Set의 추가·삭제와 타입 안전성은 Dart List·Map·Set 선언과 안전한 수정 방법으로 분리했습니다.