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