Dart const 생성자·컬렉션·canonicalization 이해하기

핵심 요약
const는 단순한 재할당 금지가 아니라 컴파일 시점에 확정되는 객체 그래프를 만들며 같은 상수 표현식은 정규화될 수 있습니다.

검수 범위
const 생성자의 조건, final과의 차이, Flutter 위젯에서의 의미를 공식 생성자 문서와 대조했습니다.

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

identicaltrue라는 건, 값이 같다는 수준이 아니라 “메모리 상에서 완전히 같은 객체”라는 뜻입니다. 반대로 const가 아니면 매번 새 객체가 만들어지기 때문에, 모양이 같아도 객체는 다릅니다.

final q1 = ImmutablePoint(1, 1);
final q2 = ImmutablePoint(1, 1);

print(identical(q1, q2)); // false

이 구조는 메모리를 아끼는 의미도 있지만, Flutter 관점에서는 더 실전적인 이점으로 이어집니다. “같은 위젯을 계속 새로 만들지 않고, 동일 인스턴스를 재사용할 가능성”이 열리기 때문입니다.

const 컬렉션: const [] vs final [] 차이

많은 분들이 finalconst를 “둘 다 안 바뀌는 것”으로 묶어서 이해하다가, 컬렉션에서 크게 헷갈립니다. 결론부터 말하면, 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로 고정해두면 리빌드가 발생해도 불필요한 위젯 객체 생성과 업데이트가 줄어들 수 있어, 성능 최적화뿐 아니라 구조적으로도 “변하는 부분/안 변하는 부분”을 분리하는 좋은 설계 습관으로 이어집니다.


검증 기준과 참고 자료

이 글은 2026-09-08에 아래 공식 문서와 공개 기술 문서를 기준으로 내용과 용어를 다시 검수했습니다. 제품 버전과 기기 제조사에 따라 화면 이름은 달라질 수 있으므로 실제 화면과 공식 문서를 함께 확인하세요.

관련 글