Dart 튜토리얼 2편: 변수와 타입 안전성

요약#

핵심 요지#

  • 문제 정의: 초기화하지 않은 변수에 접근하면 null1 참조 오류가 발생한다.
  • 핵심 주장: Dart는 타입 추론2null 안전성으로 변수 관련 오류를 컴파일 단계에서 잡는다.
  • 주요 근거: 타입 추론, non-nullable3 기본값, late4/final5/const6 키워드가 핵심이다.
  • 실무 기준: nullable7은 필요한 경우에만 사용하고, 불변 데이터는 final/const로 선언한다.
  • 한계: 혼합 버전 코드베이스에서는 null 안전성 보장이 약해질 수 있다.

문서가 설명하는 범위#

  • 변수 선언과 타입 추론이 작동하는 방식
  • nullablenon-nullable의 차이와 실용적 활용
  • late, final, const의 실무 적용 기준

읽는 시간: 16분 | 난이도: 초급


참고 자료#


문제 상황#

대부분의 프로그래밍 언어에서 초기화하지 않은 변수는 예측 불가능한 값을 가집니다.
JavaScript와 Python 같은 동적 언어는 변수에 어떤 타입의 값이든 넣을 수 있어 편리하지만, 타입 불일치 오류가 runtime8에 발생합니다.

기존 방식의 한계#

// JavaScript 예시
let count;
console.log(count.toString()); // undefined.toString() → 오류
let items = [];
items.add(1); // TypeError: items.add is not a function
items.push("hello"); // 타입 체크 없음

문제는 다음과 같습니다.

  • 초기화하지 않은 변수가 어떤 값을 가지는지 불명확하다.
  • 타입 불일치가 실행 시점에 드러나서 디버깅이 어렵다.
  • null 체크를 빼먹으면 예상치 못한 오류가 발생한다.

해결 방법#

Dart는 타입 추론null 안전성을 결합해서 변수 관련 오류를 컴파일 단계에서 잡습니다.

단계 1: 타입 추론으로 편리함과 안전성 확보#

Why#

NOTE

타입을 매번 적기 시작하면 코드가 금방 길어집니다.
그래서 “초기값을 보고 타입을 잡는 방식”으로 반복 입력을 줄입니다.

String name = 'Bob';
int year = 1977;
double diameter = 3.7;

What#

NOTE

타입 추론은 변수의 초기값을 보고 컴파일러가 타입을 결정하는 기능입니다.

How#

TIP

Dart는 변수 선언 시 타입을 명시하지 않아도 초기값을 보고 타입을 추론합니다.
var 키워드를 사용하면 코드가 간결해지면서도 타입 검사는 그대로 작동합니다.

var name = 'Bob'; // String으로 추론
var year = 1977; // int로 추론
var diameter = 3.7; // double로 추론
var satellites = ['Moon']; // List<String>으로 추론

타입을 명시하고 싶으면 직접 선언할 수도 있습니다.

String name = 'Bob';
int year = 1977;

Watch out#

WARNING

var로 선언한 변수는 한 번 타입이 추론되면, 다른 타입 값을 다시 넣을 수 없습니다.

var year = 1977;
// year = '1977'; // 컴파일 오류: year는 int로 추론되었다.

결론: 타입을 적지 않아도 컴파일러가 타입 불일치를 잡아냅니다.


단계 2: nullable과 non-nullable 구분#

Why#

NOTE

null은 “값이 없음”을 표현할 때 편리하지만, 체크를 빼먹기 쉬워서 오류의 원인이 됩니다.
그래서 “어디가 null이 될 수 있는지”를 타입으로 드러내는 규칙이 필요합니다.

String? name = null;
// print(name.length); // 컴파일 오류: name이 null일 수 있다.

What#

NOTE

Dart는 기본적으로 non-nullable을 사용하고, 필요할 때만 nullable을 명시합니다.

How#

TIP

Dart는 기본적으로 모든 변수가 non-nullable입니다.
null을 허용하려면 타입 뒤에 ?를 붙여야 합니다.

int count = 0; // null 불가
int? maybeCount = null; // null 허용
String title = 'Dart';
String? subtitle; // 초기화하지 않으면 null

nullable 변수 사용 시 null 체크 필수

String? name = getName();
// 컴파일 오류: name이 null일 수 있음
// print(name.length);
// 올바른 사용법
if (name != null) {
print(name.length);
}
// 또는 null-aware 연산자 사용
print(name?.length); // name이 null이면 null 반환

Watch out#

WARNING

nullable은 “편의”가 아니라 “정말 null이 필요한 경우”에만 선택하는 게 안전합니다.

결론: nullable 변수에 접근할 때 null 체크를 강제해서 runtime 오류를 예방합니다.


단계 3: late로 초기화 시점 미루기#

Why#

NOTE

값은 지금 정할 수 없지만, “사용 전에는 반드시 값이 들어간다”는 상황이 있습니다.
이때 nullable로 돌려서 흐림 처리하기보다, 초기화 시점을 분리하는 편이 더 명확할 수 있습니다.

// 값을 나중에 넣어도 되지만, 사용할 때는 null이 아니어야 한다.
late String description;

What#

NOTE

late는 초기화를 나중으로 미루되, 타입은 non-nullable로 유지하게 해줍니다.

How#

TIP

변수를 선언할 때 값이 없지만 사용 전에 반드시 초기화된다면 late 키워드를 사용합니다.

late String description;
void main() {
description = 'Feijoada!';
print(description); // OK
}

lazy initialization9

비용이 큰 초기화를 실제 사용 시점까지 미룰 수 있습니다.

late String temperature = readThermometer(); // 사용 전까지 호출 안 됨

Watch out#

WARNING

late 변수를 초기화하지 않고 사용하면 runtime 오류가 발생합니다.

late String description;
void main() {
// description = '값';
// print(description); // runtime 오류: 초기화되지 않은 late 변수 접근
}

결론: 초기화 시점을 제어하면서도 non-nullable 타입을 유지합니다.


단계 4: final과 const로 불변 데이터 선언#

Why#

NOTE

값이 계속 바뀌는 변수는 어디서 값이 바뀌는지 추적하기 어렵습니다.
그래서 “바뀌면 안 되는 값”은 선언 단계에서 잠그는 편이 안전합니다.

final name = 'Bob';
// name = 'Alice'; // 컴파일 오류: final은 재할당 불가

What#

NOTE

final은 한 번만 할당되는 값이고, constcompile-time10에 확정되는 상수입니다.

How#

TIP

값을 한 번만 할당하고 변경하지 않을 변수는 final 또는 const로 선언합니다.

final: runtime 상수

final name = 'Bob'; // 타입 추론
final String nickname = 'Bobby';
// name = 'Alice'; // 컴파일 오류

실행 중에 결정되는 값을 final로 선언할 수 있습니다.

final timestamp = DateTime.now(); // runtime에 결정

const: compile-time 상수

const bar = 1000000; // 숫자 리터럴
const double atm = 1.01325 * bar; // 컴파일 시 계산됨

const는 컴파일 시점에 값이 확정되어야 합니다.

var foo = const []; // foo는 변수, 값은 const
final bar = const []; // bar도 변수, 값은 const
const baz = []; // baz 자체가 const (= const [])
foo = [1, 2, 3]; // OK: foo는 var이므로 재할당 가능
// bar = [1, 2, 3]; // 오류: final은 재할당 불가
// baz = [1, 2, 3]; // 오류: const는 재할당 불가

설계 의도는 다음과 같습니다.

  • final: 한 번만 할당되지만 runtime에 값이 결정되는 경우
  • const: 컴파일 시점에 값이 확정되는 상수

Watch out#

WARNING

final은 “재할당”만 막습니다.
const는 “값 자체가 상수”라서, const로 만든 컬렉션은 수정할 수 없습니다.

final list1 = [1, 2, 3];
list1.add(4); // OK: final은 재할당만 막는다.
const list2 = [1, 2, 3];
// list2.add(4); // 오류: const 컬렉션은 수정할 수 없다.

결론: 불변 데이터를 명시적으로 표현해서 의도치 않은 수정을 방지합니다.


한계#

Dart 2.x에서는 null 안전 코드와 null 비안전 코드가 섞일 수 있었습니다.
이 경우 unsound null safety11 상태가 되어 보장이 약해집니다.

  • null 비안전 라이브러리와 함께 사용하면 non-nullable 변수가 runtimenull을 가질 가능성이 생깁니다.
  • Dart 3부터는 null 안전이 필수이므로 이 문제가 해결되었습니다.

Footnotes#

  1. null(널): 값이 없음을 나타내는 특수한 값이다.

  2. type inference(타입 추론): 변수의 초기값을 보고 컴파일러가 타입을 자동으로 결정하는 기능이다.

  3. non-nullable(널 비허용): null 값을 가질 수 없는 타입이다.

  4. late(지연 초기화): 선언 시점이 아닌 나중에 초기화할 수 있게 하는 키워드다.

  5. final(파이널): 한 번만 할당 가능한 변수를 선언하는 키워드다.

  6. const(상수): 컴파일 시점에 값이 확정되는 상수를 선언하는 키워드다.

  7. nullable(널 허용): null 값을 가질 수 있는 타입으로 ?를 붙여 표시한다.

  8. runtime(런타임): 코드가 실제로 실행되는 시점과 환경을 뜻한다.

  9. lazy initialization(지연 초기화): 실제 사용 시점까지 초기화를 미루는 방식이다.

  10. compile-time(컴파일 타임): 코드가 컴파일되는 시점을 뜻한다.

  11. unsound null safety(불완전한 널 안전): null 안전 코드와 null 비안전 코드가 섞여 보장이 약해지는 상태다.

공유

이 글이 도움이 되었다면 다른 사람과 공유해주세요!

Dart 튜토리얼 2편: 변수와 타입 안전성
https://moodturnpost.net/posts/dart/dart-variables-types-null-safety/
작성자
Moodturn
게시일
2026-01-04
Moodturn

목차