프로그래밍을 처음 배울 때 가장 먼저 마주치는 개념이 있습니다. 바로 "값을 어딘가에 저장한다"는 아이디어입니다. 숫자 42를 계산에 쓰고 싶다면, 그 숫자를 어딘가에 보관해두어야 합니다. 그 보관 장소가 바로 변수(variable)입니다.
Dart에서 변수를 선언하는 방법은 여러 가지입니다. 그리고 변수 중에는 한번 값을 담으면 바꿀 수 없는 특별한 것도 있습니다. 이것이 상수(constant)입니다. 이번 챕터에서는 그릇을 고르는 법, 즉 변수와 상수를 선언하고 사용하는 법을 배웁니다.
var로 변수 선언하기
Dart에서 가장 간단하게 변수를 만드는 방법은 var 키워드를 사용하는 것입니다.
void main() {
var name = 'Alice';
var age = 25;
var height = 165.5;
print(name); // Alice
print(age); // 25
print(height); // 165.5
}
var를 쓰면 Dart가 오른쪽 값을 보고 타입을 스스로 결정합니다. 'Alice'는 문자열이니 name은 String 타입, 25는 정수니 age는 int 타입이 됩니다. 이것을 타입 추론이라고 하며, 자세한 내용은 Ch 03에서 다룹니다.
변수는 값을 바꿀 수 있습니다.
void main() {
var score = 80;
print(score); // 80
score = 95;
print(score); // 95
}
단, 한번 타입이 정해진 변수에 다른 타입의 값을 넣으면 오류가 납니다.
void main() {
var score = 80;
score = '완벽'; // 오류! int 변수에 String을 넣을 수 없음
}
명시적 타입 선언
var 대신 타입을 직접 써서 변수를 선언할 수도 있습니다.
void main() {
String name = 'Bob';
int age = 30;
double weight = 72.5;
bool isStudent = true;
print('$name, $age세, 몸무게 ${weight}kg');
}
var와 명시적 타입 선언, 어느 것이 더 좋을까요. 정답은 없지만, 일반적으로 지역 변수에는 var를 많이 씁니다. 타입이 명확할 때는 var가 더 읽기 편하고, 타입을 강조하고 싶을 때는 명시적으로 씁니다.
void main() {
// 이 둘은 같은 의미입니다
var message = 'Hello';
String message2 = 'Hello';
}
final — 런타임 상수
프로그램을 만들다 보면 "이 값은 한번 정하면 절대 바꾸지 않겠다"고 약속하고 싶을 때가 있습니다. 예를 들어 사용자 이름을 한번 받으면 그 세션에서는 변경하지 않겠다거나, 파일 경로를 고정하고 싶을 때입니다.
이럴 때 final을 씁니다.
void main() {
final String userName = 'Charlie';
final startTime = DateTime.now(); // 실행 시점에 결정됨
print(userName);
print(startTime);
// userName = 'Dave'; // 오류! final 변수는 재할당 불가
}
final의 핵심은 런타임(프로그램 실행 중)에 값이 결정된다는 점입니다. DateTime.now()처럼 프로그램이 실제로 실행될 때 비로소 알 수 있는 값도 final에 담을 수 있습니다.
const — 컴파일 타임 상수
const는 final보다 한 단계 더 엄격합니다. 컴파일 시점, 즉 코드를 기계어로 바꾸기 전에 이미 값이 결정되어 있어야 합니다.
void main() {
const pi = 3.14159265;
const appName = 'My Dart App';
const maxRetry = 3;
print('$appName의 최대 재시도 횟수: $maxRetry');
print('원주율: $pi');
}
const를 쓰면 Dart는 그 값이 프로그램 전체에서 딱 하나만 존재하도록 최적화합니다. 메모리를 아끼고 성능을 높이는 방법입니다.
반면 아래 코드는 오류입니다.
void main() {
// 오류! DateTime.now()는 실행 시점에 결정되므로 const 불가
const startTime = DateTime.now();
}
final vs const 정리
| 구분 | 재할당 | 값 결정 시점 | 사용 예 |
|---|---|---|---|
var |
가능 | 언제든지 | 일반 변수 |
final |
불가 | 런타임 | 사용자 입력, 현재 시각 |
const |
불가 | 컴파일 타임 | 수학 상수, 고정 문자열 |
void main() {
// var: 값 변경 가능
var count = 0;
count = 1; // OK
// final: 한번만 할당, 런타임 결정 가능
final createdAt = DateTime.now(); // OK
// const: 컴파일 타임에 이미 알아야 함
const version = '1.0.0'; // OK
// const now = DateTime.now(); // 오류!
}
const 생성자 맛보기
Dart에서 const는 변수뿐 아니라 객체를 만들 때도 씁니다. Flutter를 공부하다 보면 이런 코드를 자주 보게 됩니다.
// Flutter의 예시 (지금은 느낌만 파악해도 충분합니다)
// const Text('Hello')
// const EdgeInsets.all(16.0)
클래스 앞에 const를 붙이면 그 객체는 컴파일 타임에 생성되고 재사용됩니다. 같은 값의 const 객체는 메모리에 딱 하나만 존재합니다. 지금은 이 개념을 가볍게 봐두고, PART 05 클래스 편에서 자세히 다룹니다.
변수 초기화와 스코프
변수를 선언했지만 아직 값을 넣지 않은 상태를 미초기화라고 합니다. Dart에서는 미초기화 변수를 사용하면 컴파일 오류가 납니다.
void main() {
int count;
// print(count); // 오류! 초기화되지 않은 변수 사용
count = 10;
print(count); // 10
}
값을 나중에 넣을 것이 확실하다면, 일단 선언만 해두고 나중에 초기화할 수 있습니다. 단, 사용하기 전에 반드시 초기화해야 합니다.
스코프(scope)는 변수가 살아있는 범위입니다. 중괄호 {} 안에서 선언된 변수는 그 중괄호 밖에서는 보이지 않습니다.
void main() {
int outer = 10;
{
int inner = 20;
print(outer); // 10 — outer는 보임
print(inner); // 20 — inner는 보임
}
print(outer); // 10 — outer는 여전히 보임
// print(inner); // 오류! inner는 이 범위에서 보이지 않음
}
이 규칙을 이해하면 나중에 함수와 반복문을 배울 때 훨씬 수월합니다.
전체 예제로 복습하기
지금까지 배운 내용을 하나의 예제로 정리해봅니다.
void main() {
// var: 타입 추론
var language = 'Dart';
var version = 3.11;
// 명시적 타입
String author = 'Google';
int year = 2011;
// final: 런타임 상수
final launchTime = DateTime.now();
// const: 컴파일 타임 상수
const maxVersion = 999;
print('$language $version (출시: $year년, 개발: $author)');
print('실행 시각: $launchTime');
print('최대 버전: $maxVersion');
// 값 변경
language = 'Flutter'; // var는 변경 가능
version = 3.12;
// author = 'Meta'; // String 타입이지만 var가 아니어도 변경 가능
// launchTime = ...; // 오류! final은 재할당 불가
// maxVersion = 1000; // 오류! const는 재할당 불가
print('변경 후: $language $version');
}
다음 챕터에서는 Dart가 기본으로 제공하는 타입들, 즉 숫자와 문자열과 불리언을 더 깊이 살펴봅니다. 각 타입에서 쓸 수 있는 유용한 메서드와 변환 방법도 함께 배웁니다.