null과 undefined 타입은 일반적으로 값이 비어있거나 없음을 나타냅니다. 안타깝게도, null 값 타입의 typeof 결과는 예상과 다릅니다. "null" 대신 다음과 같은 결과가 나옵니다.
이것이 null이 특별한 종류의 객체라는 의미는 아닙니다. 이것은 자바스크립트 초창기의 유산일 뿐이며, 세상에 존재하는 수많은 코드를 망가뜨릴 수 있기 때문에 변경될 수 없는 부분입니다.
undefined 타입은 명시적인 undefined 값뿐만 아니라, 값이 없는 것처럼 보이는 모든 곳에서 보고됩니다.
Note:
typeof nonExistent표현식은 선언되지 않은 변수nonExistent를 참조합니다. 일반적으로 선언되지 않은 변수를 참조하면 예외가 발생하지만,typeof연산자는 존재하지 않는 식별자에도 안전하게 접근하여 예외를 발생시키는 대신 침착하게"undefined"를 반환하는 특별한 능력이 있습니다.
그러나 각각의 “빈” 타입은 정확히 하나의 값, 즉 타입과 같은 이름의 값을 가집니다. 따라서 null은 null 값 타입의 유일한 값이며, undefined는 undefined 값 타입의 유일한 값입니다. 의미상으로, null과 undefined 타입은 모두 일반적인 비어있음, 또는 다른 긍정적이고 의미 있는 값의 부재를 나타냅니다.
Note:
null이나undefined를 만났을 때 동일하게 동작하는 JS 연산을 “null’ish” (또는 “nullish”)라고 합니다. 아마도 “undefined’ish”는 보기에도 발음하기에도 너무 이상했을 것입니다.
많은 JS 코드, 특히 개발자들이 작성하는 코드에서 이 두 개의 nullish 값은 서로 교환하여 사용할 수 있습니다. 특정 시나리오에서 의도적으로 null 또는 undefined를 사용할지 할당할지는 상황에 따라 다르며 개발자에게 달려있습니다.
따라서 이런 상황에서 코드를 안정적으로 수정하기 위해서 추가된 기능이 ?? (nullish 병합) 연산자입니다.
삼항 연산자로 표현된 등가식에서 볼 수 있듯이, ??는 myName이 nullish가 아닌지 확인하고, 그렇다면 그 값을 반환합니다. 그렇지 않다면 다른 피연산자(여기서는 "User")를 반환합니다.
??와 함께, JS는 ?. (nullish 조건부 체이닝, 옵셔널 체이닝) 연산자도 추가했습니다.
?. 연산자는 바로 앞(왼쪽)의 값을 확인하고, 만약 nullish라면 연산자는 멈추고 undefined 값을 반환합니다. 그렇지 않다면, 해당 값에 대해 . 속성 접근을 수행하고 표현식을 계속 진행합니다.
명확히 하자면, record?.는 “. 속성 접근 전에 record가 nullish인지 확인하라”는 의미입니다. 마찬가지로, billingAddress?.는 “. 속성 접근 전에 billingAddress가 nullish인지 확인하라”는 의미입니다.
Note: 일부 JS 개발자들은 새로운
?.가.보다 우월하므로 거의 항상.대신 사용해야 한다고 믿습니다. 저는 그것에 동의하지 않습니다. 첫째, 이는 추가적인 시각적 혼란을 더하며, 이로 인해 이점을 얻는 경우에만 사용해야 합니다. 둘째,?.를 사용하는 것을 정당화하려면 어떤 값의 비어있음을 인지하고 계획해야 합니다. 어떤 표현식에 항상 nullish가 아닌 값이 존재할 것으로 예상한다면, 해당 값의 속성에 접근하기 위해?.를 사용하는 것은 불필요하고 낭비일 뿐만 아니라, 값의 존재에 대한 당신의 가정이 틀렸을 때?.가 이를 덮어버려 잠재적인 미래의 버그를 숨길 수 있습니다. JS의 대부분 기능과 마찬가지로,.는 가장 적절한 곳에 사용하고,?.는 가장 적절한 곳에 사용하십시오. 하나가 더 적절할 때 다른 것으로 대체해서는 안 됩니다.
. 접근 대신 [ .. ] 스타일 접근이 필요할 때 사용하는, ?[가 아닌 다소 이상한 형태의 ?.[ 연산자도 있습니다.
Note: 연산자의 형태에 주의하세요. 이런 형태의 연산자를 시각적으로 구별하기 위해서는 좋은 폰트를 사용해야 합니다. 개발에 사용하고 있는 폰트를 점검하세요.
“옵셔널 호출(optional-call)”이라고 하는 또 다른 변형은 ?.(이며, 값이 nullish가 아닐 경우 조건부로 함수를 호출할 때 사용됩니다.
?.( 연산자는 someFunc(..)가 호출될 수 있는 유효한 함수인지 확인하는 것처럼 보입니다. 하지만 그렇지 않습니다. 이 연산자는 단지 값을 호출하기 전에 nullish가 아닌지 확인만 할 뿐입니다. 만약 값이 nullish가 아니면서 함수도 아닌 다른 타입이라면, 실행 시도는 여전히 TypeError 예외와 함께 실패할 것입니다.
Note: 바로 그 함정 때문에, 이 연산자 형태에 주의를 기울여야 합니다. 누구에게든 신중하게 사용하라고 경고합니다. 이것은 득보다 실(JS 자체와 프로그램에)이 많은, 스펙상 약간의 논쟁이 있는 기능이라고 생각합니다. 이렇게 까지 주의해야 할 기능이 몇개가 있는지 모르겠지만, 이것은 나쁜 부분(bad parts) 중 하나입니다.
null과 undefined는 실제로 별개의 타입이며, 따라서 null은 undefined와 눈에 띄게 다를 수 있다는 점을 명심하는 것이 중요합니다. 신중하게, 이 둘을 거의 구별할 수 없는 것으로 취급하는 프로그램을 구성할 수는 있습니다. 하지만 이는 개발자의 주의와 규율을 필요로 합니다. JS의 관점에서 보면, 이 둘은 대개 구별됩니다.
null과 undefined가 언어에 의해 다른 동작을 유발하는 경우가 있으며, 이를 염두에 두는 것이 중요합니다. 여기서 모든 경우를 전부 다루지는 않겠지만, 한 가지 예는 다음과 같습니다.
매개변수에 있는 = .. 절은 “매개변수 기본값(parameter default)”이라고 합니다. 이 기능은 해당 위치의 인자가 없거나, 정확히 undefined 값일 경우에만 작동하여 기본값을 매개변수에 할당합니다. 만약 null을 전달하면, 이 절은 작동하지 않으며 null이 매개변수에 할당됩니다.
프로그램에서 null이나 undefined를 사용하는 데 있어 옳고 그른 방법은 없습니다. 따라서 이 값들 중 하나를 선택할 때 신중해야 합니다. 그리고 만약 이들을 서로 교환하여 사용하고 있다면, 특히 더 주의해야 합니다.
!대신undefined를 막는 4가지 안전한 방법
export function simpleSort(arr: number[], order = "asc"): number[] {
const sortedArr = [...arr];
for (let i = 0; i < sortedArr.length - 1; i++) {
for (let j = 0; j < sortedArr.length - 1 - i; j++) {
const a = sortedArr[j] ?? 0; // undefined면 0으로 대체
const b = sortedArr[j + 1] ?? 0;
const swap = order === "asc" ? a > b : a < b;
if (swap) {
[sortedArr[j], sortedArr[j + 1]] = [b, a];
}
}
}
return sortedArr;
}export function simpleSort(arr: number[], order = "asc"): number[] {
const sortedArr = [...arr];
for (let i = 0; i < sortedArr.length - 1; i++) {
for (let j = 0; j < sortedArr.length - 1 - i; j++) {
const a = sortedArr[j];
const b = sortedArr[j + 1];
// undefined 체크
if (a === undefined || b === undefined) {
continue; // 또는 throw new Error("배열에 undefined 값이 있습니다")
}
const swap = order === "asc" ? a > b : a < b;
if (swap) {
[sortedArr[j], sortedArr[j + 1]] = [b, a];
}
}
}
return sortedArr;
}function isDefined<T>(value: T | undefined): value is T {
return value !== undefined;
}
export function simpleSort(arr: number[], order = "asc"): number[] {
const sortedArr = [...arr];
for (let i = 0; i < sortedArr.length - 1; i++) {
for (let j = 0; j < sortedArr.length - 1 - i; j++) {
const a = sortedArr[j];
const b = sortedArr[j + 1];
if (!isDefined(a) || !isDefined(b)) {
continue;
}
const swap = order === "asc" ? a > b : a < b;
if (swap) {
[sortedArr[j], sortedArr[j + 1]] = [b, a];
}
}
}
return sortedArr;
}export function simpleSort(arr: number[], order = "asc"): number[] {
const filteredArr = arr.filter((item): item is number => item !== undefined);
const sortedArr = [...filteredArr];
for (let i = 0; i < sortedArr.length - 1; i++) {
for (let j = 0; j < sortedArr.length - 1 - i; j++) {
const a = sortedArr[j]; // 이제 확실히 number 타입
const b = sortedArr[j + 1];
const swap = order === "asc" ? a > b : a < b;
if (swap) {
[sortedArr[j], sortedArr[j + 1]] = [b, a];
}
}
}
return sortedArr;
}p.prev나 p.next가 null일 수 있는데, null.next나 null.prev에 접근하려고 해서 에러 발생delete(value: T): void {
let p = this.head;
while (p !== null) {
if (p.value === value) {
if (this.length === 1) {
this.head = null;
this.tail = null;
}
else if (p === this.head) {
this.head = p.next;
if (this.head !== null) {
this.head.prev = null;
}
}
else if (p === this.tail) {
this.tail = p.prev;
if (this.tail !== null) {
this.tail.next = null;
}
}
else {
// null 체크 추가
if (p.prev !== null) {
p.prev.next = p.next;
}
if (p.next !== null) {
p.next.prev = p.prev;
}
}
this.length--;
return;
}
p = p.next;
}
}current.next가 Node<T> | null 타입이므로 할당 시 타입 에러 발생current가 Node<T>로 타입이 좁혀졌지만, current.next는 여전히 Node<T> | null 타입length++ 대신 this.length++ 사용해야 함i.value = value 대신 i.value === value 사용해야 함append(value: T): void {
const newNode = new Node(value);
if (!this.head) {
this.head = newNode;
this.tail = newNode;
} else {
if (this.tail) {
this.tail.next = newNode;
newNode.prev = this.tail;
this.tail = newNode;
}
}
this.length++; // this. 추가
}
delete(value: T): void {
if (!this.head) {
return;
}
let i: Node<T> | null = this.head;
while (i) {
if (i.value === value) { // === 사용
// 올바른 삭제 로직
if (i === this.head && i === this.tail) {
this.head = null;
this.tail = null;
} else if (i === this.head) {
this.head = i.next;
if (this.head) this.head.prev = null;
} else if (i === this.tail) {
this.tail = i.prev;
if (this.tail) this.tail.next = null;
} else {
if (i.prev) i.prev.next = i.next;
if (i.next) i.next.prev = i.prev;
}
this.length--;
return;
}
i = i.next;
}
}
search(value: T): T | null {
if (!this.head) return null;
let i: Node<T> | null = this.head;
while (i) {
if (i.value === value) return i.value; // === 사용
i = i.next;
}
return null;
}
printList(): T[] {
if (!this.head) return []; // this.tail 대신 this.head 체크
let result: T[] = [];
let i: Node<T> | null = this.head;
while (i) {
result.push(i.value);
i = i.next;
}
return result;
}if(this.head = null) 대신 if(this.head === null) 사용해야 함append(value: T): void {
const node = new ListNode(value);
if (this.head === null) { // === 사용
this.head = this.tail = node;
} else {
// 올바른 연결 순서
if (this.tail) {
this.tail.next = node; // 먼저 기존 tail의 next 설정
node.prev = this.tail; // 새 노드의 prev 설정
}
this.tail = node; // 마지막에 tail 업데이트
}
this.nodesize++;
}class LinkedList<T> {
private length: number = 0;
// 잘못된 방법
append(value: T): void {
const newNode = new Node(value);
// ... 노드 연결 로직 ...
length++; // 지역 변수 length를 찾으려고 하지만 정의되지 않음 → 에러!
}
// 올바른 방법
append(value: T): void {
const newNode = new Node(value);
// ... 노드 연결 로직 ...
this.length++; // 인스턴스 변수 length를 증가시킴
}
}class LinkedList<T> {
private length: number = 0;
// 잘못된 구현
append(value: T): void {
const newNode = new Node(value);
if (!this.head) {
this.head = this.tail = newNode;
} else {
this.tail!.next = newNode;
newNode.prev = this.tail;
this.tail = newNode;
}
length++; // ReferenceError 발생!
}
// 올바른 구현
append(value: T): void {
const newNode = new Node(value);
if (!this.head) {
this.head = this.tail = newNode;
} else {
this.tail!.next = newNode;
newNode.prev = this.tail;
this.tail = newNode;
}
this.length++; // 인스턴스 변수 증가
}
}this 키워드 범위length++: 지역 변수 length를 찾으려고 하지만 정의되지 않아서 에러 발생this.length++: 현재 인스턴스의 length 속성에 접근하여 값을 증가시킴
this는 “현재 객체의 속성에 접근하라”는 의미로, 클래스 기반 프로그래밍에서 인스턴스 변수와 지역 변수를 구분하는 핵심 키워드입니다.
=)과 비교(===) 연산자 구분&&, ||) 적절한 사용this. 키워드로 인스턴스 변수 접근Node, Array(List) 를 사용하여 구현 가능한지 확인 후 제출Node vs Index
T에 <, >를 직접 사용할 수 없습니다.
if (value < current.value) \(\to\) 타입 에러CompareFn 타입과 비교 함수를 추가하여 비교 연산을 수행하도록 변경node.right가 null이 아니어도, TypeScript는 current가 null일 수 있다고 판단node.right!로 타입 단언을 사용하거나 null이 아님을 명시해야 함TypeScript는 배열 인덱스 접근(array[index])을 안전하지 않은 연산으로 간주합니다.
undefined을 포함시키는가?배열 범위를 보장할 수 없음(런타임 에러 발생 가능)
TypeScript의 보수적 타입 추론
minIdx가 유효하다는 것을 개발자가 보장해야 함(런타임 에러 발생 가능)배열 인덱스 접근은 항상 undefined를 포함할 수 있는 타입으로 추론됩니다.
TypeScript는 안전성을 우선시합니다.
undefined를 포함한 타입으로 추론배열 인덱스 접근은 TypeScript가 범위를 보장할 수 없어 undefined를 포함한 타입으로 추론됩니다.
!)을 사용??)을 사용this.tree[j] < min 경고 이유// 20XX5XX52님
for (let i = 0; i < this.table[index].length; i++) {
if (this.table[index][i][0] === key) { // 에러
this.table[index][i][1] = value; // 에러
return;
}
}
for (let i = 0; i < this.table[index].length; i++) {
const current = this.table[index][i];
if (current && current[0] === key) {
current[1] = value;
return;
}
}인덱스 서명(Index Signature)은 TypeScript에서 객체 또는 배열의 속성을 동적으로 다룰 때, 그 속성의 키와 값 타입을 명시할 수 있는 타입스크립트 문법입니다. 객체나 배열의 키가 사전에 정확히 정해져 있지 않고, 동적으로 생성될 수 있을 때 사용합니다.
일반 객체 타입(예: {name: string; age: number;})은 사전에 정의된 속성만 가질 수 있습니다. 인덱스 서명이 있으면, 정의되지 않은 임의의 키에도 값을 할당하고 타입 검사를 받을 수 있습니다.
| 버전 | 변경 내용 |
|---|---|
| v.20250922 | 정렬 과제 코드 리뷰 |
| v.20250929 | 연결리스트 과제 코드 리뷰 |
| v.20251117 | 이진트리 및 해시테이블 과제 코드 리뷰 |