개요
Flutter로 크로스 플랫폼 앱 개발을 하다보면, 아래와 같이 `const`를 붙이라는 경고메세지가 등장한다. ▼
이는 Error가 아닌 단순 Warning이라서 지키지 않아도 프로그램이 컴파일 되는 데에는 큰 문제가 없다. 하지만 아래에 파란줄이 남은 것을 볼 때면 굉장히 찝찝해져서 const를 붙이지 않고 넘어갈 수 없다. ▼
이쯤되면 의문이 하나 생긴다.
"도대체 왜 `const` 키워드를 붙이라고 권장하는걸까?"
프로그래밍 문법을 배울 때 const는 상수라고 배운다.
그리고 상수는 변하지 않는 값을 선언할 때 사용한다고도 배우는데, Dart에는 `const`외에 `final`이라는 키워드가 존재한다.
변하지 않는 값을 선언하는 것을 권장하는 것이라면 `const`가 아니라 `final`을 권하는 것이 좋을텐데 어째서 Flutter에서는 const 키워드를 붙이는 것을 권장할까?
Dart는 컴파일러 언어
컴파일러 언어 VS 인터프리터 언어
이에 대해 알기 위해서는 우선 컴파일러 언어와 인터프리터 언어의 차이점에 대해서 알고 들어가야한다. 둘의 프로그램 번역/실행에는 차이가 존재하고 이 차이점이 final과 const의 차이를 만들기 때문이다.
컴파일러 언어
먼저 컴파일러는 고급 언어로 작성 된 소스 코드를 저급 언어로 번역하는 프로그램을 말한다. 고급 언어는 사람이 이해하기 쉽게 작성된 프로그래밍 언어이고, 저급 언어는 기계가 이해하기 쉽게 작성된 프로그래밍 언어라고 생각하면 쉽다.
컴파일러 언어는 컴파일러를 통해 전체 소스 코드(고급 언어)를 한 번에 기계어(저급 언어)로 변환하여 실행 파일을 만든다. 컴파일러 언어는 컴파일 단계와 런타임 단계가 나뉘어져 있으며, 컴파일 단계는 한 번만 수행된다. 쉽게 말해 컴파일 단계는 실제로 실행시킬 파일을 만드는 단계고, 런타임 단계는 그 파일을 실행시키는 단계라고 할 수 있다.
실행 시에는 컴파일 과정을 통해 다 번역된 코드를 실행하기만 하면 되기에 실행 속도가 빠르다. 허나 프로젝트 규모가 커지만 컴파일 시간이 오래걸리게 되며, 운영체제에 맞춰 컴파일을 다시 해줘야한다. 이전에 컴파일 해 둔 프로그램이 윈도우 OS를 위한 것이었다면, 맥 OS에서 실행시키기 위해서는 맥 환경에 맞게 다시 컴파일을 해줘야하는 문제가 있다.
C, C++, C#, Go와 같은 언어들이 컴파일러 언어에 속하며, Dart도 이에 속한다.
인터프리터 언어
인터프리터는 프로그래밍 언어의 소스 코드를 바로 실행하는 프로그램을 말한다.
인터프리터 언어는 컴파일러와 다르게 소스 코드를 한 번에 기계어로 번역하지 않고, 소스 코드를 한 줄 씩 읽어들여 실행한다. 컴파일 하는 과정이 없기에 컴파일 하는 시간은 없지만, 실행파일이 별도로 생성되지 않기에 실행시 마다 인터프리트 과정이 반복 수행되어 실행 속도가 느리다.
허나 컴파일 언어와는 다르게 OS마다 적절한 인터프리터가 구비되어있다면, 운영체제의 구분없이 실행시킬 수 있다.
Python, JavaScript, R과 같은 언어들이 인터프리트 언어에 속한다.
JAVA는?
그 유명한 JAVA는 어디갔지? 라는 의문이 들 수 있다.
JAVA의 경우엔 둘이 혼재된 하이브리드 언어이다.
final과 const의 차이
앞에서 컴파일 언어와 인터프리트 언어의 이야기를 한 이유는 둘의 핵심적인 차이인 컴파일 타임과 런타임 타임의 존재 때문이었다.
"그게 final과 const의 차이와 무슨 상관인데?"
아주 큰 상관이 있다.
final과 const의 차이는 런타임 시와 컴파일 시에 달려있기 때문이다.
그럼 둘의 차이를 보자.
먼저 final 변수는, 런타임 시에 값이 설정되고 그 이후에 바뀌지 않는 변수를 말한다.
예를 들면 클래스의 변수들이 있다.
클래스가 리터럴 값을 가지고 처음 인스턴스화 될 때는 컴파일 과정에서도 처리를 할 수 있지만, 변수의 값을 가지고 처음 인스턴스화 될 때는 컴파일 과정에서 처리를 할 수 없다.
변수에 들어있는 값이 언제 어디서 바뀔 지 모르기 때문이다.
이를 처리하기 위해 final 키워드가 붙은 변수는 프로그램이 실행될 때 값이 설정되고 더 바뀌지 않는다.
반대로 const 변수는 final과 마찬가지로 값이 한 번 설정되면 그 이후에 바뀌지 않는 변수이다.
하지만 final과는 다르게 런타임 시에 값이 설정되는 것이 아니라 컴파일 시에 값이 설정된다. ▼
Const를 사용해야하는 이유
"런타임 시에 설정되든, 컴파일 시에 설정이 되든 똑같이 설정되고 안바뀌는건데 이를 구분할 필요가 있는 건가?"
"굳이 const를 써야하는 이유가 있는건가?"
물론이다. 둘은 아주 큰 차이가 있다.
문법적인 차이와 성능적인 차이가 존재한다.
문법적인 차이
생성 시간
변수에 프로그램이 실행되고 있는 현재시간을 담고 싶다고 해보자.
이를 코드로 작성하면 아래와 같이 두 가지로 작성해볼 수 있을 것이다.
const DateTime currentTimeConst = DateTime.now(); //컴파일 에러
final DateTime currentTimeFinal = DateTime.now(); //정상 코드
값을 설정하고, 설정된 값은 변하지 않는다는 것은 같지만 둘의 결정적인 차이는 '언제 만들어지는가'이다.
final 키워드로 선언하게 되면 실행시간이 담기게 되므로 우리가 의도한 바와 맞다.
그에 비해, const 키워드로 선언하게 되면 컴파일시간이 담기게 되므로 우리가 의도한 바와 어긋나게 된다.
const에는 const만
허나 더 큰 문제는 문법적으로 틀렸다는 것이다.
const에는 정적으로 알려진 상수만 할당할 수 있다. 쉽게 말해, 상수에는 상수만 들어갈 수 있다는 것이다.
하지만 위의 DateTime의 DateTime.now()는 상수로 선언된 값이 아니기 때문에 문법적으로 오류가 난다.
성능적인 차이
이 부분이 이 글의 핵심 부분이라고 할 수 있다.
final보다 const를 사용해야하는 핵심적인 이유는 최적화 때문이며, 그 이유를 이제부터 알아보자.
const의 선언 방식
변수는 로컬 변수, 글로벌 변수, 클래스 멤버 변수, 클로저 변수 등 여러 형태로 존재한다.
각 변수 유형은 다른 방식으로 다른 위치에 저장되지만, 폭넓게 설명하면 모든 변수가 스택(프로그램의 실행 경로를 따르는 선형 메모리 섹션) 또는 힙(어느 정도 구조화되지 않은 메모리 덩어리로서, 장기 프로세스의 수명 동안 또는 앱의 수명 동안 존재)에 저장된다.
변수에 값을 할당하면 프로그램에게 해당 변수의 메모리 위치로 이동하여 그 값을 할당하도록 지시하는 것이다.
그러나 상수는 완전히 다른 개념이다.
상수는 선언되거나 인스턴스화되거나 값이 할당되지 않으며, 스택 또는 힙에 저장되는지 묻는 것은 무의미하다.
상수는 컴파일 시간에 알려진 값으로, 예를 들어 1, true 및 "Hello World"와 같다.
상수를 선언할 때 아래와 같이 선언했다고 해보자.
const SECONDS_IN_MINUTE = 60;
이는 변수를 인스턴스화하는 것이 아니라 컴파일러가 상수를 사용하는 모든 곳에 대체할 값에 대한 별칭을 만드는 것이다.
즉, `int fiveMinutes = SECONDS_IN_MINUTE * 5;` 와 같은 코드는 `int fiveMinutes = 60 * 5;` 로 컴파일된다.
마찬가지로 `const Duration(seconds: 1)`은 런타임에 생성되는 변수나 객체가 아니다.
프로그램이 실행되기 전에 컴파일될 때 이미 명시된 값이다.
이것이 상수 생성자를 선언할 때 클래스가 final 필드만 가져야하고 매개변수는 자체적으로 상수인 타입만 가능한 이유다.
상수가 아닌 필드로 정의할 수 있는 객체는 본질적으로 상수가 아니다.
캐노니컬 상수(Canonical constants)
또한 Dart는 모든 상수를 대상으로 캐노니컬 상수라는 개념을 지원한다. 예를 들어 아래와 같이 코드를 작성했다고 해보자.
a, b, c 및 d 변수 각각은 비상수 값을 저장하고 있으므로 서로 다른 네 개의 Duration 객체가 별도로 생성된다. ▼
var a = Duration(seconds: 1);
var b = Duration(seconds: 1);
var c = Duration(seconds: 1);
var d = Duration(seconds: 1);
반면에 아래와 같이 const 키워드를 붙여 작성하게 되면, ▼
var a = const Duration(seconds: 1);
var b = const Duration(seconds: 1);
var c = const Duration(seconds: 1);
var d = const Duration(seconds: 1);
각 변수는 상수 값이 할당되어 있으며, 초(second) 값이 동일하기 때문에 각각이 동일한 Duration 값에 대한 참조를 가리킨다.
이를 통해 최적화를 할 수 있다.
예를 들어, 동일한 패딩을 가진 많은 패딩 위젯이 있는 앱을 만드는 경우, 모든 EdgeInsets를 상수로 변경하면 사용할 때마다 새로운 EdgeInsetsGeometry를 생성하지 않아도 된다.
이런 문법적인, 성능적인 차이로 final과 const를 구분해서 사용해야하며, 최적화를 위해 const를 사용해야한다.
마치며
그동안 const를 사용하라는 권장사항을 봤을 때 '일단 좋아진다고 하니까 쓰지 뭐', '파란 줄이 거슬리네' 라는 이유로 const를 작성해왔었다. 굳이 더 알려고 하지 않았고 왜 빨라지는지는걸까 라는 의문은 들었지만 항상 우선순위에서 밀려있었다. 그러다가 이유도 모르고 사용하는게 의미가 있을까? 라는 생각이 강해져 이번에 자세하게 찾아보게 되었다.
이렇게 깊이 파고드는게 시간은 많이 소모되어 단기적으로는 안좋아보이지만 그 이후의 모든 개발에 적용되는 지식을 얻은 것이므로 필수적으로 해야한다고 생각한다.
다음에 또 궁금한게 생기면 찾아보고 해결해야겠다.
'Develop > Flutter' 카테고리의 다른 글
[Flutter][Package] ScreenUtil 패키지 (0) | 2024.02.15 |
---|---|
[Flutter][Widget] Spacer 위젯 (0) | 2024.02.14 |
[Flutter][Widget] FloatingActionButton 위젯 (0) | 2024.02.12 |
[Flutter][Widget][Package] WebView Widget(4.2.x) (0) | 2024.02.12 |
[Flutter] FutureBuilder로 비동기 화면 그리기 (feat.GetX) (0) | 2024.02.01 |