Dart/Flutter에서 const는 단순히 “값이 안 바뀐다”를 넘어, 컴파일 타임 상수(compile-time constant) 를 만들고, 동일한 const 표현식은 하나의 인스턴스로 공유(canonicalization) 되며, Flutter에서는 이 특성이 위젯 트리에서 불필요한 객체 생성을 줄이는 방법입니다.
const 생성자
클래스에 const 생성자를 붙인다는 건, 그 클래스가 “상수 인스턴스”로 만들어질 자격이 있다는 선언입니다. 다만 여기서 상수 인스턴스라는 말은 감성적인 표현이 아니라, “컴파일 타임에 값이 완전히 정해지고, 내부 상태가 절대 바뀌지 않는 형태로 만들 수 있다”라는 엄격한 조건을 뜻합니다.
그래서 const 생성자를 만들려면 보통 클래스의 인스턴스 필드가 final이어야 합니다. 필드가 변경 가능하면, 컴파일 타임에 고정된 인스턴스라는 개념 자체가 성립하지 않기 때문입니다.
class ImmutablePoint {
final double x;
final double y;
const ImmutablePoint(this.x, this.y);
}
const 생성자가 있다고 해서, 그 생성자를 호출하는 순간 항상 상수 인스턴스가 만들어지는 것은 아닙니다. const는 “가능성”을 열어주는 것이고, 실제로 상수 인스턴스로 만들지는 호출 방식이 결정합니다.
final a = ImmutablePoint(1, 1); // const 생성자지만 const 없이 호출: 일반 인스턴스 const b = ImmutablePoint(1, 1); // const로 호출: 컴파일 타임 상수 인스턴스 final c = const ImmutablePoint(1, 1); // 값은 const 인스턴스, 변수는 final
즉, const 생성자는 “상수로 만들 수 있다”이고, const 호출은 “지금 이 인스턴스를 상수로 만들어라”입니다. 이 차이를 구분하면 이후 개념을 쉽게 이해하실 수 있을거예요.
canonicalization
const가 진짜 강력해지는 지점이 바로 canonicalization입니다. Dart는 컴파일 타임에 평가 가능한 const 표현식을 만나면, “이 결과는 항상 같으니 하나만 만들어서 공유하자”라는 방식으로 동작할 수 있습니다. 그래서 동일한 const 표현식은 서로 다른 위치에서 쓰더라도 실제로는 같은 객체 인스턴스로 합쳐질 수 있습니다.
const p1 = ImmutablePoint(1, 1); const p2 = ImmutablePoint(1, 1); print(identical(p1, p2)); // true
identical이 true라는 건, 값이 같다는 수준이 아니라 “메모리 상에서 완전히 같은 객체”라는 뜻입니다. 반대로 const가 아니면 매번 새 객체가 만들어지기 때문에, 모양이 같아도 객체는 다릅니다.
final q1 = ImmutablePoint(1, 1); final q2 = ImmutablePoint(1, 1); print(identical(q1, q2)); // false
이 구조는 메모리를 아끼는 의미도 있지만, Flutter 관점에서는 더 실전적인 이점으로 이어집니다. “같은 위젯을 계속 새로 만들지 않고, 동일 인스턴스를 재사용할 가능성”이 열리기 때문입니다.
const 컬렉션: const [] vs final [] 차이
많은 분들이 final과 const를 “둘 다 안 바뀌는 것”으로 묶어서 이해하다가, 컬렉션에서 크게 헷갈립니다. 결론부터 말하면, final은 “변수 재할당을 막는 것”이고, const는 “객체 자체를 불변 상수로 만드는 것”입니다. 그래서 리스트나 맵, 셋 같은 컬렉션에선 체감 차이가 매우 큽니다.
final list1 = [1, 2, 3]; list1.add(4); // 가능: list1은 재할당만 금지, 리스트 내용은 수정 가능
final은 “list1이라는 이름을 다른 리스트로 바꾸지 못한다”일 뿐, 리스트 내부는 여전히 바뀔 수 있습니다. 반면 const 컬렉션은 내부 변경 자체가 금지됩니다.
final list2 = const [1, 2, 3]; list2.add(4); // 실행 중 오류: const 리스트는 수정 불가
또 한 가지 실전에서 자주 쓰는 패턴이 있습니다. 빈 리스트를 만들 때 아래 세 형태는 의미가 꽤 다릅니다.
var a = const []; // a는 재할당 가능, 값은 불변 const 빈 리스트 final b = const []; // b는 재할당 불가, 값은 불변 const 빈 리스트 const c = []; // c 자체가 컴파일 타임 상수(불변)
이 차이는 코드 리뷰에서 의도를 드러내는 데도 도움이 됩니다. “이 변수는 다시 다른 리스트를 가리킬 수 있다/없다”, “리스트 내용이 바뀔 여지가 있다/없다”가 코드만 봐도 분명해지기 때문입니다.
const 컨텍스트
const를 배우다 보면 const가 너무 많이 반복돼서 코드가 지저분해 보일 때가 있습니다. Dart에는 이런 반복을 줄이는 개념이 있는데, 흔히 “const 컨텍스트”라고 부릅니다. 쉽게 말해 바깥이 const로 고정되면, 안쪽 표현식도 상수로 평가될 자리를 얻어서 const를 생략할 수 있는 경우가 많습니다.
const data = {
'nums': [1, 2, 3],
'point': ImmutablePoint(0, 0),
};
위 예시처럼 바깥이 const인 리터럴 구조에서는, 내부도 컴파일 타임 상수로 평가될 수 있는 형태라면 자연스럽게 상수로 묶입니다. 다만 내부에 런타임 값이 끼어들면 그 즉시 const 평가가 깨지니, “내부 값이 컴파일 타임에 확정 가능한가”를 기준으로 판단하면 됩니다.
Flutter에서 const MyWidget()
Flutter에서는 화면이 바뀌는 기본 단위가 위젯이고, 상태 변화가 생기면 build()가 자주 호출됩니다. 이때 많은 초보자들이 “const를 붙이면 리빌드 자체가 안 되는가?”라고 기대하는데, 그건 정확하지 않습니다. build() 호출은 여전히 일어날 수 있고, 프레임워크는 새 위젯 구성을 받아서 트리를 업데이트합니다. 다만 const는 그 과정에서 쓸데없는 객체 생성과 업데이트 비용을 줄이는 방향으로 도움을 줍니다.
const 위젯을 쓰면, 동일한 표현식이 canonicalization으로 합쳐져 같은 인스턴스가 될 수 있습니다. 그러면 리빌드 때마다 “겉모양이 같은데 매번 새 위젯 객체를 생성하는 상황”이 줄어듭니다. 특히 children: [...]처럼 리스트로 많은 위젯을 나열하는 구조에서, 변하지 않는 위젯들을 const로 고정하면 프레임워크 입장에서 “이건 매번 새로 만들 필요가 없는 덩어리”라는 힌트를 강하게 제공하게 됩니다.
예를 들어 아래처럼 아이콘과 텍스트가 항상 동일한 정적 영역이 있다면 const로 고정하는 게 자연스럽습니다.
class ProfileHeader extends StatelessWidget {
const ProfileHeader({super.key});
@override
Widget build(BuildContext context) {
return const Row(
children: [
Icon(Icons.person),
SizedBox(width: 8),
Text('프로필'),
],
);
}
}
반대로 빌드 시점에 계산되는 값이나 상태 값이 들어가면 const가 깨집니다. 예를 들어 Text('${state.count}')처럼 문자열이 매번 달라지면 그 위젯은 당연히 const가 될 수 없습니다. 이때 실전 설계의 요령은 “바뀌는 것만 작은 위젯으로 떼어내고, 바뀌지 않는 주변부는 const로 고정”하는 방향으로 구성하는 것입니다. 이렇게 하면 리빌드가 발생해도 변경이 필요한 부분만 영향을 받고, 정적인 서브트리는 최대한 안정적으로 유지됩니다.
결론
const 심화에서 핵심은 “불변”이라는 감각이 아니라, 컴파일 타임 상수라는 규칙과 그 결과로 생기는 canonicalization을 정확히 이해하는 것입니다. const 생성자는 상수 인스턴스를 만들 수 있는 형태를 열어주고, 실제로 상수 인스턴스를 만들지는 const 호출 여부가 결정합니다. 동일한 const 표현식은 하나의 인스턴스로 공유될 수 있어 객체 생성 비용과 메모리 낭비를 줄이는 방향으로 작동합니다. 컬렉션에서는 final이 “재할당 금지”인 반면 const는 “컬렉션 자체의 변경 불가”이므로, 의도에 따라 final 컬렉션과 const 컬렉션을 명확히 구분해야 합니다. Flutter 위젯 트리에서는 변하지 않는 서브트리를 const로 고정해두면 리빌드가 발생해도 불필요한 위젯 객체 생성과 업데이트가 줄어들 수 있어, 성능 최적화뿐 아니라 구조적으로도 “변하는 부분/안 변하는 부분”을 분리하는 좋은 설계 습관으로 이어집니다.
FAQ
const 생성자가 있으면 무조건 const 인스턴스가 만들어지나요?
아닙니다. const 생성자는 “상수로 만들 수 있다”는 가능성을 제공할 뿐이고, 실제로 상수 인스턴스를 만들려면 호출할 때 const를 붙이거나 상수 컨텍스트에서 평가되어야 합니다. const 없이 호출하면 일반 인스턴스로 생성됩니다.
canonicalization은 정확히 무엇을 의미하나요?
동일한 const 표현식이 만들어내는 결과가 항상 같기 때문에, 런타임에서 여러 개를 따로 만들지 않고 하나의 표준 인스턴스로 합쳐 공유하는 동작을 말합니다. 그래서 같은 const는 값이 같은 수준이 아니라 객체 자체가 동일해질 수 있습니다.
const []와 final []는 뭐가 다른가요?
final은 변수(참조)의 재할당만 막습니다. 즉 리스트 자체는 변경 가능일 수 있습니다. 반면 const 컬렉션은 컬렉션 객체 자체가 불변이기 때문에 요소 추가/삭제/수정 같은 내부 변경이 불가능합니다.
final x = const […] 처럼 섞어 쓰는 이유가 있나요?
변수 재할당은 막고 싶지만(=final), 값은 불변으로 고정하고 싶을 때(=const) 이 조합이 가장 의도가 명확합니다. 특히 “내용은 절대 변하면 안 된다”를 코드로 강하게 표현할 때 유용합니다.
const 컨텍스트에서는 왜 const를 생략할 수 있나요?
바깥이 const로 고정되면 내부 표현식도 컴파일 타임에 평가 가능한 형태라면 자동으로 상수로 묶일 수 있기 때문입니다. 다만 내부에 런타임 값이 섞이면 상수 평가가 깨져서 const가 불가능해집니다.
Flutter에서 const MyWidget()을 쓰면 리빌드가 아예 안 되나요?
아닙니다. build 호출 자체가 없어지는 개념은 아닙니다. 다만 동일한 const 위젯 표현식이 공유될 수 있어, 리빌드 과정에서 불필요한 위젯 객체 생성 부담을 줄이는 방향으로 도움을 줄 수 있습니다. 실무에서는 정적 서브트리를 const로 고정하고, 변하는 부분만 분리하는 설계가 중요합니다.
어떤 위젯에 const를 붙여야 효과가 좋은가요?
텍스트, 아이콘, SizedBox처럼 입력값이 고정된 정적 UI 조각이나, 화면이 리빌드되더라도 내용이 변하지 않는 서브트리에 붙이는 것이 효과적입니다. 반대로 상태값, 비동기 결과, BuildContext 기반 계산처럼 런타임에 달라지는 값이 들어가면 const를 붙일 수 없습니다.
const를 무조건 많이 붙이면 항상 더 좋아지나요?
항상 그렇진 않습니다. 붙일 수 있는 곳에 붙이는 건 일반적으로 이득이지만, 핵심은 “안 변하는 영역을 명확히 const로 고정하고, 변하는 영역은 분리”하는 구조입니다. const 자체가 마법처럼 성능을 해결하는 것이 아니라, 트리를 더 예측 가능하고 안정적으로 설계하는 데 도움이 된다고 보는 편이 정확합니다.