들어가며
JavaScript를 쓰다 보면 "비슷해 보이지만 동작이 전혀 다른" 패턴들을 만납니다. 이 글에서는 실무에서 자주 혼동하는 패턴들을 하나씩 비교하고, 각각 언제 써야 하는지 정리합니다.
Object.create(null) vs
빈 객체의 두 가지 의미
const dict = Object.create(null);
const obj = {};
"toString" in obj; // true, Object.prototype에서 상속
"toString" in dict; // false, 프로토타입 자체가 없음{}는 Object.prototype을 상속하므로, toString, hasOwnProperty, valueOf 같은 메서드가 포함되어 있습니다. 반면 Object.create(null)은 프로토타입이 null 인 객체를 만들어, 상속된 속성이 전혀 없습니다.
왜 프로토타입 없는 객체가 필요한가
// 문제: 키가 상속 메서드와 충돌
const cache = {};
cache["toString"] = "커스텀 값";
// cache.toString은 이제 문자열 "커스텀 값"
// String(cache)를 호출하면 TypeError 발생 가능
// 해결: 프로토타입 없는 순수 딕셔너리
const safeCache = Object.create(null);
safeCache["toString"] = "커스텀 값"; // 충돌 없음라이브러리에서 키-값 저장소를 구현할 때 Object.create(null)을 사용하는 이유입니다. Express.js의 내부 저장소, Vue의 반응형 객체 등에서 이 패턴을 볼 수 있습니다.
요즘은 Map 이 더 나은 대안인 경우가 많습니다. Map은 키 타입에 제한이 없고, size 속성으로 크기를 바로 알 수 있으며, 이터러블이기도 합니다.
structuredClone vs JSON.parse(JSON.stringify())
깊은 복사의 두 가지 방법
2022년에 주요 브라우저에서 지원되기 시작한 structuredClone()은 HTML 명세의 구조화된 복제 알고리즘 을 사용하는 Web API입니다 (ECMAScript가 아닌 HTML Living Standard에 정의). 그전까지 깊은 복사의 관용적 방법이었던 JSON.parse(JSON.stringify())와 비교해보겠습니다.
const source = {
date: new Date("2026-01-01"),
regex: /abc/g,
map: new Map([["key", "value"]]),
undef: undefined,
};
source.self = source; // 순환 참조
// structuredClone, 모든 타입을 올바르게 복제
const a = structuredClone(source);
a.date instanceof Date; // true
a.self === a; // true (순환 참조도 처리)
// JSON 왕복, 타입 정보 손실
const b = JSON.parse(JSON.stringify(source));
// TypeError: Converting circular structure to JSON타입별 비교
| 타입 | structuredClone | JSON 왕복 |
|---|---|---|
| Date | Date 객체 유지 | 문자열로 변환 |
| RegExp | RegExp 유지 (lastIndex 제외) | 빈 객체 {} |
| Map / Set | Map / Set 유지 | 빈 객체 {} |
| undefined | undefined 유지 | 속성 제거됨 |
| 순환 참조 | 정상 복제 | TypeError 발생 |
| 함수 | DataCloneError 발생 | 속성 제거됨 |
| Symbol 속성 | 속성 제거됨 | 속성 제거됨 |
| 프로토타입 | 복제되지 않음 | 복제되지 않음 |
언제 JSON 방식을 쓰나? 데이터가 JSON-safe (문자열, 숫자, boolean, 배열, 일반 객체만 포함) 하다면 JSON 방식이 더 빠를 수 있습니다. 하지만 타입 안전성이 필요하면 structuredClone()을 사용하세요.
for...in vs for...of
이 둘은 이름이 비슷하지만 완전히 다른 것 을 반복합니다.
for...in: 열거 가능한 문자열 속성
Object.prototype.inherited = "yes";
const arr = [10, 20, 30];
arr.custom = "hello";
for (const key in arr) {
console.log(key);
}
// "0", "1", "2", "custom", "inherited"for...in은 객체의 열거 가능한 문자열 속성을 반복합니다. 프로토타입 체인을 따라 상속된 속성까지 포함합니다. Symbol 속성은 포함하지 않습니다. 순서는 정수 키가 먼저 (오름차순), 그다음 문자열 키가 생성 순서대로입니다.
for...of: 이터러블의 값
const arr = [10, 20, 30];
arr.custom = "hello";
for (const val of arr) {
console.log(val);
}
// 10, 20, 30 (custom은 나타나지 않음)for...of는 이터러블 프로토콜([Symbol.iterator])을 구현한 객체의 값 을 반복합니다. Array, String, Map, Set, NodeList 등이 이터러블입니다. 일반 객체 {}는 이터러블이 아니므로 for...of를 사용할 수 없습니다.
| for...in | for...of | |
|---|---|---|
| 반복 대상 | 열거 가능한 문자열 속성 (키) | 이터러블의 값 |
| 상속 포함 | 포함 | 해당 없음 |
| 일반 객체 | 사용 가능 | TypeError |
| 배열에 사용 | 가능하지만 권장하지 않음 | 권장 |
| Symbol 키 | 제외 | 해당 없음 |
const dict = Object.create(null);
dict.key = "value";
const obj = {};
obj.key = "value";Array.from vs 스프레드
유사 배열 처리
function example() {
// arguments는 유사 배열 (이터러블이기도 함)
const a = Array.from(arguments); // OK
const b = [...arguments]; // OK
// NodeList도 이터러블
const nodes = document.querySelectorAll("div");
const c = Array.from(nodes); // OK
const d = [...nodes]; // OK
}두 방법 모두 이터러블을 배열로 변환합니다. 차이점은 매핑 입니다.
// Array.from의 두 번째 인자: 맵 함수
const doubled = Array.from([1, 2, 3], (x) => x * 2);
// [2, 4, 6], 중간 배열 없이 바로 매핑
// 스프레드 + map: 중간 배열 생성
const doubled2 = [...[1, 2, 3]].map((x) => x * 2);
// [2, 4, 6], 배열이 두 번 생성됨또한 Array.from은 이터러블이 아닌 유사 배열 (length 속성만 있는 객체)도 처리할 수 있습니다.
const arrayLike = { 0: "a", 1: "b", length: 2 };
Array.from(arrayLike); // ["a", "b"]
[...arrayLike]; // TypeError: arrayLike is not iterable| Array.from | 스프레드 [...] | |
|---|---|---|
| 이터러블 | 변환 가능 | 변환 가능 |
| 유사 배열 | 변환 가능 | TypeError |
| 매핑 | 두 번째 인자로 가능 | .map() 체이닝 필요 |
== null vs === null vs === undefined
null 체크 패턴
JavaScript에서 "값이 없음"을 나타내는 두 가지 값이 있습니다: null과 undefined. 이 둘을 체크하는 패턴이 자주 혼동됩니다.
// == null은 null과 undefined 둘 다 잡음
value == null;
// 위는 아래와 동일:
value === null || value === undefined;
// === null은 null만 잡음
value === null; // undefined는 false
// === undefined는 undefined만 잡음
value === undefined; // null은 false== null은 == 연산자의 유일하게 유용한 사용처 로 여겨집니다. null과 undefined를 동시에 체크하는 관용 표현입니다.
function greet(name) {
// name이 null이거나 undefined면 기본값 사용
if (name == null) {
name = "익명";
}
return `안녕하세요, ${name}`;
}
greet(null); // "안녕하세요, 익명"
greet(undefined); // "안녕하세요, 익명"
greet(""); // "안녕하세요, " (빈 문자열은 통과)
greet(0); // "안녕하세요, 0" (0도 통과)참고로 ES2020의 nullish coalescing (??) 연산자도 같은 동작입니다.
const name = input ?? "익명";
// input이 null 또는 undefined일 때만 "익명" 사용
// "" 이나 0은 그대로 유지 (|| 연산자와의 차이)| 패턴 | null | undefined | 0 | "" | false |
|---|---|---|---|---|---|
== null | true | true | false | false | false |
=== null | true | false | false | false | false |
=== undefined | false | true | false | false | false |
!value | true | true | true | true | true |
시리즈 마무리
JavaScript Deep Dive 시리즈를 통해 살펴본 주제들을 정리합니다.
- Map과 Set , 키 타입의 자유, 고유값 컬렉션, ES2025 Set 메서드
- WeakMap과 WeakSet , 약한 참조와 메모리 관리, private data 패턴
- 이터레이터와 제너레이터 , Symbol.iterator 프로토콜, 지연 평가, 무한 시퀀스
- Proxy와 Reflect , 메타프로그래밍, 트랩, 반응형 시스템
- 자주 혼동하는 패턴들 , 비슷해 보이지만 다른 것들의 정확한 차이
JavaScript는 유연한 만큼 혼동하기 쉬운 언어입니다. "대충 동작하는 코드"와 "정확히 이해하고 쓰는 코드" 사이의 차이가 이 시리즈가 전하고 싶은 메시지입니다.