final을 붙였는데 리스트에 add()가 된다. ‘한 번 정하면 바꿀 수 없다’고 배웠다면 이상하게 느껴진다. Dart에서 final은 변수가 다른 객체를 다시 가리키지 못하게 하고, 리스트 안의 원소 변경까지 막지는 않는다. const 리스트는 그 원소도 바꿀 수 없다.
final이 고정하는 것은 변수의 할당이다
도식은 변수와 그 변수가 가리키는 리스트를 따로 그린다. final은 화살표를 다른 리스트로 돌리지 못하게 하고, const 리스트는 add()처럼 내용 자체를 바꾸는 경로도 막는다.
같은 작업 단계 목록으로 확인해 보자. steps라는 변수는 처음 만든 리스트를 계속 가리키지만, 그 리스트 자체는 바뀔 수 있다.
void main() {
final steps = <String>['draft'];
steps.add('review');
print(steps);
}[draft, review]steps = <String>['done'];처럼 변수에 다른 리스트를 재할당하면 분석 단계에서 막힌다. 반면 steps.add('review')는 같은 리스트의 내용을 바꾸므로 허용된다. 코드 검토에서 final만 보고 “이 함수에 전달된 목록은 절대 바뀌지 않는다”고 판단하면 안 되는 이유다.
const 리스트는 add에서 어떤 오류가 날까?
이번에는 동일한 초기 항목을 const 리스트로 만든다. add 호출 자체는 문법상 쓸 수 있지만, 실행하면 변경 불가능한 리스트이므로 오류가 난다.
void main() {
const fixedSteps = <String>['draft'];
try {
fixedSteps.add('review');
} catch (error) {
print(error.runtimeType);
}
}UnsupportedError오류를 잡아 숨기라는 뜻은 아니다. 비교를 위해 예제에서만 잡았다. 앱 코드에서 이 오류를 만나면 먼저 목록이 const로 만들어졌는지, 변경하려던 단계가 새 상태를 만들어야 하는 자리인지 확인하자. 예를 들어 새 목록이 필요하다면 final nextSteps = [...fixedSteps, 'review'];처럼 새 값을 만들 수 있다. 기존 fixedSteps는 그대로다.
실행 중에 정해지는 값과 변경 금지를 구분한다
const는 컴파일 시점에 결정할 수 있는 상수 표현식에 사용한다. 지금 시각처럼 실행 중에야 결정되는 값은 final startedAt = DateTime.now();로 한 번 받아 둘 수 있지만, 같은 호출을 const 값으로 만들 수는 없다. 그렇다고 final의 모든 결과가 불변 객체가 되는 것은 아니다.
외부에 목록을 넘길 때 받는 쪽이 바꾸면 안 되는 계약이라면 변수에 final을 붙이는 것만으로는 부족하다. 변경 가능한 원본을 그대로 넘기는지, 복사한 목록이나 변경 불가 뷰를 넘기는지 API 경계에서 정해야 한다. 특히 여러 화면이 같은 리스트를 공유하면 한 곳의 add()가 다른 곳의 결과까지 바꿀 수 있다. 반대로 매 입력마다 새 복사본을 만들면 메모리와 할당 비용이 늘므로, 목록 크기와 변경 빈도에 맞춰 선택한다.
핵심 요약: 재할당과 내용 변경은 별개다
final은 변수의 재할당을 막지만 가리키는 리스트의 add()는 허용한다. const 리스트는 원소 변경도 막아 add()에서 UnsupportedError가 난다. 데이터가 바뀌면 안 되는 요구라면 변수 선언뿐 아니라 전달하는 객체의 변경 가능성까지 확인하자.
작성자
기초 개념을 구현과 검증, 실제 운영 판단까지 연결해 기록합니다.

