- Published on
Flutter 에서 null safety 이해하기 (1)
- Authors

- Name
- Hyeseong Shin
- @hyeseong__e
오늘 회사에서 코드 리뷰를 하다가, 다트에서는 어떻게 null 처리를 하는게 좋을지에 대한 이야기가 나오게 되었다. 아주 다행스럽게도 dart 공식 문서에 null safety가 있어 함께 읽고 정리를 해 보았다.
- null 비허용 (Non-nullable)
- null 허용 (nullable)
- 제네릭에 대한 nullable 형식 매개 변수
- non-null 연산자 ! (The non-null assertion !)
null 비허용 (Non-nullable)
void main() {
int a = 0;
a = null;
print('a 는 ${a}');
}
위의 코드를 실행하면 a = null 에서 아래와 같은 에러가 발생한다.
A value of type 'Null' can't be assigned.
해당 에러가 나는 이유는 a 의 type이 null 을 허용하지 않는 int 값이기 때문이다.
그렇다면, null 을 허용하려면 어떻게 해야할까?
null 허용 (nullable)
위와 동일한 코드에서, a의 type을 지정해 줄 때, 아래와 같이 타입 뒤에 물음표(?)를 입력해 주면된다.
a는 int 타입 또는 null 타입을 허용하는 type이 된다.
void main() {
int? a;
a = null;
print('a 는 ${a}');
}
위 처럼 a 를 선언할 때, int 뒤에
?를 입력해 주면, a에 할당될 수 있는 값은 int 혹은 null 값이다.
제네릭에 대한 nullable 형식 매개 변수
제네릭 타입 또한 nullable 일 수 있고, non-nullable 일 수 있다.
아래의 코드를 dartpad에서 실행시키면 에러가 발생한다.
void main() {
List<String> aListOfStrings = ['one', 'two', 'three'];
List<String> aNullableListOfStrings;
List<String> aListOfNullableStrings = ['one', null, 'three'];
print('aListOfStrings is $aListOfStrings.');
print('aNullableListOfStrings is $aNullableListOfStrings.');
print('aListOfNullableStrings is $aListOfNullableStrings.');
}
첫번째 에러는,
Error: The element type 'Null' can't be assigned to the list type 'String'.
aListOfNullableStrings 변수는 List<String>타입인데, 리스트의 값 중에 null이 들어가서 null 값은 할당될 수 없다고 알려주고 있다. 이 에러를 없애려면, 리스트를 이루는 값이 String 과 null 이 될 수 있으면 된다.
두번째 에러는,
Error: The non-nullable local variable 'aNullableListOfStrings' must be assigned before it can be used.
aNullableListOfStrings 변수는 null 허용 문자열 목록인데, 값을 할당하지 않았기 때문에 일반적으로 Dart는 이런 값을 null로 초기화 한다.
하지만 위의 코드에서는 문자열 리스트로 선언되어져 있다. 이 변수가 전체가 nullable 하다고 지정해 주면 해당 문제는 해결된다. 그래서 아래와 같이 코드를 수정해 주면, 발생하던 에러는 사라진다.
void main() {
List<String> aListOfStrings = ['one', 'two', 'three'];
List<String>? aNullableListOfStrings;
List<String?> aListOfNullableStrings = ['one', null, 'three'];
print('aListOfStrings is $aListOfStrings.');
print('aNullableListOfStrings is $aNullableListOfStrings.');
print('aListOfNullableStrings is $aListOfNullableStrings.');
}
non-null 연산자 ! (The non-null assertion !)
null이 아닌 nullable 으로 지정된 어떤 값을 사용해야 할 때, 해당 값이 사용되는 시점에 값이 있다는 것이 보장 받게 되면, 이 값은 null 이 아니야! 라고 지정해 줄 수 있다. 이 값은 있어! 라고 확인 시켜 주듯, 해당 값 뒤에 !표현식을 넣어주면 된다.
해당 표현식을 넣어주게 되면,
- 해당 값은 null 이 아니다.
- null을 허용하지 않는 변수를 할당할 수 있다.
어떤한 값에 해당 표현식을 사용했는데 그 값이 null이라면, Dart는 런타임 에러를 발생시킨다. 그렇기 때문에, nullable 하지 않은 것이 확신이 설 때만 해당 표현식을 사용해야 한다.
아래의 코드를 보고 예를 들어보자.
int? couldReturnNullButDoesnt() => -3;
void main() {
int? couldBeNullButIsnt = 1;
List<int?> listThatCouldHoldNulls = [2, null, 4];
int a = couldBeNullButIsnt;
int b = listThatCouldHoldNulls.first;
int c = couldReturnNullButDoesnt().abs();
print('a is $a.');
print('b is $b.');
print('c is $c.');
}
위 코드를 살펴보면 신기한 일이 있는데,int? couldBeNullButIsnt = 1; 해당 값이 nullable 임에도 불구하고, int a 에 재 할당 되었을 때, 에러가 나지 않는다.
Dart 는 line 4에 할당된 값이 literal인 것을 알 만큼 충분히 똑똑하고, Dart이 정적 분석이, 같은 함수 스코프에서 할당 된 값이면 실제로는 null이 허용되지 않는 필드 인 것 처럼 처리한다. -> 이를 유형승격이라 한다. (유형승격은 여기서 자세히 알아보자)int? couldBeNullButIsnt = 1; 해당 코드를 main()함수 바깟에 정의하게 되면, line 7 에서 int a 에서 에러를 발생시킨다.\왜냐하면 더이상 Dart는 couldBeNullButIsnt 값이 nullable 인지 아닌지, 확신 할 수 없기 때문이다.
그래서 위의 이슈를 해결하려면 아래와 같이, couldBeNullButIsnt 값이 non-nullable 하다는 것을 dart 에게 확신시켜주면 된다.
int? couldBeNullButIsnt = 1;
void main() {
int a = couldBeNullButIsnt!;
}
그리고 위 코드를 실행시켜보면 두가지 에러가 발생한다.
첫번째 에러는,
Error: A value of type 'int?' can't be assigned to a variable type of int
이다. 이 에러는 listThatCouldHoldNulls.first 값은 nullable 한 값을 가지는 list 인데, 할당 받은 b의 타입이 non-nullable한 int 값이기 때문에 발생한다.
헤당 에러를 수정하기 위해선, dart에게 first 에 값이 확실히 있어서, 아래와 같이 nullable 하지 않다는 것을 알려주면 된다.
void main() {
List<int?> listThatCouldHoldNulls = [2, null, 4];
int b = listThatCouldHoldNulls.first!;
print('b is $b.');
}
두번째 에러는,
Error: The method 'abs' can't be unconditionally invoked because the receiver can be 'null'.
이 에러는 couldReturnNullButDoesnt() 값이 nullable 한 int 값으로 지정되어 있어서, abs()를 호출하는 시점에 couldReturnNullButDoesnt() 값이 null 일 수 있다고 생각하여 발생한다.
그래서 해당 에러를 발생시키지 않으려면 abs() 가 호출 되기 전에 couldReturnNullButDoesnt() 값은 반드시 있다고 dart에게 알려주면 해결된다!
int? couldReturnNullButDoesnt() => -3;
void main() {
int c = couldReturnNullButDoesnt()!.abs();
print('c is $c.');
}