Ray Book
JavaScript Deep Dive

자주 혼동하는 패턴들

Object.create(null) vs {}, structuredClone vs JSON, for...in vs for...of, 비슷해 보이지만 전혀 다른 패턴들을 시각적으로 비교합니다

javascriptpatternscomparisondeep-dive

들어가며

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

타입별 비교

타입structuredCloneJSON 왕복
DateDate 객체 유지문자열로 변환
RegExpRegExp 유지 (lastIndex 제외)빈 객체 {}
Map / SetMap / Set 유지빈 객체 {}
undefinedundefined 유지속성 제거됨
순환 참조정상 복제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...infor...of
반복 대상열거 가능한 문자열 속성 (키)이터러블의 값
상속 포함포함해당 없음
일반 객체사용 가능TypeError
배열에 사용가능하지만 권장하지 않음권장
Symbol 키제외해당 없음
const dict = Object.create(null);
dict.key = "value";

const obj = {};
obj.key = "value";
Object.create(null)
dict
key"value"
[[Prototype]]: null
toString없음
hasOwnProperty없음
valueOf없음
프로토타입 오염 없음순수 딕셔너리
{}
obj
key"value"
[[Prototype]]: Object.prototype
toString[Function]
hasOwnProperty[Function]
valueOf[Function]
상속 메서드 포함키 충돌 가능

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에서 "값이 없음"을 나타내는 두 가지 값이 있습니다: nullundefined. 이 둘을 체크하는 패턴이 자주 혼동됩니다.

// == null은 null과 undefined 둘 다 잡음
value == null;
// 위는 아래와 동일:
value === null || value === undefined;

// === null은 null만 잡음
value === null;   // undefined는 false

// === undefined는 undefined만 잡음
value === undefined; // null은 false

== null== 연산자의 유일하게 유용한 사용처로 여겨집니다. nullundefined를 동시에 체크하는 관용 표현입니다.

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은 그대로 유지 (|| 연산자와의 차이)
패턴nullundefined0""false
== nulltruetruefalsefalsefalse
=== nulltruefalsefalsefalsefalse
=== undefinedfalsetruefalsefalsefalse
!valuetruetruetruetruetrue

시리즈 마무리

JavaScript Deep Dive 시리즈를 통해 살펴본 주제들을 정리합니다.

  1. Map과 Set, 키 타입의 자유, 고유값 컬렉션, ES2025 Set 메서드
  2. WeakMap과 WeakSet, 약한 참조와 메모리 관리, private data 패턴
  3. 이터레이터와 제너레이터, Symbol.iterator 프로토콜, 지연 평가, 무한 시퀀스
  4. Proxy와 Reflect, 메타프로그래밍, 트랩, 반응형 시스템
  5. 자주 혼동하는 패턴들, 비슷해 보이지만 다른 것들의 정확한 차이

JavaScript는 유연한 만큼 혼동하기 쉬운 언어입니다. "대충 동작하는 코드"와 "정확히 이해하고 쓰는 코드" 사이의 차이가 이 시리즈가 전하고 싶은 메시지입니다.