[TIL-003] Dart 실전 문법 파헤치기 - 'const 생성자'

강의를 듣다 보면 자주 쓰이는 코드의 형태가 있다. 그렇지만 이게 도대체 무엇인지는 모른다.

그래서 자세하고 낱낱이 파헤쳐보자. 오늘 파헤칠 것은 `const 생성자`이다.

 

사전 지식

컴파일 타임(Compile Time)

  • 런 타임이 되기 전 사람이 작성한 코드(Dart, C++, Java 등)를 컴퓨터가 이해할 수 있는 기계어로 바꾸는 시간(순간, 과정)이다.
  • 변수 선언이 잘못되거나 문법이 틀리면 컴파일 타임 때 에러를 발견한다.
void main() {
  int number = "Hello"; // ❌ 컴파일 에러 중 타입 에러(문자열을 숫자 변수에 할당)
}

 

런 타임(Run Time)

  • 컴파일 타임이 끝난 후 프로그램이 실제로 실행되는 시간이다.
  • 컴파일 에러가 없더라도 런 타임에는 에러가 생길 수도 있고, 사용자가 입력을 잘못하면 런 타임 때 에러가 발생한다.
void main() {
  List<int> numbers = [1, 2, 3];
  
  // ❌ 런타임 에러 발생 (범위를 벗어난 인덱스)
  print(numbers[5]);
}
  • `print(numbers[5]);` → 컴파일 타임일 때는 에러가 발생하지 않지만 실제 프로그램이 실행되는 런 타임에서는 에러를 일으킨다.
  • 컴파일 타임에서는 List의 요소가 몇 개인지 파악을 하지 못하기 때문에 컴파일 에러를 발생시키지 않는다.

 

const 키워드

dart에서 `const` 키워드는 상수를 만들기 위해 사용하며 js, ts와 달리 변수의 초기화를 컴파일 타임(Compile Time)때 진행한다.

void main() {
  const Map<String, String> p1 = {'name': 'lee'};
  const Map<String, String> p2 = {'name': 'lee'};

  print('p1 == p2 : ${p1 == p2}'); // true (같은 객체로 취급됨)
}
  • `const` 키워드로 선언한 변수에 같은 값을 할당했다면 런 타임이 됐을 때 값이 같다고 취급한다.

 

const 생성자

Dart에서 `const 생성자`는 객체를 불변(immutable)하게 만들어주는 특별한 생성자다.
일반 생성자와 다르게 값이 변하지 않는다면 메모리를 절약할 수 있는 장점이 있다.

 

사용 이유

class Person {
  String name;
  
  Person(this.name);
}

void main() {
  var p1 = Person("lee");
  var p2 = Person("lee");

  print(p1 == p2); // false (서로 다른 객체)
  
  const p3 = Person('lee'); // 컴파일 에러
  const p4 = Person('lee'); // 컴파일 에러
}
  • `p1`과 `p2`는 `{name: 'lee'}`로 같은 값을 가지지만, 서로 다른 객체로 생성된다. 즉, 매번 새로운 메모리를 차지하게 된다.
  • `const` 키워드로 선언한 변수에 동일한 값을 할당하면 서로 같다고 취급된다. 하지만 `p3`와 `p4`에 `const`를 적용하면 컴파일 에러가 발생한다.
  • `Person('lee');`는 호출이다. 함수나 메서드, 클래스의 호출은 런 타임일 때 호출이 된다.
  • const p3 = Person('lee');에서 const를 사용했기 때문에 컴파일 타임에 변수를 초기화해야 한다. 하지만 Person('lee');는 런타임에 호출되므로 컴파일 타임에서 초기화할 수 없어 컴파일 에러가 발생한다.

그런데 `const 생성자`를 사용하면 클래스 호출로 인한 컴파일 에러를 방지할 수 있고 값이 같을 때는 같은 객체로 취급하여 메모리를 절약할 수 있다. 

 

사용법

class Person {
  final String name;

  const Person(this.name);
}

void main() {
  // 클래스에 const 키워드를 붙여 호출하거나
  // const 키워드를 붙여 변수를 선언한다.
  var p1 = const Person("lee");
  var p2 = const Person("lee");
  const p3 = Person('lee');

  print(p1 == p2); // true (같은 객체로 취급됨)
  print(p2 == p3); // true (같은 객체로 취급됨)
}
  • `final String name;` → 불변성을 보장하기 위해 클래스 내의 모든 멤버 변수는 `final`이어야 한다.
  • `const Person(this.name);` → `const` 키워드를 붙여 생성자를 정의한다.
  • `const Person("lee");` 클래스에 `const` 키워드를 붙여 호출한다.
  • `const p3 = Pserson('lee');`  `const` 키워드를 붙여 변수를 선언한다.

class Person {
  final String name;

  const Person(this.name);

  @override
  String toString() {
    return '{name: $name}';
  }
}

class Person2 {
  final String name;

  const Person2(this.name);

  @override
  String toString() {
    return '{name: $name}';
  }
}

void main() {
  const Map<String, String> p1 = {'name': 'lee'};
  const Map<String, String> p2 = {'name': 'lee'};

  const p3 = Person('lee');
  const p4 = Person('lee');
  
  const p5 = Person2('lee');
  
  print(identical(p1, p2)); // true
  print(identical(p3, p4)); // true
  print(identical(p1, p3)); // false
  print(identical(p3, p5)); // false
}
  • `print(identical(p1, p3));` → 값이 같아도 맵으로 만든 객체와 클래스로 만든 객체는 서로 다르다.
  • `print(identical(p3, p5));`  값이 같아도 생성자가 다르면 즉, 클래스가 다르면 서로 다르다.  

 

느낀 점

책을 읽으면 저자의 생각을 알 수 있다고 하던가?

나도 이제 코드에서 클래스에 `const`를 붙여 호출하는 개발자의 의도를 알게 된 것 같다.

 

Flutter의 `StatefulWidget`을 사용할 때 상태가 변하면 `build()`가 매번 호출 될텐데 그때마다 같은 형태의 객체를 생성한다면 앱의 성능이 저하될 것을 예측할 수 있게 되어 기쁘다.