iBetter Books
수정

변수, 타입, Null Safety

이미 Dart를 써봤다면 기본 변수 선언은 익숙할 겁니다. 이 챕터는 빠른 복습을 목표로 핵심 내용만 압축합니다. Null Safety는 Dart 3.x에서 완전히 정착했으므로, 사운드 null safety 관점에서 정확히 이해하고 넘어갑니다.


변수 선언 방법

// var: 타입 추론, 재할당 가능
var name = 'Dart';
name = 'Flutter';

// final: 한 번 할당 후 불변 (런타임 값 가능)
final now = DateTime.now();

// const: 컴파일 타임 상수 (리터럴만 가능)
const pi = 3.14159;
const version = '3.0';

// 타입 명시 (optional)
String language = 'Dart';
int count = 0;

finalconst의 차이를 명확히 구분합니다.

// final: 런타임에 결정되는 값도 OK
final startTime = DateTime.now();  // 실행할 때마다 다른 값

// const: 반드시 컴파일 타임에 알 수 있어야 함
const maxRetry = 3;               // 리터럴 상수만 가능
// const now = DateTime.now();    // 오류! 컴파일 타임에 알 수 없음

기본 내장 타입

// 숫자
int age = 30;
double height = 175.5;
num value = 42;        // int와 double의 상위 타입

// 문자열
String greeting = 'Hello, Dart!';
String multiLine = '''
  여러 줄
  문자열
''';
String interpolated = 'My name is $name and I am ${age + 1} next year.';

// 불리언
bool isActive = true;
bool isEmpty = false;

// 동적 타입 (사용 자제)
dynamic anything = 42;
anything = 'now a string';  // 타입 오류 없음. 런타임에서 발견됨.

// Object? (최상위 타입)
Object? obj = 42;
obj = 'string';

Null Safety 핵심 개념

Dart 3.x에서는 모든 타입이 기본적으로 non-nullable입니다.

// non-nullable: null 불가
String name = 'Dart';
// name = null;  // 컴파일 오류!

// nullable: ? 붙이면 null 가능
String? nickname = null;
int? age = null;

null 체크 방법

String? nickname = getUserNickname();

// 1. if로 체크
if (nickname != null) {
  print(nickname.toUpperCase());  // 스마트 캐스트로 non-null 처리
}

// 2. null 병합 연산자 ??
final display = nickname ?? 'Anonymous';

// 3. null 조건 접근 ?.
final length = nickname?.length;  // null이면 null 반환

// 4. null 조건 할당 ??=
nickname ??= 'Guest';  // null이면 'Guest' 할당

// 5. null 단언 ! (사용 주의)
final upper = nickname!.toUpperCase();  // null이면 런타임 예외 발생

late 키워드

선언 시점에는 초기화할 수 없지만, 사용 전에 반드시 초기화할 것을 보장하는 경우에 사용합니다.

class Database {
  late Connection _connection;  // 선언 시점에 초기화 불가

  Future<void> connect(String url) async {
    _connection = await Connection.open(url);  // 나중에 초기화
  }

  Future<void> query(String sql) async {
    // _connection이 초기화되지 않으면 런타임 에러
    return _connection.execute(sql);
  }
}

// late final: 한 번만 초기화 가능
class Config {
  late final String apiKey;

  Config(Map<String, String> env) {
    apiKey = env['API_KEY'] ?? '';  // 생성자에서 초기화
  }
}

흐름 분석 (Flow Analysis)

Dart 컴파일러는 코드 흐름을 분석해서 null 여부를 자동으로 판단합니다.

String? value = getValue();

// 흐름 분석: if 이후 non-null임을 인식
if (value == null) return;
print(value.length);  // String으로 승격 — ? 없이 접근 가능

// 타입 검사 후 승격
Object obj = getObject();
if (obj is String) {
  print(obj.toUpperCase());  // String으로 승격
}

// 흐름 분석의 한계: 외부에서 변경될 수 있는 경우
String? field;

void check() {
  if (field == null) return;
  // field가 getter라면 다른 스레드에서 변경될 수 있음
  // final local = field; 로 먼저 잡아두는 것이 안전
  final local = field!;
  print(local.length);
}

타입 변환

// String → int/double
int n = int.parse('42');
double d = double.parse('3.14');

// 실패 시 null 반환 (예외 없음)
int? safe = int.tryParse('abc');  // null

// 숫자 → String
String s = 42.toString();
String formatted = 3.14159.toStringAsFixed(2);  // '3.14'

// int ↔ double
int i = 5;
double dbl = i.toDouble();
int back = dbl.toInt();  // 소수점 버림

// bool
bool b = true;
String bs = b.toString();  // 'true'

정리

핵심 요점만 정리합니다.

  • var / final / const — 용도에 맞게 선택합니다. 기본은 final로 시작합니다.
  • 모든 타입은 non-nullable. null을 허용하려면 ?를 붙입니다.
  • ??, ?., ??=, ! 연산자로 null을 안전하게 처리합니다.
  • late는 초기화 시점을 늦출 때 사용하되, 실제로 초기화되지 않으면 런타임 에러가 납니다.
  • 흐름 분석으로 컴파일러가 nullable 타입을 자동으로 승격시켜줍니다.