변수
Dart의 변수를 학습합니다. 아래는 변수를 생성하고 초기화하는 예시 코드입니다.
var name = 'Jack';Code language: JavaScript (javascript)
변수는 참조(레퍼런스)를 저장합니다. name이라는 변수 안에는 값이 “Bob”인 문자열 객체를 가리키는 참조가 보관됩니다.
참조란 메모리 상의 주소를 의미합니다. 데이터를 메모리에 효율적으로 저장하기 위해 각 메모리 영역에 고유한 주소를 부여하는데, 집의 번지수와 같은 개념입니다.

컴파일러는 name의 타입을 자동으로 String으로 추론하지만, 직접 타입을 지정해 추론 결과를 바꿀 수도 있습니다. 여러 가지 타입의 객체를 저장해야 하는 변수라면 타입을 Object로 선언하세요(필요한 경우 dynamic을 사용할 수 있습니다).
Object name = 'Jack';Code language: JavaScript (javascript)
다른 작성 방식으로 타입을 명시적으로 지정하는 방법이 있습니다(예시로 이름은 텍스트이므로 String 타입으로 저장할 수 있다는 것을 알 경우 직접 문자열 타입을 명시합니다).
String name = 'Jack';Code language: JavaScript (javascript)
Null 안전성 (Null Safety)
Null 안전성은 값이 null인 변수를 잘못 접근해 발생하는 오류를 방지해줍니다. 해당 오류는 널 역참조 오류(null dereference error)라고 부릅니다. 연산 결과가 null인 표현식에서 속성에 접근하거나 메서드를 호출할 때 발생합니다.
예외 케이스가 존재합니다: null 자체가 기본 지원하는 속성 또는 메서드(toString(), hashCode)일 경우 오류가 발생하지 않습니다. Null 안전성을 통해 Dart 컴파일러는 컴파일 단계에서 이런 잠재적 오류를 미리 감지할 수 있습니다.
프로그래밍 경험이 있는 독자라면 널 참조 오류가 매우 빈번하게 발생한다는 사실을 알 것입니다. 컴파일 단계에서 해당 오류를 미리 차단할 수 있다면 프로그램의 안정성과 견고함이 크게 향상됩니다.
예시: int 타입 변수 i의 절댓값을 가져오려고 한다고 가정해보겠습니다. 만약 i가 null이라면 i.abs()를 실행했을 때 널 역참조 오류가 발생합니다. 다른 프로그래밍 언어에서는 이 코드가 실행될 때만 오류를 내지만, Dart 컴파일러는 해당 불법 호출 자체를 금지해 문제를 미리 잡아냅니다.
Null 안전성으로 인한 세 가지 핵심 변경점
1 널 허용 타입: 타입을 지정할 때 타입 뒤에 물음표(?)만 붙이면 해당 타입이 null을 허용할지 제어할 수 있습니다.
String? name // 널 허용 타입: 값이 null이거나 문자열
String name // 널 비허용 타입: null을 저장할 수 없고 문자열만 보관 가능Code language: JavaScript (javascript)
일부 프로그래밍 언어에서는 string name 변수에 null을 저장할 수 있지만 Dart에서는 불가능합니다. null을 저장해야 한다면 string? 타입을 사용해야 합니다.
2 변수는 사용 전 초기화를 완료해야 함. 널 허용 변수의 기본값은 null이므로 자동 초기화된 상태와 동일합니다. 반면 널 비허용 타입은 기본값이 없어 반드시 수동으로 초기값을 할당해야 합니다. Dart는 초기화되지 않은 변수에 접근하는 것을 금지해 문제를 원천 차단합니다.
void main() {
int year; // 오류: 초기화되지 않은 널 비허용 변수
print(year); // 컴파일 오류: year 변수가 초기화되지 않음
}
Code language: Dart (dart)
위 코드는 컴파일 시 오류를 출력합니다.
C:\dartdemo\firstdart>dart run
Building package executable...
Failed to build firstdart:firstdart:
bin/firstdart.dart:7:9: Error: Non-nullable variable 'year' must be assigned before it can be used.
print(year); // 컴파일 오류: year 변수가 초기화되지 않음
^^^^Code language: JavaScript (javascript)
반면 널 허용 변수는 아래와 같이 작성해도 정상 동작합니다.
void main() {
int? age; // 널 허용 변수, 기본값은 null
print(age); // null이 출력되며 오류 없음
//age = 18; // 수동으로 값 할당
//print(age); // 18이 출력됨
}Code language: JavaScript (javascript)
3 널 허용 타입 표현식에 직접 속성 접근 또는 메서드 호출 불가. 여기에도 예외가 존재하는데, null 자체가 기본 제공하는 속성·메서드(hashCode, toString())는 호출할 수 있습니다.
void main() {
String? name; // 널 허용 타입, 기본값 null
// 오류: length에 직접 접근하면 실패
// print(name.length);
// 정상: null 기본 내장 메서드 호출 허용
print(name.toString()); // "null" 출력
print(name.hashCode); // 정수값 출력
// 정상: 널 안전 연산자 사용
print(name?.length); // null 출력, 오류 없음
}Code language: PHP (php)
기본값
수동으로 값을 할당하지 않은 널 허용 변수의 초기 기본값은 null입니다. 숫자 타입 변수마저 기본값이 null인데,
Dart에서는 숫자를 비롯한 모든 데이터가 본질적으로 객체이기 때문입니다.
void main() {
int? lineCount;
assert(lineCount == null);
}Code language: JavaScript (javascript)
유의사항: 배포 환경 코드에서는
assert()검증 로직이 무시됩니다. 개발 단계에서assert(조건)안의 조건이 성립하지 않으면 프로그램이 예외를 던집니다.
Null 안전성을 활성화한 후 널 비허용 변수는 사용 전 반드시 초기화를 완료해야 합니다.
int lineCount = 0;
지역 변수는 선언한 같은 줄에 값을 할당하지 않아도 되지만, 사용하기 전 반드시 값을 할당해야 합니다. 아래 코드는 Dart가 print() 실행 시점에 lineCount가 무조건 널이 아닌 값을 갖는다고 분석할 수 있으므로 유효한 코드입니다.
int lineCount;
if (weLikeToCount) {
lineCount = countLines();
} else {
lineCount = 0;
}
print(lineCount);
Code language: Dart (dart)
위 코드에서 lineCount = countLines(); 와 lineCount = 0; 은 변수에 값을 할당하는 행위일 뿐 ‘사용’이 아닙니다. ‘사용’이란 변수에 저장된 값을 읽는 행위를 의미합니다. 예를 들어 아래 코드는 컴파일 오류가 발생합니다.
void main() {
int lineCount;
print(lineCount);
}
Code language: Dart (dart)
컴파일 오류가 출력됩니다.

최상위 변수와 클래스 멤버 변수는 지연 초기화가 적용되며, 처음으로 접근하는 순간에만 할당 로직이 실행됩니다.
late 수식어 변수
late 수식어는 두 가지 사용 시나리오를 가집니다.
- 나중에 값을 할당할 널 비허용 변수 선언;
- 변수의 지연 초기화 구현.
대부분의 경우 Dart의 흐름 분석 기능은 널 비허용 변수가 사용 전 널이 아닌 값을 할당받았는지 판단할 수 있습니다. 하지만 일부 상황에서는 분석이 실패하는데, 가장 흔한 케이스가 전역 변수와 인스턴스 변수입니다. 컴파일러는 이 변수들이 초기화되었는지 확인할 수 없어 오류를 표시합니다.
변수를 사용하기 전 반드시 값이 할당된다고 확신하지만 컴파일러가 감지하지 못한다면 late 수식어를 붙여 오류를 제거할 수 있습니다.
아래 예시의 description은 전역 변수이므로 컴파일러가 할당 여부를 파악할 수 없지만, 절대 값이 할당된다고 확신한다면 late를 사용합니다.
late String description;
void main() {
description = 'Feijoada!';
print(description);
}
주의점
late변수를 선언한 뒤 끝내 값을 할당하지 않으면 변수에 접근하는 순간 런타임 예외가 발생합니다.
late 변수 선언과 동시에 초기값을 기입했다면 초기화 코드는 변수를 처음 접근하는 순간에만 실행됩니다. 이 지연 초기화는 두 가지 상황에서 매우 유용합니다.
- 변수가 전혀 사용되지 않을 가능성이 있고 초기화 연산에 자원 소모가 큼;
- 인스턴스 변수 초기화 시 초기화 로직에서
this에 접근해야 함.
아래 예시에서 temperature 변수를 한 번도 사용하지 않는다면 자원 소모가 큰 readThermometer() 함수는 전혀 실행되지 않습니다.
// temperature에 처음 접근할 때만 readThermometer() 호출
late String temperature = readThermometer(); // 지연 초기화
final 과 const
변수의 값을 수정할 수 없게 하고 싶다면 final 또는 const를 사용하세요. 두 키워드 모두 단독으로 사용하거나 타입 주석과 함께 써서 var를 대체할 수 있습니다.
final변수는 단 한 번만 값 할당 가능;const변수는 컴파일 타임 상수 (const변수는 암시적으로final속성을 가짐).
참고
인스턴스 멤버 변수는
final로 수식할 수 있지만const는 사용 불가하며, 클래스 단위 상수는static const를 사용하세요.
final 변수 선언 예시는 아래와 같습니다.
final name = 'Jack'; // 타입 주석 없음
final String nickname = 'Jason';
final 변수의 값은 덮어쓸 수 없으며 컴파일러가 직접 오류를 출력합니다.
void main() {
final name = 'Jack'; // 타입 주석 없음
final String nickname = 'Jason';
name = 'Alice'; // 오류: final 변수는 단 한 번만 할당 허용
}
C:\dartdemo\firstdart>dart run
Building package executable...
Failed to build firstdart:firstdart:
bin/firstdart.dart:6:2: Error: Can't assign to the final variable 'name'.
name = 'Alice'; // 오류: final 변수는 단 한 번만 할당 허용
const는 컴파일 타임 상수를 정의할 때 사용합니다. 클래스 내부에 상수를 선언한다면 static을 추가해 static const로 작성해야 합니다. const 변수 선언 시 할당하는 값은 컴파일 단계에서 확정 가능한 값이어야 합니다: 숫자, 문자열 리터럴, 다른 const 변수, 상수 숫자 간 산술 연산 결과 등이 해당됩니다.
const bar = 1000000; // 압력 단위 (다인/cm²)
const double atm = 1.01325 * bar; // 표준 대기압
const 키워드는 상수 변수 선언 외에도 상수 객체 생성, 상수 인스턴스를 반환하는 생성자 정의에 활용할 수 있습니다. 모든 변수는 상수 객체를 값으로 받을 수 있습니다.
var foo = const [];
final bar = const [];
const baz = []; // const []와 동일한 의미
const 변수에 값을 할당하는 표현식 안에서는 위 baz 예시처럼 const 키워드를 생략할 수 있습니다.
변수에 상수 객체를 저장한 적이 있다 해도 해당 변수가 final 또는 const가 아니라면 참조 대상을 변경할 수 있습니다.
foo = [1, 2, 3]; // foo는 원래 상수 빈 배열을 가리켰지만 참조를 교체 가능
하지만 const 변수에는 재할당할 수 없으며 컴파일러가 오류를 표시합니다.
// 정적 분석: 코드 오류
baz = [42]; // 오류: 상수 변수는 재할당 불가
상수를 정의할 때 타입 검사, 타입 캐스트(is, as), 컬렉션 if, 전개 연산자(..., ...?)를 함께 사용할 수 있습니다.
void main() {
const Object i = 3; // i는 int 값을 담는 Object 타입 상수
const list = [i as int]; // 타입 캐스트 사용
const map = {if (i is int) i: 'int'}; // is 검사 + 컬렉션 if
const set = {if (list is List<int>) ...list}; // 전개 연산자와 결합
}
참고
final수식 객체는 참조 대상을 바꿀 수 없지만 객체 내부 필드는 수정 가능합니다. 반면const객체는 객체 자체와 내부 모든 데이터가 완전히 불변입니다.
const로 상수 컬렉션을 생성하는 자세한 내용은 리스트, 맵, 클래스 관련 문서를 참고하세요.
* 와일드카드 변수
버전 안내: 와일드카드 변수를 사용하려면 Dart 3.7 이상 언어 버전이 필요합니다.
_라는 이름의 와일드카드 변수는 바인딩되지 않는 지역 변수/매개변수로 단순 자리 표시자 역할을 합니다. 초기화 표현식이 작성되어 있다면 표현식은 실행되지만 변수에 저장된 값을 읽을 수 없습니다. 같은 스코프 내에 여러 개의 _ 변수를 선언해도 이름 충돌이 발생하지 않습니다.
와일드카드 변수란 무엇인가?
Dart에서 단일 밑줄 _가 바로 와일드카드 변수입니다.
- 사용하지 않을 값을 받을 때 쓰며 컴파일러에 “이 변수는 읽거나 사용하지 않을 것”이라고 알림;
- 컴파일러가 “변수가 선언되었지만 사용되지 않음” 경고를 출력하지 않음;
_를 읽으려 하면 오류가 발생하며 단순히 불필요한 데이터를 버리는 용도로만 사용.
// 첫 번째, 세 번째 값만 취하고 두 번째는 _로 폐기
final [a, _, c] = [10, 20, 30];
print(a); // 10
print(c); // 30
// print(_); // 오류, 와일드카드 변수는 읽을 수 없음
최상위 선언과 라이브러리 개인 접근 권한에 영향을 주는 클래스 멤버에는 와일드카드 변수를 사용할 수 없습니다. 블록 스코프 내 선언만 사용 가능하며 예시는 아래와 같습니다.
- 지역 변수 선언
void main() {
var _ = 1;
int _ = 2;
}
- for 반복문 변수
for (var _ in list) {}
- catch 매개변수
try {
throw '!';
} catch (_) {
print('oops');
}
- 제네릭 타입, 함수 제네릭 매개변수
class T<_> {}
void genericFunction<_>() {}
takeGenericCallback(<_>() => true);
- 함수 매개변수
Foo(_, this._, super._, void _()) {}
list.where((_) => true);
void f(void g(int _, bool _)) {}
typedef T = void Function(String _, String _);