🗃️ 내가 다시 볼 것

타입스크립트에서 null을 처리하는 방법

전호영 2025. 10. 19. 17:51

Java로 개발할 때 가장 중요하게 여겼던 것은 Null에 대한 처리이다.

특히 NullPointerException(이하 NPE)이 발생하면 우리 서비스가 갑자기 종료될 수 있기에 사용자 경험에 매우 좋지 않다.

 

타입스크립트/자바스크립트에도 Java의 NPE와 비슷한 Cannot read properties of undefined 오류가 있다.

이 오류는 NPE만큼이나 치명적이다. 프론트엔드에서 이 오류가 발생하면 사용자 화면이 하얗게 변하거나, 특정 기능이 작동하지 않는다.

백엔드에서 발생하면 API 응답이 실패하고 전체 요청이 무산된다.(NPE와 동일하다.)

더 큰 문제는 JavaScript의 특성상 undefinednull이라는 두 가지 '없음'이 공존한다는 점이다. Java 개발자 입장에서는 혼란스러울 수 있지만, TypeScript를 제대로 활용하면 이 문제를 컴파일 타임에 잡아낼 수 있다.

 

JavaScript는 Java와 다르게 undefinednull이라는 두 가지 '없음'이 공존한다. 

이런 특성이 우리를 어지럽게 만든다.

 

undefined와 null은 도대체 뭐가 다를까?


자바스크립트 undefined와 null의 차이

의도 타입(typeof)  예시
undefined 값이 할당되지 않음(변수는 선언되었지만 초기화되지 않음) 'udefined' - 변수를 선언만 했을 경우
- 객체에 없는 속성에 접근시
null 값이 의도적으로 비어 있음(개발자가 명시적으로 빈 값을 할당) 'object' - 명시적으로 객체를 비울 때
- 함수가 유효하지 않은 값을 반환할 때

 

비슷한듯 다르다.

// 1. undefined: 값이 할당되지 않음
let name;
console.log(name);              // 출력: undefined
console.log(typeof name);       // 출력: "undefined"

const user = { username: "Alice" };
console.log(user.age);          // 출력: undefined (속성이 없음)

// 2. null: 명시적으로 비어 있는 값
let data = null;
console.log(data);              // 출력: null
console.log(typeof data);       // 출력: "object"

 

이는 초기 언어 제작 시 발생한 문제로, 현재까지 고쳐지지 않는다.

 

비교연산을 할 경우에도 해당 문제가 발생한다. 

js 계열에선 두 가지 비교 연산자를 사용한다.

 

== 과 ===이다.

 

== 비교 연산자는 타입을 강제로 변환해서 비교한다.

null과 undefined를 동등하다고 간주한다.

 

console.log(null == undefined); // 출력: true (타입 변환 후 동등하다고 간주)
console.log(null == 0);         // 출력: false
console.log(undefined == 0);    // 출력: false

 

=== 는 엄격하게 비교한다.

무슨 말이냐면, 타입과 값을 모두 비교한다.

따라서 이 두 값은 서로 다르게 취급된다.

console.log(null === undefined); // 출력: false (타입이 다르므로)

 

그러다 보니 js로 개발을 하면 대부분 === 를 사용하여 비교한다.

 

이런 자바스크립트의 null / undefined 문제와 비교해 , 코틀린과 자바는 어떻게 Null 안정성을 확보할까?


자바와 코틀린의 Null 처리

자바

자바의 경우, if-else 또는 Optional<T> 클래스를 사용해 Null을 처리한다.

Optional <T>의 의미는 "값이 있을 수도 있음"을 의미한다.

 

 if-else를 사용한 null 처리

String result;
if (name != null) {
  result = name.toUpperCase();
} else {
  result = "DEFAULT_USER";
}

System.out.println(result);

 

null이 될 수 있는 값을 Optional로 감싸서 null 처리

Optional<String> nameOptional = getUsernameOptional();
String result = nameOptional // 값이 존재할 때만 함수 적용
.map(String::toUpperCase) // 값이 없으면 기본값을 반환
.orElse("DEFAULT_USER");

System.out.println(result);

 

Java의 경우, Optional <T>를 사용한 옵셔널 체이닝을 통해 null 체크 없는 코드를 작성할 수 있다.


코틀린

Kotlin의 경우엔 언어에서 null 안정성을 보장한다.

이게 무슨 말이냐면! 

별도로 표기하지 않으면, 모든 타입은 기본적으로 null 값을 가질 수 없다.

fun main() {
    var name = ""
    name = null
    println(name)
}

위 코드를 실행하면
Null cannot be a value of a non-null type 'String'

Null을 할당할 수 없다는 에러가 나온다.

컴파일부터 실패한다.

 

별도로 표기하지 않았을 경우고, 만약 null을 허용하고 싶다면 뒤에? 를 붙여주면 된다.

fun main() {
    var name: String? = ""
    name = null
    println(name)
}

 

이러면 null 이 출력된다.

 

위 언어적 특성을 사용해 kotlin에선 다음과 같은 방식으로 코드를 작성한다.

fun main() {
    var name: String? = "홍길동"
    var length: Int? = name?.length // name이 null이 아니므로 3 할당

    println(length) // 출력: 3
    
    name = null
    length = name?.length          // name이 null이므로 전체 결과는 null 할당

    println(length) // 출력: null
}

 

자바스크립트에선 이런 문제를 null 체크를 통해 해결한다.

이제 자바스크립트의 슈퍼셋인 타입스크립트에서 null을 어떻게 처리하는지 알아보자.


타입스크립트의 널 병합 연산자와 옵셔널 체이닝

타입스크립트는 ??(널 병합 연산자) 와 ?.(옵셔널 체이닝)  두 가지 문법을 활용해 널 안정성을 보장한다.

(좀 더 정확하게 들어가면 '??'과 '?.'는 ECMAScript 2020 표준 문법으로 자바스크립트에서도 사용할 수 있다. 하지만 타입스크립트는 여기에 타입 안정성을 더해준다.)

 

널 병합 연산자(??)

널 병합 연산자(??)란 왼쪽 피연산자가 null 또는 undefined일 때 오른쪽 피연산자를 반환하고, 그렇지 않으면 왼쪽 피연산자를 반환하는 논리 연산자이다.

 

const foo = null ?? "default string";
console.log(foo);
// 출력: "default string"

const baz = 0 ?? 42;
console.log(baz);
// 출력: 0

 

위 코드를 보면 || 연산자와 다른 점이 있다.

falsy 값이 와도 해당 값을 출력한다.

 

옵셔널 체이닝(?.)

?. 은 옵셔널 체이닝 연산자로, kotlin에서 null 처리하는 방식 그대로 사용하면 된다.

const human = {
	name: "홍길동",
	home: {
		city: "서울",
		address: "강남구",
	},
};

const zipcode = human.home.zipcode;
console.log(zipcode);

console.log(human.notExistsMethod?.());

위 코드를 실행하면 undefined가 출력된다.

타입에러를 ?. 을 사용해서 막을 수 있다.

 

그래서 타입스크립트에선.??? 를 섞어서 null 처리를 한다.

interface User {
	id: number;
	home?: {
		address?: {
			zipcode: string;
		} | null;
	};
}

const userWithoutHome: User = { id: 1};
const userWithHome: User = {id: 2, home: { address: {zipcode: "A123"}}};

const defaultZipcode = "00000"; // 기본값 정의

// 1. 집주소가 없는 사용자 처리
const zipcodeA = userWithoutHome.home?.address?.zipcode ?? defaultZipcode;

console.log(`집주소가 없는 사람의 우편번호 : ${zipcodeA}`); // 출력 00000

// 2. 집주소가 있는 사용자 처리
const zipcodeB = userWithHome.home?.address?.zipcode ?? defaultZipcode;

console.log(`집주소가 있는 사람의 우편번호 : ${zipcodeB}`);

 

위 코드를 실행하면 

집주소가 없는 사람의 우편번호 : 00000
집주소가 있는 사람의 우편번호 : A123

 

위와 같이 타입에러 또는 undefined, null 처리 없이 깔끔하게 처리할 수 있다.


더 복잡한 상황이 온다면..?

 

앞에서 타입스크립트의 ?. 과 ?? 를 사용해 null을 안전하게 다루는 방법을 배웠다.

하지만 만약 다음과 같은 상황이면 어떨까?

 

function processUserData(userId: string) {
  const user = findUser(userId);
  if (!user) return null;
  
  const validated = validateUser(user);
  if (!validated) return null;
  
  const enriched = enrichUserData(validated);
  if (!enriched) return null;
  
  const formatted = formatForDisplay(enriched);
  if (!formatted) return null;
  
  return formatted;
}

 

위 코드는 각 단계에서 실패할 수 있다.

추가로

 

[함수 호출 -> null 체크 -> 실패하면 null 반환 -> 성공하면 다음 단계]

 

이 과정이 반복된다.

코드가 길어진다 또는 각 단계마다 로깅을 한다거나 기본 값에 대한 처리가 달라진다면 더더 복잡해진다.

 

Kotlin 이었다면 

val result = findUser(userId)
    ?.let { validateUser(it) }
    ?.let { enrichUserData(it) }
    ?.let { formatForDisplay(it) }

 

let 체이닝을 사용해 처리했을 것이다.


Java 라면 

Optional<DisplayUser> result = findUser(userId)
    .flatMap(this::validateUser)
    .flatMap(this::enrichUserData)
    .map(this::formatForDisplay);

 

Optional 체이닝을 사용했을 것이다.

 

TypeScript에선 어떻게 할 수 있을까?

 

다음 글에선 Java의 Optional과 같은 타입을 직접 구현해 보자.