핵심 요약
?는 null 가능성을 타입에 표현하고, !와 late는 안전을 자동으로 보장하는 기능이 아니라 개발자가 책임지는 런타임 단언입니다.
검수 범위
Dart 3의 sound null safety를 기준으로 nullable 타입, null assertion, 지연 초기화의 실패 조건을 검수했습니다.
Flutter 하다가 name.length 한 줄 썼는데 갑자기 에러가 뜨고, ! 붙이니까 또 어느 날 앱이 크래시 나는 경험을 하게 됩니다. 그 원인은 대부분 null safety에서 ?, !, late를 헷갈려서입니다. 이 글에서 세 가지를 “실무에서 왜 자주 쓰는지” 기준으로 깔끔하게 정리해드리겠습니다.
Null safety 한 줄 정의
Dart의 null safety는 “null이 들어올 수 있는지”를 타입으로 구분해서 위험한 코드를 실행 전에 막는 규칙입니다. String은 null 불가, String?는 null 가능입니다.
?의 의미와 사용법
String? name;
name은 문자열일 수도 있고 null일 수도 있습니다.
String? name; // String 또는 null
그래서 바로 쓰면 막힙니다.
String? name; print(name.length); // ❌ 컴파일 에러: null일 수도 있음
실무에서 자주 쓰는 처리 3가지
null 체크 후 사용합니다.
String? name = fetchName();
if (name != null) {
print(name.length); // 안전
}
?.로 null이면 멈춥니다.
String? name; final int? len = name?.length; // name이 null이면 len도 null
??로 기본값을 줍니다.
String? name; final int len = (name ?? '').length; // null이면 ''로 대체
!의 의미와 주의점
!는 nullable을 강제로 non-null처럼 쓰는 연산자입니다.
String? name; print(name!.length); // 실행 중 name이 null이면 크래시
!는 컴파일 단계 안전장치를 끄는 것이므로, 문제를 실행 중 크래시로 미루게 됩니다.
! 대신 권장되는 가드 패턴
void printLen(String? name) {
if (name == null) return; // null이면 종료
print(name.length); // ! 없이 안전
}
late의 의미와 사용법
late는 null 불가 타입을 유지하면서 초기화를 미루고 싶을 때 씁니다.
late String name; // null은 불가, 초기화를 나중에 함
초기화 전에 읽으면 LateInitializationError가 납니다.
late String name;
void main() {
print(name); // ❌ 초기화 전 접근 -> LateInitializationError
}
Flutter에서 late가 자주 나오는 이유
initState에서 컨트롤러를 만들기 때문입니다.
class MyPageState extends State {
late final TextEditingController controller;
@override
void initState() {
super.initState();
controller = TextEditingController(); // ✅ 여기서 초기화
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextField(controller: controller);
}
}
late final의 의미와 사용법
final은 한 번 대입하면 바뀌지 않습니다. 여기에 late가 붙으면 초기화는 나중에 하되 딱 한 번만 대입하는 형태가 됩니다.
late final String token;
void init(String t) {
token = t; // 첫 대입 OK
// token = 'x'; // 두 번째 대입은 불가
}
컨트롤러나 서비스 인스턴스처럼 한 번 세팅되면 고정되는 값에 특히 자주 씁니다.
String?와 late를 고르는 기준
String?는 “없을 수도 있음이 정상”인 경우에 맞습니다. 예를 들면 로그인 전 닉네임, 선택 입력값, API 응답 필드 누락 가능성입니다.
String? nickname; // null 자체가 정상 상태
late는 “반드시 있어야 하는데 초기화 시점만 뒤”인 경우에 맞습니다. 예를 들면 initState에서 무조건 세팅되는 컨트롤러, 런타임 주입되는 의존성입니다.
late final ApiClient client; // 반드시 세팅될 값
초보자가 자주 만나는 에러 두 가지
String?을 String에 바로 넣으려 할 때
String? a = 'hi'; String b = a; // 불가
대부분 이렇게 처리합니다.
String? a = 'hi'; String b = a ?? ''; // 기본값
! 때문에 런타임 크래시가 날 때
String? a; print(a!.length); // a가 null이면 크래시
이 경우 !를 제거하고 가드, 기본값, required 같은 구조로 null을 처리하는 편이 안전합니다.
결론
Dart의 null safety는 “null이 들어올 수 있는가”를 타입으로 구분해, 앱이 실행 중에 터질 수 있는 문제를 작성 단계에서 줄이는 장치입니다. String?처럼 null 가능 타입을 쓰면 반드시 null 처리를 동반해야 하고, !는 그 처리를 건너뛰는 대신 런타임 크래시 위험을 떠안는 선택입니다. 반대로 late와 late final은 “null이 오면 안 되는 값”을 nullable로 끌고 가지 않으면서도 초기화 시점을 늦출 수 있게 해주는 도구라, Flutter의 생명주기(initState 등)와 결합해 실무에서 매우 자주 사용됩니다. 결국 기준은 단순합니다. null이 “정상 상태”라면 ?로 모델링하고, null이 “원래 있으면 안 되는 값인데 초기화만 늦는 것”이라면 late/late final로 구조를 잡는 것이 가장 안전하고 유지보수에 유리합니다.
검증 기준과 참고 자료
이 글은 2026-09-08에 아래 공식 문서와 공개 기술 문서를 기준으로 내용과 용어를 다시 검수했습니다. 제품 버전과 기기 제조사에 따라 화면 이름은 달라질 수 있으므로 실제 화면과 공식 문서를 함께 확인하세요.