목록으로

프로그래밍 · TypeScript

TypeScript 완전정복 7화: null과 undefined 제대로 이해하기

BeanCon
TypeScript에서 null과 undefined를 안전하게 다루는 방법을 설명하는 대표 이미지

TypeScript 완전정복 7화입니다. 값이 없는 상태를 표현하는 null과 undefined를 입문자 눈높이에서 정리했습니다. 두 값의 차이, typeof null, strictNullChecks, 안전한 검사 방법, 널 병합 연산자, 선택적 연결 연산자, 함수와 객체에서 값이 없는 상태를 다루는 방법, API와 JSON에서의 처리 기준까지 다룹니다.

목차

null과 undefined 제대로 이해하기

지난 시간에는 TypeScript의 기본 타입 삼총사를 만났습니다.

const userName: string = "김타입";
const age: number = 25;
const isLearning: boolean = true;

문자열은 `string`, 숫자는 `number`, 참과 거짓은 `boolean`로 표현할 수 있었습니다.

그런데 현실의 프로그램에서는 항상 값이 준비되어 있는 것은 아닙니다.

사용자가 아직 프로필 사진을 등록하지 않았을 수도 있습니다.

const profileImage = null;

서버에서 데이터가 아직 도착하지 않았을 수도 있습니다.

let responseData: string | undefined;

객체에 우리가 찾는 속성이 없을 수도 있습니다.

const user = {
  name: "김타입",
};

console.log(user.nickname);

프로그램에서 값이 없음은 오류가 아니라 하나의 중요한 상태입니다.

문제는 값이 없다는 사실을 무시하고 사용하는 순간 발생합니다.

const userName: string | null = null;

console.log(userName.toUpperCase());

사용자 이름이 없는데 대문자로 바꾸려고 했습니다.

TypeScript가 없었다면 프로그램은 실행 중에 넘어졌을 것입니다.

TypeScript는 코드가 달리기 전에 호루라기를 붑니다.

“잠깐! 지금 그 상자 안은 비어 있을 수도 있습니다.” 🚨

이번 시간에는 값이 없는 상태를 표현하는 두 가지 타입을 알아봅니다.

null
undefined

둘 다 “값이 없다”는 의미를 가지고 있지만, 만들어지는 과정과 사용하는 목적은 다릅니다.

1. 이번 시간에 배울 내용

이번 시간에는 다음 내용을 알아봅니다.

  • `null`은 무엇인가?
  • `undefined`는 무엇인가?
  • 두 값은 어떻게 다른가?
  • `typeof null`의 결과는 왜 이상한가?
  • `strictNullChecks`는 왜 중요한가?
  • 값이 없을 가능성을 타입으로 표현하는 방법
  • 안전하게 `null`과 `undefined`를 확인하는 방법
  • `?.` 선택적 연결 연산자 사용하기
  • `??` 널 병합 연산자 사용하기
  • `||`와 `??`는 무엇이 다른가?
  • 함수와 객체에서 값이 없는 상태 다루기
  • 실무에서 `null`과 `undefined`를 선택하는 기준

오늘의 등장인물은 조금 특이합니다.

null      : 일부러 비워둔 빈 상자
undefined : 아직 내용물이 정해지지 않은 상자

둘 다 상자는 비어 있지만 사연이 다릅니다.

하나는 계획된 공석이고, 다른 하나는 아직 배정되지 않은 자리입니다. 🪑

2. 값이 없다는 것은 어떤 의미일까?

다음 변수에는 문자열이 들어 있습니다.

const userName: string = "김타입";

이 변수의 상태는 명확합니다.

userName이라는 변수에 "김타입"이라는 값이 있다.

하지만 프로그램에서는 값이 없는 상황도 자주 발생합니다.

예를 들어 쇼핑몰에서 사용자가 쿠폰을 선택하지 않았다고 생각해 보겠습니다.

let selectedCoupon = null;

검색 결과에서 원하는 사용자를 찾지 못할 수도 있습니다.

const foundUser = null;

함수에 선택 사항인 값을 전달하지 않을 수도 있습니다.

function greet(message?: string) {
  console.log(message);
}

greet();

이 경우 `message`는 `undefined`가 됩니다.

값이 없다는 상태는 다음과 같은 여러 의미를 가질 수 있습니다.

아직 값이 정해지지 않았다.
값을 찾지 못했다.
사용자가 선택하지 않았다.
데이터가 아직 도착하지 않았다.
해당 정보가 존재하지 않는다.
개발자가 의도적으로 값을 비워두었다.

따라서 값이 없다는 사실도 타입으로 정확하게 표현해야 합니다.

3. `null`이란?

`null`은 개발자가 의도적으로 값이 없음을 표현할 때 사용하는 값입니다.

const selectedUser: null = null;

하지만 `null` 타입만 단독으로 사용하는 경우는 많지 않습니다.

다음 변수는 오직 `null`만 저장할 수 있기 때문입니다.

let selectedUser: null = null;

다른 값을 저장하면 오류가 발생합니다.

selectedUser = "김타입";

오류 메시지는 다음과 비슷합니다.

Type '"김타입"' is not assignable to type 'null'.

실제 프로그램에서는 보통 정상적인 타입과 `null`을 함께 사용합니다.

let selectedUser: string | null = null;

이 변수에는 문자열 또는 `null`을 저장할 수 있습니다.

selectedUser = "김타입";
selectedUser = null;

여기에서 `|` 기호는 또는이라는 의미입니다.

string | null
문자열 또는 null

여러 타입을 연결하는 문법은 9편의 유니언 타입에서 자세히 다룰 예정입니다.

지금은 값이 없을 가능성을 타입에 포함하기 위해 사용한다고 이해하면 충분합니다.

4. `null`은 언제 사용할까?

`null`은 일반적으로 개발자가 의도적으로 값을 비우거나, 값을 찾지 못했다는 결과를 표현할 때 사용합니다.

선택된 값이 없을 때

let selectedProductId: number | null = null;

사용자가 상품을 선택하면 숫자가 저장됩니다.

selectedProductId = 101;

선택을 취소하면 다시 `null`을 저장할 수 있습니다.

selectedProductId = null;

검색 결과가 없을 때

type User = {
  id: number;
  name: string;
};

function findUser(id: number): User | null {
  if (id === 1) {
    return {
      id: 1,
      name: "김타입",
    };
  }

  return null;
}

사용자를 찾으면 사용자 객체를 반환하고, 찾지 못하면 `null`을 반환합니다.

const user = findUser(10);

console.log(user);

출력 결과:

null

아직 연결되지 않은 요소를 저장할 때

브라우저에서 HTML 요소를 찾는 코드를 생각해 보겠습니다.

const button = document.querySelector("button");

`querySelector()`는 요소를 찾지 못할 수 있습니다.

따라서 결과 타입에는 `null`이 포함됩니다.

Element | null

요소가 실제로 존재하는지 확인한 뒤 사용해야 합니다.

if (button !== null) {
  button.addEventListener("click", () => {
    console.log("버튼 클릭");
  });
}

TypeScript는 버튼이 없는 상태에서 이벤트를 연결하는 사고를 막아줍니다.

5. `undefined`란?

`undefined`는 값이 아직 할당되지 않았거나 존재하지 않을 때 나타나는 값입니다.

let userName: string | undefined;

아직 값을 할당하지 않았으므로 현재 값은 `undefined`입니다.

console.log(userName);

출력 결과:

undefined

이후 문자열을 저장할 수 있습니다.

userName = "김타입";

다시 `undefined`를 저장할 수도 있습니다.

userName = undefined;

`undefined`는 개발자가 직접 작성할 수도 있지만, JavaScript가 자동으로 만들어 주는 경우가 많습니다.

6. `undefined`가 자동으로 만들어지는 순간

변수를 선언하고 값을 할당하지 않았을 때

let message: string | undefined;

console.log(message);

출력 결과:

undefined

변수라는 방은 만들어졌지만 아직 입주자가 없습니다.

객체에 존재하지 않는 속성을 읽을 때

const user = {
  name: "김타입",
};

console.log(user.nickname);

TypeScript에서는 객체 타입에 `nickname`이 정의되지 않았다면 오류가 발생합니다.

선택적 속성으로 정의해 보겠습니다.

type User = {
  name: string;
  nickname?: string;
};

const user: User = {
  name: "김타입",
};

console.log(user.nickname);

출력 결과:

undefined

`nickname?`은 해당 속성이 있을 수도 있고 없을 수도 있다는 의미입니다.

nickname?: string

개념적으로는 다음과 비슷합니다.

nickname: string | undefined

함수의 인수를 전달하지 않았을 때

function greet(message?: string) {
  console.log(message);
}

greet();

출력 결과:

undefined

`message?`는 선택적 매개변수입니다.

값을 전달해도 되고 전달하지 않아도 됩니다.

greet("안녕하세요.");
greet();

반환값이 없는 함수의 결과를 받을 때

function printMessage() {
  console.log("메시지를 출력합니다.");
}

const result = printMessage();

console.log(result);

출력 결과:

메시지를 출력합니다.
undefined

함수에 `return`이 없으면 실행 결과로 `undefined`가 반환됩니다.

반환값이 없는 함수를 나타내는 `void` 타입은 10편에서 자세히 다룰 예정입니다.

배열이나 컬렉션에서 값을 찾지 못했을 때

const numbers = [10, 20, 30];

const foundNumber = numbers.find(
  (number) => number === 100
);

console.log(foundNumber);

출력 결과:

undefined

`find()`는 원하는 값을 찾지 못하면 `undefined`를 반환합니다.

배열은 다음 편에서 본격적으로 살펴보겠습니다.

7. `null`과 `undefined`의 차이

두 값은 모두 값이 없다는 의미를 나타냅니다.

하지만 일반적으로 다음처럼 구분할 수 있습니다.

구분`null``undefined`
주요 의미의도적으로 비워둔 값아직 값이 정해지지 않음
누가 주로 만드는가?개발자JavaScript 또는 개발자
변수 초기화명시적으로 사용값 미할당 시 발생
객체 속성명시적으로 비움속성이 없을 때 발생
함수 인수직접 전달 가능인수를 생략하면 발생
검색 결과찾지 못함을 표현할 때 사용 가능여러 기본 API가 사용

쉽게 표현하면 다음과 같습니다.

null
"여기에는 값이 없기로 결정했습니다."

undefined
"여기에 어떤 값이 들어올지 아직 정해지지 않았습니다."

호텔 객실에 비유해 보겠습니다.

null      : 객실은 있지만 오늘은 사용하지 않기로 결정
undefined : 객실 배정표가 아직 작성되지 않음

둘 다 손님은 없지만 프런트 직원의 설명은 다릅니다. 🛎️

8. 직접 비교해 보기

다음 두 값을 만들어 보겠습니다.

const emptyValue: null = null;
const unknownValue: undefined = undefined;

엄격한 일치 연산자인 `===`로 비교하면 두 값은 다릅니다.

console.log(null === undefined);

출력 결과:

false

타입과 값이 서로 다르기 때문입니다.

console.log(typeof null);
console.log(typeof undefined);

출력 결과:

object
undefined

그런데 `typeof null`의 결과가 이상합니다.

`null`인데 왜 `object`일까요?

9. `typeof null`이 `object`인 이유

다음 코드를 실행해 보겠습니다.

console.log(typeof null);

출력 결과:

object

`null`은 객체가 아닙니다.

그런데도 `typeof null`은 `"object"`를 반환합니다.

이 현상은 JavaScript 초기 구현에서 생긴 오래된 설계 문제입니다.

당시 값을 내부적으로 구분하는 과정에서 `null`이 객체와 같은 표시를 갖게 되었고, 이후 호환성 문제 때문에 그대로 유지되었습니다.

즉, 다음 코드는 믿으면 안 됩니다.

const value = null;

if (typeof value === "object") {
  console.log("객체입니다.");
}

이 조건은 `null`일 때도 참이 됩니다.

객체 여부를 확인하려면 `null`을 별도로 제외해야 합니다.

if (
  typeof value === "object" &&
  value !== null
) {
  console.log("실제 객체입니다.");
}

`typeof null === "object"`는 JavaScript 박물관에 전시된 고대 유물과 같습니다.

없애고 싶지만 너무 많은 프로그램이 그 유물을 기준으로 지어져 있어 철거할 수 없습니다. 🏛️

10. 느슨한 비교와 엄격한 비교

`null`과 `undefined`를 느슨한 일치 연산자 `==`로 비교하면 결과가 참입니다.

console.log(null == undefined);

출력 결과:

true

하지만 엄격한 일치 연산자 `===`로 비교하면 거짓입니다.

console.log(null === undefined);

출력 결과:

false

정리하면 다음과 같습니다.

null == undefined   → true
null === undefined  → false

일반적인 값 비교에서는 `===` 사용을 권장합니다.

if (value === null) {
  console.log("값이 null입니다.");
}

if (value === undefined) {
  console.log("값이 undefined입니다.");
}

다만 `null`과 `undefined`를 한 번에 검사하려는 코드에서는 다음 표현을 의도적으로 사용하기도 합니다.

if (value == null) {
  console.log("값이 null 또는 undefined입니다.");
}

이 조건은 정확히 두 값에 대해 참입니다.

null
undefined

하지만 초보 단계에서는 명시적으로 검사하는 방식이 더 읽기 쉽습니다.

if (
  value === null ||
  value === undefined
) {
  console.log("값이 없습니다.");
}

11. `strictNullChecks`란?

TypeScript에서 `null`과 `undefined`를 안전하게 다루려면 `strictNullChecks` 설정이 중요합니다.

`tsconfig.json`에서 다음처럼 설정할 수 있습니다.

{
  "compilerOptions": {
    "strictNullChecks": true
  }
}

또는 `strict` 옵션을 활성화할 수 있습니다.

{
  "compilerOptions": {
    "strict": true
  }
}

`strict: true`에는 `strictNullChecks`를 포함한 여러 엄격한 검사 옵션이 포함됩니다.

12. `strictNullChecks`가 꺼져 있다면?

`strictNullChecks`가 꺼져 있으면 `null`과 `undefined`를 여러 타입에 할당할 수 있습니다.

let userName: string = null;
let age: number = undefined;

이런 코드는 위험할 수 있습니다.

`userName`은 타입상 문자열인데 실제 값은 `null`입니다.

console.log(userName.toUpperCase());

TypeScript가 오류를 발견하지 못하면 실행 중에 문제가 발생할 수 있습니다.

Cannot read properties of null

문자열이라고 적힌 상자를 열었는데 문자열 대신 텅 빈 공간이 들어 있던 것입니다.

13. `strictNullChecks`가 켜져 있다면?

`strictNullChecks`가 켜져 있으면 다음 코드는 오류가 발생합니다.

let userName: string = null;

`string` 타입에는 `null`을 저장할 수 없기 때문입니다.

`null`이 들어올 수 있다면 타입에 명시해야 합니다.

let userName: string | null = null;

`undefined`가 들어올 수 있다면 다음처럼 작성합니다.

let message: string | undefined;

둘 다 들어올 수 있다면 다음처럼 표현할 수 있습니다.

let value: string | null | undefined;

TypeScript는 이제 개발자에게 확인을 요구합니다.

“이 값이 정말 문자열인지 확인한 뒤 사용해 주세요.”

조금 까다로워 보이지만, 실행 중에 터질 오류를 개발 중에 발견할 수 있습니다.

14. `strictNullChecks`를 사용하는 이유

다음 함수를 살펴보겠습니다.

function findUserName(id: number): string | null {
  if (id === 1) {
    return "김타입";
  }

  return null;
}

함수를 호출합니다.

const userName = findUserName(10);

사용자를 찾지 못했으므로 `userName`은 `null`입니다.

그런데 바로 문자열 메서드를 호출하면 오류가 발생합니다.

console.log(userName.toUpperCase());

TypeScript가 알려주는 핵심 내용은 다음과 같습니다.

'userName' is possibly 'null'.

`userName`이 `null`일 가능성이 있다는 뜻입니다.

이 경고 덕분에 우리는 값의 존재 여부를 먼저 확인할 수 있습니다.

if (userName !== null) {
  console.log(userName.toUpperCase());
}

TypeScript는 조건문 안에서 `userName`이 문자열이라는 사실을 이해합니다.

조건문 밖: string | null
조건문 안: string

이렇게 조건을 통해 타입의 범위를 줄이는 것을 타입 좁히기라고 합니다.

타입 좁히기는 PART 4에서 깊이 있게 다룰 예정입니다.

15. `null`을 안전하게 확인하기

다음 변수에는 문자열 또는 `null`이 들어올 수 있습니다.

let userName: string | null = null;

문자열 기능을 사용하기 전에 `null`인지 확인해야 합니다.

if (userName !== null) {
  console.log(userName.toUpperCase());
}

`else`를 사용해 값이 없는 경우도 처리할 수 있습니다.

if (userName !== null) {
  console.log(`사용자: ${userName}`);
} else {
  console.log("사용자 정보가 없습니다.");
}

이 방식은 코드의 의도가 명확합니다.

값이 있으면 사용한다.
값이 없으면 별도의 처리를 한다.

16. `undefined`를 안전하게 확인하기

다음 변수에는 문자열 또는 `undefined`가 들어올 수 있습니다.

let message: string | undefined;

값이 있는지 확인한 뒤 사용할 수 있습니다.

if (message !== undefined) {
  console.log(message.toUpperCase());
}

값이 없을 때 기본 메시지를 보여줄 수도 있습니다.

if (message === undefined) {
  console.log("메시지가 없습니다.");
} else {
  console.log(message);
}

17. `null`과 `undefined`를 함께 확인하기

다음 변수에는 세 가지 상태가 들어올 수 있습니다.

let userName:
  | string
  | null
  | undefined;

값이 있는지 명확하게 확인해 보겠습니다.

if (
  userName !== null &&
  userName !== undefined
) {
  console.log(userName.toUpperCase());
}

조건문 내부에서는 `userName`이 문자열로 좁혀집니다.

조금 더 짧게 작성할 수도 있습니다.

if (userName != null) {
  console.log(userName.toUpperCase());
}

`!= null`은 `null`과 `undefined`를 모두 제외합니다.

다만 팀의 코드 규칙에 따라 명시적인 비교 방식을 선호할 수도 있습니다.

18. 단순히 `if (value)`로 검사해도 될까?

다음 코드는 값이 있는지 검사하는 것처럼 보입니다.

if (value) {
  console.log("값이 있습니다.");
}

하지만 이 조건은 `null`과 `undefined`만 검사하지 않습니다.

다음 값들도 모두 거짓으로 판단합니다.

false
0
""
NaN
null
undefined

예를 들어 숫자 `0`이 유효한 값이라고 생각해 보겠습니다.

const quantity: number | null = 0;

if (quantity) {
  console.log(`수량: ${quantity}`);
} else {
  console.log("수량이 없습니다.");
}

출력 결과:

수량이 없습니다.

하지만 수량은 없는 것이 아니라 `0`입니다.

값이 없는 상태만 확인하려면 정확하게 비교해야 합니다.

if (quantity !== null) {
  console.log(`수량: ${quantity}`);
}

빈 문자열도 유효한 값일 수 있습니다.

const searchKeyword: string | null = "";

if (searchKeyword !== null) {
  console.log("검색어가 입력되었습니다.");
}

따라서 다음 두 질문은 서로 다릅니다.

값이 참인가?
값이 null 또는 undefined가 아닌가?

TypeScript에서는 질문을 정확하게 해야 대답도 정확하게 돌아옵니다.

19. 기본값을 지정하는 전통적인 방법

값이 없을 때 기본값을 사용하고 싶을 수 있습니다.

const userName: string | null = null;

조건 연산자를 사용하면 기본값을 지정할 수 있습니다.

const displayName =
  userName !== null
    ? userName
    : "익명 사용자";

결과:

익명 사용자

`undefined`까지 함께 처리하려면 조건이 조금 길어집니다.

const displayName =
  userName !== null &&
  userName !== undefined
    ? userName
    : "익명 사용자";

이럴 때 사용할 수 있는 연산자가 있습니다.

바로 `??`입니다.

20. 널 병합 연산자 `??`

`??`는 왼쪽 값이 `null` 또는 `undefined`일 때 오른쪽 값을 사용합니다.

const userName: string | null = null;

const displayName =
  userName ?? "익명 사용자";

console.log(displayName);

출력 결과:

익명 사용자

왼쪽에 문자열이 들어 있으면 원래 값을 사용합니다.

const userName: string | null = "김타입";

const displayName =
  userName ?? "익명 사용자";

console.log(displayName);

출력 결과:

김타입

동작 원리는 다음과 같습니다.

왼쪽 값이 null 또는 undefined
→ 오른쪽 기본값 사용

왼쪽 값이 존재함
→ 왼쪽 값 사용

`??`는 비어 있는 좌석을 발견하면 예비 손님을 앉혀주는 안내 직원입니다.

21. `||`와 `??`의 차이

기본값을 설정할 때 `||` 연산자도 자주 사용됩니다.

const displayName =
  userName || "익명 사용자";

하지만 `||`는 `null`과 `undefined`뿐 아니라 모든 Falsy 값을 빈 값으로 판단합니다.

0
""
false
NaN
null
undefined

숫자 `0`을 비교해 보겠습니다.

const count = 0;

console.log(count || 10);
console.log(count ?? 10);

출력 결과:

10
0

`||`는 `0`을 거짓으로 판단해 기본값 `10`을 사용합니다.

`??`는 `0`을 정상적인 값으로 인정합니다.

빈 문자열도 비교해 보겠습니다.

const message = "";

console.log(message || "기본 메시지");
console.log(message ?? "기본 메시지");

출력 결과:

기본 메시지

두 번째 결과는 빈 문자열입니다.

정리하면 다음과 같습니다.

연산자기본값을 사용하는 경우
-------------------------------------------------
``왼쪽 값이 Falsy일 때
`??`왼쪽 값이 `null` 또는 `undefined`일 때

`0`, 빈 문자열, `false`가 유효한 값이라면 `??`가 더 안전합니다.

22. 선택적 연결 연산자 `?.`

객체 안에 값이 없을 가능성이 있을 때 `?.`를 사용할 수 있습니다.

다음 타입을 살펴보겠습니다.

type User = {
  name: string;
  profile?: {
    nickname?: string;
  };
};

사용자에게 `profile`이 없을 수도 있고, 프로필 안에 `nickname`이 없을 수도 있습니다.

const user: User = {
  name: "김타입",
};

다음처럼 바로 접근하면 문제가 될 수 있습니다.

console.log(user.profile.nickname);

`profile`이 `undefined`일 가능성이 있기 때문입니다.

조건문으로 확인할 수 있습니다.

if (
  user.profile !== undefined &&
  user.profile.nickname !== undefined
) {
  console.log(user.profile.nickname);
}

하지만 코드가 길어집니다.

선택적 연결 연산자를 사용하면 간단하게 작성할 수 있습니다.

console.log(user.profile?.nickname);

`profile`이 없다면 결과는 `undefined`입니다.

오류를 발생시키지 않고 안전하게 접근을 중단합니다.

23. `?.`와 `??` 함께 사용하기

선택적 연결 연산자와 널 병합 연산자를 함께 사용하면 매우 편리합니다.

type User = {
  name: string;
  profile?: {
    nickname?: string;
  };
};

const user: User = {
  name: "김타입",
};

const nickname =
  user.profile?.nickname ?? "닉네임 없음";

console.log(nickname);

출력 결과:

닉네임 없음

코드를 순서대로 해석하면 다음과 같습니다.

user.profile이 존재하는가?
↓
존재하면 nickname을 확인한다.
↓
결과가 null 또는 undefined라면 "닉네임 없음"을 사용한다.

한 줄이지만 작은 안전요원 두 명이 함께 근무하고 있습니다.

?.  : 들어가도 안전한지 문을 확인
??  : 값이 없으면 기본값 준비

24. 메서드 호출에도 `?.`를 사용할 수 있다

함수가 없을 가능성이 있을 때도 선택적 연결 연산자를 사용할 수 있습니다.

type ButtonOptions = {
  onClick?: () => void;
};

const options: ButtonOptions = {};

`onClick`이 존재하는지 확인한 뒤 호출할 수 있습니다.

if (options.onClick !== undefined) {
  options.onClick();
}

선택적 호출을 사용하면 더 간단합니다.

options.onClick?.();

`onClick`이 존재하면 함수를 호출하고, 없으면 아무 작업도 하지 않습니다.

25. 함수에서 `null` 반환하기

검색 함수는 원하는 값을 찾지 못할 수 있습니다.

type Product = {
  id: number;
  name: string;
};

const products: Product[] = [
  {
    id: 1,
    name: "키보드",
  },
  {
    id: 2,
    name: "마우스",
  },
];

상품을 찾는 함수를 만들어 보겠습니다.

function findProduct(
  id: number
): Product | null {
  const product = products.find(
    (item) => item.id === id
  );

  return product ?? null;
}

`find()`는 값을 찾지 못하면 `undefined`를 반환합니다.

함수에서는 검색 실패 결과를 `null`로 통일했습니다.

const product = findProduct(10);

결과를 사용하기 전에 확인합니다.

if (product !== null) {
  console.log(product.name);
} else {
  console.log("상품을 찾지 못했습니다.");
}

함수의 반환 타입을 보면 호출하는 사람도 값이 없을 가능성을 알 수 있습니다.

Product | null

타입이 함수의 사용 설명서 역할을 하는 것입니다.

26. 함수에서 `undefined` 반환하기

값을 찾지 못했을 때 `undefined`를 반환하도록 설계할 수도 있습니다.

function findProduct(
  id: number
): Product | undefined {
  return products.find(
    (product) => product.id === id
  );
}

함수를 호출합니다.

const product = findProduct(10);

`undefined` 여부를 확인한 뒤 사용합니다.

if (product !== undefined) {
  console.log(product.name);
}

`null`과 `undefined` 중 어느 쪽을 반환할지는 프로젝트의 규칙에 따라 정할 수 있습니다.

중요한 것은 한 프로젝트 안에서 일관되게 사용하는 것입니다.

27. 선택적 매개변수와 `undefined`

함수의 매개변수 뒤에 `?`를 붙이면 선택적으로 만들 수 있습니다.

function greet(
  name: string,
  message?: string
) {
  console.log(name);
  console.log(message);
}

두 번째 인수는 생략할 수 있습니다.

greet("김타입");

이때 `message`는 `undefined`입니다.

function greet(
  name: string,
  message?: string
) {
  const finalMessage =
    message ?? "반갑습니다.";

  console.log(
    `${name}님, ${finalMessage}`
  );
}

호출 결과를 비교해 보겠습니다.

greet("김타입");
greet("김타입", "환영합니다.");

출력 결과:

김타입님, 반갑습니다.
김타입님, 환영합니다.

28. 기본 매개변수 사용하기

선택적 매개변수에 기본값을 지정할 수도 있습니다.

function greet(
  name: string,
  message: string = "반갑습니다."
) {
  console.log(`${name}님, ${message}`);
}

두 번째 인수를 생략하면 기본값이 사용됩니다.

greet("김타입");

출력 결과:

김타입님, 반갑습니다.

`undefined`를 직접 전달해도 기본값이 사용됩니다.

greet("김타입", undefined);

하지만 `null`을 전달하면 기본값이 자동으로 적용되지 않습니다.

greet("김타입", null);

매개변수 타입이 `string`이므로 `strictNullChecks` 환경에서는 오류가 발생합니다.

기본 매개변수는 `undefined`에 반응하지만 `null`에는 반응하지 않는다는 점을 기억해야 합니다.

29. 선택적 객체 속성

객체의 속성에도 `?`를 사용할 수 있습니다.

type User = {
  id: number;
  name: string;
  nickname?: string;
};

`nickname`은 선택 사항입니다.

따라서 다음 객체는 정상입니다.

const user: User = {
  id: 1,
  name: "김타입",
};

닉네임을 포함해도 정상입니다.

const user: User = {
  id: 1,
  name: "김타입",
  nickname: "타입마스터",
};

선택적 속성을 읽으면 `undefined`일 가능성이 있습니다.

console.log(user.nickname);

안전하게 기본값을 적용할 수 있습니다.

const nickname =
  user.nickname ?? "닉네임 없음";

30. `null` 속성과 선택적 속성은 다르다

다음 두 타입을 비교해 보겠습니다.

type UserA = {
  nickname?: string;
};

type UserB = {
  nickname: string | null;
};

`UserA`에서는 `nickname` 속성 자체를 생략할 수 있습니다.

const userA: UserA = {};

`UserB`에서는 속성이 반드시 존재해야 합니다.

다만 값은 문자열 또는 `null`입니다.

const userB: UserB = {
  nickname: null,
};

다음 코드는 오류가 발생합니다.

const userB: UserB = {};

차이를 정리하면 다음과 같습니다.

nickname?: string
속성이 없을 수 있다.

nickname: string | null
속성은 반드시 있지만 값이 비어 있을 수 있다.

건물 주소록에 비유하면 다음과 같습니다.

선택적 속성
→ 이름 자체가 주소록에 없을 수 있음

null 속성
→ 이름은 주소록에 있지만 연락처 칸이 비어 있음

31. API 데이터에서 `null` 다루기

서버에서 받는 데이터에는 `null`이 포함될 수 있습니다.

type UserResponse = {
  id: number;
  name: string;
  profileImage: string | null;
};

프로필 이미지를 등록하지 않은 사용자는 `null`을 받을 수 있습니다.

const user: UserResponse = {
  id: 1,
  name: "김타입",
  profileImage: null,
};

화면에 표시할 이미지 주소를 결정해 보겠습니다.

const imageUrl =
  user.profileImage ??
  "/images/default-profile.png";

프로필 이미지가 있으면 해당 주소를 사용하고, 없으면 기본 이미지를 사용합니다.

console.log(imageUrl);

출력 결과:

/images/default-profile.png

API 문서를 확인할 때는 어떤 필드가 `null`이 될 수 있는지 반드시 확인해야 합니다.

타입에서 이를 빠뜨리면 TypeScript가 잘못된 확신을 갖게 됩니다.

32. JSON에서 `null`과 `undefined`

`null`과 `undefined`는 JSON으로 변환할 때도 다르게 처리됩니다.

다음 객체를 살펴보겠습니다.

const data = {
  name: "김타입",
  nickname: null,
  message: undefined,
};

`JSON.stringify()`를 사용해 문자열로 변환합니다.

console.log(JSON.stringify(data));

출력 결과:

{"name":"김타입","nickname":null}

`nickname`의 `null`은 JSON에 포함됩니다.

하지만 `message`의 `undefined`는 속성 자체가 생략됩니다.

null
→ 값이 없는 상태로 JSON에 기록됨

undefined
→ 객체 속성이 JSON에서 빠질 수 있음

서버와 데이터를 주고받을 때 이 차이는 중요할 수 있습니다.

예를 들어 다음 두 요청은 의미가 다를 수 있습니다.

nickname 속성을 보내지 않음
→ 기존 값을 그대로 유지

nickname: null을 보냄
→ 기존 닉네임을 삭제

API 설계에 따라 의미가 다르므로 문서를 확인해야 합니다.

33. 실무에서 `null`과 `undefined`를 선택하는 기준

프로젝트마다 규칙은 다를 수 있지만 다음 기준을 참고할 수 있습니다.

`undefined`를 사용하기 좋은 경우

  • 선택적 함수 매개변수
  • 선택적 객체 속성
  • 아직 값이 할당되지 않은 상태
  • JavaScript 기본 API의 결과를 그대로 사용할 때
  • 생략된 값이라는 의미를 표현할 때
type Options = {
  timeout?: number;
};

`null`을 사용하기 좋은 경우

  • 의도적으로 값을 비운 상태
  • 검색 결과가 없음을 명확히 표현할 때
  • UI에서 선택된 항목이 없을 때
  • API나 데이터베이스에서 빈 값을 표현할 때
  • 값이 있었지만 제거되었음을 표현할 때
let selectedItemId: number | null = null;

가장 중요한 원칙

`null`과 `undefined` 중 무엇을 선택하든 프로젝트 안에서 일관성을 유지하는 것이 중요합니다.

다음처럼 함수마다 제각각 사용하면 호출하는 사람이 혼란스러워집니다.

findUser();    // 실패하면 null
findProduct(); // 실패하면 undefined
findOrder();   // 실패하면 빈 문자열

가능하다면 팀 규칙을 정합니다.

선택적 속성은 undefined
검색 실패 결과는 null

또는 다음처럼 정할 수도 있습니다.

JavaScript 기본 동작에 맞춰 undefined로 통일

정답 하나가 있는 문제라기보다 일관성이 핵심인 설계 문제입니다.

34. 사용을 피해야 하는 위험한 방법

값이 반드시 있다고 TypeScript에게 강제로 주장하는 방법이 있습니다.

const userName: string | null = null;

console.log(userName!.toUpperCase());

`!`를 사용하면 TypeScript에게 다음과 같이 말하는 셈입니다.

“이 값은 절대 `null`이 아니니 나를 믿어.”

하지만 실제 값은 `null`입니다.

TypeScript의 경고는 사라지지만 실행 중 오류는 그대로 발생합니다.

Cannot read properties of null

`!`는 값을 만들어 주는 연산자가 아닙니다.

TypeScript의 경고음만 끄는 버튼입니다.

화재경보기를 끈다고 화재가 사라지지는 않습니다. 🔥

타입 단언과 `!` 연산자는 11편에서 자세히 다룰 예정입니다.

지금은 가능하면 조건문이나 `?.`, `??`를 사용하는 것이 안전합니다.

35. 실습 1: 사용자 검색 기능

사용자 타입을 정의합니다.

type User = {
  id: number;
  name: string;
  email: string | null;
};

사용자 목록을 준비합니다.

const users: User[] = [
  {
    id: 1,
    name: "김타입",
    email: "type@example.com",
  },
  {
    id: 2,
    name: "이컴파일",
    email: null,
  },
];

사용자를 찾는 함수를 만듭니다.

function findUser(
  userId: number
): User | null {
  const user = users.find(
    (item) => item.id === userId
  );

  return user ?? null;
}

사용자 정보를 출력하는 함수를 작성합니다.

function printUser(userId: number) {
  const user = findUser(userId);

  if (user === null) {
    console.log("사용자를 찾지 못했습니다.");
    return;
  }

  const email =
    user.email ?? "이메일 미등록";

  console.log(`이름: ${user.name}`);
  console.log(`이메일: ${email}`);
}

함수를 호출합니다.

printUser(1);
printUser(2);
printUser(10);

전체 코드입니다.

type User = {
  id: number;
  name: string;
  email: string | null;
};

const users: User[] = [
  {
    id: 1,
    name: "김타입",
    email: "type@example.com",
  },
  {
    id: 2,
    name: "이컴파일",
    email: null,
  },
];

function findUser(
  userId: number
): User | null {
  const user = users.find(
    (item) => item.id === userId
  );

  return user ?? null;
}

function printUser(userId: number) {
  const user = findUser(userId);

  if (user === null) {
    console.log("사용자를 찾지 못했습니다.");
    return;
  }

  const email =
    user.email ?? "이메일 미등록";

  console.log(`이름: ${user.name}`);
  console.log(`이메일: ${email}`);
}

printUser(1);
printUser(2);
printUser(10);

출력 결과:

이름: 김타입
이메일: type@example.com

이름: 이컴파일
이메일: 이메일 미등록

사용자를 찾지 못했습니다.

이 실습에서는 값이 없는 두 가지 상황을 구분했습니다.

사용자 자체가 없음
→ null 반환

사용자는 있지만 이메일이 없음
→ email 값이 null

36. 실습 2: 선택된 상품 관리하기

사용자가 아직 상품을 선택하지 않은 상태를 표현해 보겠습니다.

type Product = {
  id: number;
  name: string;
  price: number;
};

let selectedProduct: Product | null = null;

선택된 상품을 출력하는 함수를 만듭니다.

function printSelectedProduct() {
  if (selectedProduct === null) {
    console.log("선택된 상품이 없습니다.");
    return;
  }

  console.log(
    `선택 상품: ${selectedProduct.name}`
  );

  console.log(
    `가격: ${selectedProduct.price}원`
  );
}

처음에는 상품이 선택되지 않았습니다.

printSelectedProduct();

출력 결과:

선택된 상품이 없습니다.

상품을 선택합니다.

selectedProduct = {
  id: 1,
  name: "기계식 키보드",
  price: 120000,
};

다시 출력합니다.

printSelectedProduct();

출력 결과:

선택 상품: 기계식 키보드
가격: 120000원

선택을 취소할 수도 있습니다.

selectedProduct = null;

`null`은 단순한 오류가 아니라 UI의 정상적인 상태를 표현합니다.

상품 선택 전
상품 선택 완료
상품 선택 취소

37. 실습 3: 환경 설정 기본값 적용하기

프로그램 설정값은 사용자가 입력하지 않을 수도 있습니다.

type AppConfig = {
  theme?: string;
  fontSize?: number;
  showSidebar?: boolean;
};

설정 객체를 준비합니다.

const config: AppConfig = {
  fontSize: 0,
  showSidebar: false,
};

기본값을 적용해 보겠습니다.

const theme =
  config.theme ?? "light";

const fontSize =
  config.fontSize ?? 16;

const showSidebar =
  config.showSidebar ?? true;

결과를 출력합니다.

console.log(theme);
console.log(fontSize);
console.log(showSidebar);

출력 결과:

light
0
false

`fontSize`의 `0`과 `showSidebar`의 `false`가 그대로 유지되었습니다.

`||`를 사용했다면 결과가 달라졌을 수 있습니다.

const wrongFontSize =
  config.fontSize || 16;

const wrongShowSidebar =
  config.showSidebar || true;

결과:

16
true

`0`과 `false`가 유효한 설정인데 기본값으로 바뀌었습니다.

이처럼 `null`과 `undefined`만 처리하려면 `??`를 사용하는 것이 안전합니다.

38. 자주 발생하는 실수

실수 1. 값이 없을 가능성을 타입에서 제외하기

function findUserName(id: number): string {
  if (id === 1) {
    return "김타입";
  }

  return null;
}

반환 타입이 `string`인데 `null`을 반환하고 있습니다.

올바르게 표현합니다.

function findUserName(
  id: number
): string | null {
  if (id === 1) {
    return "김타입";
  }

  return null;
}

실수 2. 확인하지 않고 메서드 호출하기

const userName: string | null = null;

console.log(userName.toUpperCase());

값이 존재하는지 먼저 확인합니다.

if (userName !== null) {
  console.log(userName.toUpperCase());
}

실수 3. `if (value)`로 모든 값을 검사하기

const count: number | null = 0;

if (count) {
  console.log(count);
}

`0`은 유효한 값이지만 조건문이 실행되지 않습니다.

if (count !== null) {
  console.log(count);
}

실수 4. `||`로 기본값 지정하기

const count = 0;
const finalCount = count || 10;

결과는 `10`이 됩니다.

`0`을 유지하려면 `??`를 사용합니다.

const finalCount = count ?? 10;

실수 5. `!`로 무조건 경고 없애기

const button =
  document.querySelector("button");

button!.addEventListener(
  "click",
  () => {}
);

버튼이 실제로 없다면 실행 중 오류가 발생합니다.

button?.addEventListener(
  "click",
  () => {}
);

또는 명시적으로 확인합니다.

if (button !== null) {
  button.addEventListener(
    "click",
    () => {}
  );
}

실수 6. `null`과 문자열 `"null"`을 혼동하기

const firstValue = null;
const secondValue = "null";

두 값은 완전히 다릅니다.

console.log(typeof firstValue);
console.log(typeof secondValue);

출력 결과:

object
string

문자열 `"null"`은 글자 네 개로 이루어진 정상적인 문자열입니다.

39. `null`과 `undefined` 판단 공식

값이 없는 상태를 만났을 때 다음 질문을 순서대로 확인해 보세요.

질문 1. 값이 의도적으로 비어 있는가?

그렇다면 `null`을 고려할 수 있습니다.

let selectedItem: Item | null = null;

질문 2. 값이나 속성을 생략할 수 있는가?

그렇다면 선택적 속성이나 `undefined`를 고려합니다.

type Options = {
  timeout?: number;
};

질문 3. 값이 없을 때 기본값이 필요한가?

`??`를 사용할 수 있습니다.

const timeout =
  options.timeout ?? 3000;

질문 4. 중첩된 속성이 없을 수 있는가?

`?.`를 사용할 수 있습니다.

const city =
  user.address?.city;

질문 5. `0`, `false`, 빈 문자열이 유효한 값인가?

그렇다면 `||`보다 `??`를 사용하는 것이 안전합니다.

const count =
  savedCount ?? 0;

질문 6. TypeScript 경고를 `!`로 지우고 싶은가?

먼저 실제로 값이 존재하는지 확인할 방법을 찾아야 합니다.

if (value !== null) {
  // 안전하게 사용
}

40. 미니 퀴즈

문제 1

다음 변수의 값은 무엇일까요?

let message: string | undefined;

console.log(message);

정답

undefined

변수를 선언했지만 값을 할당하지 않았기 때문입니다.

문제 2

다음 코드의 결과는 무엇일까요?

console.log(null === undefined);

정답

false

`null`과 `undefined`는 서로 다른 값과 타입입니다.

문제 3

다음 코드의 결과는 무엇일까요?

console.log(typeof null);

정답

object

JavaScript의 오래된 설계 특성 때문에 이런 결과가 나타납니다.

하지만 `null`은 실제 객체가 아닙니다.

문제 4

다음 코드의 문제는 무엇일까요?

const userName: string | null = null;

console.log(userName.toUpperCase());

정답

`userName`이 `null`일 수 있는데 문자열 메서드를 바로 호출했습니다.

값을 확인해야 합니다.

if (userName !== null) {
  console.log(userName.toUpperCase());
}

문제 5

다음 코드의 출력 결과는 무엇일까요?

const count = 0;

console.log(count || 10);
console.log(count ?? 10);

정답

10
0

`||`는 `0`을 거짓으로 판단하고, `??`는 `0`을 정상적인 값으로 유지합니다.

문제 6

다음 타입에서 `nickname`의 의미는 무엇일까요?

type User = {
  nickname?: string;
};

정답

`nickname` 속성은 존재할 수도 있고 없을 수도 있습니다.

속성을 읽은 결과는 `string | undefined`로 취급됩니다.

문제 7

다음 코드의 빈칸을 채워 보세요.

let selectedUser: string ______ = null;

정답

let selectedUser: string | null = null;

문자열 또는 `null`을 저장할 수 있습니다.

문제 8

다음 코드에서 안전하게 닉네임을 출력하는 방법은 무엇일까요?

type User = {
  profile?: {
    nickname?: string;
  };
};

const user: User = {};

정답

const nickname =
  user.profile?.nickname ??
  "닉네임 없음";

console.log(nickname);

문제 9

다음 두 타입은 어떤 차이가 있을까요?

type A = {
  nickname?: string;
};

type B = {
  nickname: string | null;
};

정답

`A`는 `nickname` 속성 자체를 생략할 수 있습니다.

`B`는 `nickname` 속성이 반드시 존재하지만 값으로 `null`을 사용할 수 있습니다.

41. 핵심 정리

`null`

개발자가 의도적으로 값이 없음을 표현할 때 주로 사용합니다.

let selectedUser:
  string | null = null;

`undefined`

값이 아직 할당되지 않았거나 속성 또는 인수가 생략되었을 때 자주 나타납니다.

let message:
  string | undefined;

두 값은 서로 다르다

null === undefined;

결과:

false

값이 없을 가능성을 타입에 포함한다

string | null
number | undefined
User | null

`strictNullChecks`를 활성화한다

{
  "compilerOptions": {
    "strict": true
  }
}

`null`과 `undefined` 관련 오류를 개발 단계에서 발견할 수 있습니다.

값의 존재 여부를 확인한 뒤 사용한다

if (userName !== null) {
  console.log(userName.toUpperCase());
}

중첩 속성에는 `?.`

const nickname =
  user.profile?.nickname;

기본값에는 `??`

const nickname =
  user.nickname ?? "닉네임 없음";

`0`, `false`, 빈 문자열을 유지하려면 `||`보다 `??`

const count =
  savedCount ?? 0;

42. 마무리

프로그램에서 값이 없다는 상태는 실패가 아닙니다.

사용자가 아직 선택하지 않았거나, 데이터를 찾지 못했거나, 선택적인 정보가 생략된 정상적인 상태일 수 있습니다.

TypeScript는 이 상태를 타입에 포함시켜 개발자가 미리 대비할 수 있도록 도와줍니다.

let selectedUser:
  User | null = null;

이 타입은 단순히 변수가 비어 있다는 뜻만 전달하지 않습니다.

다음과 같은 프로그램의 상태를 설명합니다.

사용자가 선택되었을 수도 있다.
아직 아무도 선택되지 않았을 수도 있다.

`null`은 의도적으로 비워둔 값에 자주 사용합니다.

`undefined`는 값이 아직 정해지지 않았거나 생략된 상태에 자주 사용합니다.

null      → 비어 있다고 명시함
undefined → 값이 아직 정해지지 않음

그리고 값이 없을 가능성이 있다면 확인한 뒤 사용해야 합니다.

if (user !== null) {
  console.log(user.name);
}

또는 편리한 연산자를 사용할 수 있습니다.

const nickname =
  user.profile?.nickname ??
  "닉네임 없음";

TypeScript는 값을 대신 채워주지는 않습니다.

다만 상자가 비어 있을 가능성을 알려주고, 열기 전에 한 번 확인하라고 조용히 손전등을 비춰줍니다. 🔦

43. 다음 편 예고

지금까지 우리는 하나의 변수에 문자열, 숫자, 불리언 또는 값이 없는 상태를 저장하는 방법을 알아봤습니다.

const userName: string = "김타입";
const age: number = 25;
const isLearning: boolean = true;
const nickname: string | null = null;

그런데 학생 100명의 점수를 저장해야 한다면 변수를 100개 만들어야 할까요?

const score1 = 90;
const score2 = 85;
const score3 = 100;

상품 목록, 사용자 목록, 게시글 목록처럼 여러 개의 값을 관리하려면 더 편리한 방법이 필요합니다.

const scores = [90, 85, 100];

배열을 사용하면 여러 값을 하나의 변수에 모아 관리할 수 있습니다.

그렇다면 TypeScript에서는 배열의 타입을 어떻게 작성할까요?

const scores: number[] = [90, 85, 100];

숫자 배열에 문자열을 넣으면 어떻게 될까요?

scores.push("만점");

그리고 일반 배열과 비슷해 보이지만 값의 개수와 위치별 타입이 정해져 있는 튜플은 무엇일까요?

const userInfo: [string, number] = [
  "김타입",
  25,
];

다음 편에서는 여러 값을 한꺼번에 관리하는 배열과, 순서까지 엄격하게 관리하는 튜플을 살펴보겠습니다.

다음 이야기

[TypeScript 완전정복 #8] 여러 값을 한 번에 저장하려면? | 배열과 튜플 제대로 이해하기

  • 배열 타입은 어떻게 작성할까요?
  • `number[]`와 `Array<number>`는 무엇이 다를까요?
  • 배열에 잘못된 타입의 값을 넣으면 어떻게 될까요?
  • 배열의 값을 추가하고 삭제하려면 어떻게 해야 할까요?
  • `map`, `filter`, `find`는 어떻게 사용할까요?
  • 튜플은 일반 배열과 무엇이 다를까요?
  • 선택적 튜플과 읽기 전용 튜플은 무엇일까요?
  • 배열과 튜플은 언제 구분해서 사용해야 할까요?

다음 편에서는 여러 데이터를 한 줄로 세우는 배열과, 좌석 순서까지 지정하는 튜플의 세계로 들어가 보겠습니다.

댓글

0

댓글을 불러오는 중입니다.