객체 순회 완전 정복 — for...in부터 Reflect.ownKeys까지
JavaScript 객체를 순회하는 6가지 방법(for...in, Object.keys/values/entries, for...of, Reflect.ownKeys)의 차이와 실무 선택 기준을 정리합니다.
지난 글에서 객체 복사의 다양한 방법을 살펴봤습니다. 복사만큼 자주 쓰이는 작업이 바로 **순회(iteration)**입니다. JavaScript에는 객체 프로퍼티를 열거하는 방법이 여러 가지 있고, 각각 프로토타입 체인 포함 여부, Symbol 키 처리 방식, 반환 타입이 제각각입니다. 잘못 고르면 의도치 않은 프로퍼티가 섞이거나 Symbol 키가 누락될 수 있습니다.
열거 가능(enumerable) 이란
JavaScript의 모든 프로퍼티는 프로퍼티 디스크립터를 가집니다. 그 중 enumerable 플래그가 false이면 for...in, Object.keys() 같은 열거 기반 API에서 무시됩니다.
const obj = {};
Object.defineProperty(obj, 'hidden', {
value: 42,
enumerable: false,
});
console.log(obj.hidden); // 42 (접근은 가능)
console.log(Object.keys(obj)); // [] (열거 불가)
for (const k in obj) {
console.log(k); // 출력 없음
}
배열 메서드(map, filter 등)나 JSON.stringify가 무시하는 프로퍼티도 같은 이유입니다. 반면 Reflect.ownKeys()는 enumerable 여부와 무관하게 자신의 모든 키를 반환합니다.
for…in — 프로토타입 체인까지 열거
for...in은 열거 가능한 프로퍼티를 프로토타입 체인을 포함해서 순회합니다. 오래된 코드에서 자주 보이지만 현대 실무에서는 사용을 자제하는 편이 좋습니다.
function Animal(name) { this.name = name; }
Animal.prototype.type = 'animal';
const dog = new Animal('바둑이');
for (const key in dog) {
console.log(key); // name, type (프로토타입 포함!)
}
// 자신 소유 프로퍼티만 걸러내려면
for (const key in dog) {
if (Object.hasOwn(dog, key)) {
console.log(key); // name 만 출력
}
}
for...in을 쓸 때는 반드시 Object.hasOwn() 가드를 달아야 합니다. 그렇지 않으면 서드파티 라이브러리가 Object.prototype을 건드린 경우 예상치 못한 키가 섞입니다.
Object.keys / values / entries
ES5에서 도입된 세 API는 자신의 소유(own) 열거 가능 프로퍼티만 다루며, 프로토타입 체인을 따라가지 않습니다.
const person = { name: 'Alice', age: 30 };
Object.keys(person); // ['name', 'age']
Object.values(person); // ['Alice', 30]
Object.entries(person); // [['name', 'Alice'], ['age', 30]]
셋 중 Object.entries()가 가장 유연합니다. 키와 값을 동시에 받아 구조 분해 할당과 궁합이 좋고, Map 생성자에도 바로 넘길 수 있습니다.
// 구조 분해 + for...of 조합
for (const [key, value] of Object.entries(person)) {
console.log(`${key}: ${value}`);
}
// 객체 → Map 변환
const map = new Map(Object.entries(person));
Reflect.ownKeys — Symbol까지 포함
Object.keys()는 문자열 키만 반환하고 Symbol 키를 무시합니다. Symbol 키까지 모두 가져오려면 Reflect.ownKeys()를 사용합니다.
const sym = Symbol('id');
const obj = { name: 'Bob', [sym]: 42 };
Object.keys(obj); // ['name']
Object.getOwnPropertyNames(obj); // ['name']
Reflect.ownKeys(obj); // ['name', Symbol(id)]
Reflect.ownKeys()는 열거 불가능한 프로퍼티도 반환합니다. 즉, 프로토타입 체인은 제외하고, Symbol 포함, enumerable 무관하게 자신의 모든 키를 반환합니다.
실무에서 어떤 걸 써야 할까
실무에서 객체를 순회하는 경우 90% 이상은 Object.entries() + for...of 조합이면 충분합니다.
| 상황 | 추천 |
|---|---|
| 키·값 쌍 모두 필요 | Object.entries() |
| 키만 필요 | Object.keys() |
| 값만 필요 | Object.values() |
| Symbol 키 포함 | Reflect.ownKeys() |
| 프로토타입 포함 (레거시 코드 분석) | for...in + hasOwn 가드 |
변환 파이프라인에 특히 유용한 패턴: 객체를 entries로 풀고, 배열 메서드로 변환한 뒤, Object.fromEntries()로 다시 조립하는 방식입니다.
const prices = { apple: 1200, banana: 800, cherry: 3000 };
// 값이 1000 이상인 것만 필터링 + 10% 인상
const result = Object.fromEntries(
Object.entries(prices)
.filter(([, v]) => v >= 1000)
.map(([k, v]) => [k, v * 1.1])
);
// { apple: 1320, cherry: 3300 }
for…of로 직접 순회하려면
일반 객체(plain object)는 이터러블이 아니므로 for...of로 직접 순회할 수 없습니다. Object.entries()나 Object.values()가 반환하는 배열을 거쳐야 합니다.
const data = { a: 1, b: 2 };
// 에러: data is not iterable
// for (const x of data) { ... }
// 올바른 방법
for (const [k, v] of Object.entries(data)) {
console.log(k, v);
}
직접 for...of를 지원하려면 객체에 Symbol.iterator를 구현해야 하는데, 이는 이터러블 프로토콜 글에서 자세히 다룹니다.
순서 보장
ES2015 이후 스펙에서 프로퍼티 순서는 다음 규칙을 따릅니다.
- 정수 인덱스(양의 정수 문자열): 오름차순 정렬
- 문자열 키: 삽입 순서
- Symbol 키: 삽입 순서
const obj = { b: 2, '10': 10, a: 1, '2': 2 };
Object.keys(obj); // ['2', '10', 'b', 'a']
// 정수 키(2, 10)가 먼저 오름차순, 나머지는 삽입 순서
이 순서는 V8, SpiderMonkey, JavaScriptCore 모두 동일하게 구현합니다.
지난 글: 객체 복사 완전 가이드 — 얕은 복사부터 structuredClone까지
다음 글: 클래스 문법 입문 — ES6 class 키워드 완전 이해
읽어주셔서 감사합니다. 😊