순회는 계약이다: 일반 객체에 Symbol.iterator를 붙여 이터러블로 만들기
한 문장 결론: 이터러블은 “순회 가능한 형태로 값을 꺼내는 계약”이다. 일반 객체에 Symbol.iterator를 구현하면 for...of와 전개 연산자가 일관된 규칙으로 동작한다.
순회는 계약이다: 일반 객체에 Symbol.iterator를 붙여 이터러블로 만들기
한 문장 결론: 이터러블은 "값을 순서대로 꺼내는 규칙"을 약속하는 인터페이스다. 일반 객체에 Symbol.iterator를 구현하면 for...of와 전개 연산자가 같은 규칙으로 동작한다.
배경/문제
일반 객체(Object)는 키-값 저장에 최적화되어 있지만, 기본적으로는 for...of로 순회할 수 없다. 그래서 다음 같은 상황에서 코드가 금방 복잡해진다.
- "순서"가 필요한 로직인데 매번
Object.keys()/Object.values()로 변환한다. - 순회 규칙이 호출부 곳곳에 흩어져서 유지보수 시 실수가 늘어난다.
정리하면, 순회 규칙을 객체 내부로 이동시키면 호출부는 언제나 같은 문법으로 읽힌다.
핵심 개념
Iterable과 Iterator
- 이터러블(Iterable):
Symbol.iterator메서드를 제공하는 객체. 이 메서드는 이터레이터를 반환한다. - 이터레이터(Iterator):
next()메서드를 가진 객체.next()는{ value, done }형태의 결과를 반환한다.
프로토콜(계약) 자체는 언어 차원에서 정의되어 있다. 정의/형식은 공식 문서를 기준으로 한다.
→ 기대 결과/무엇이 달라졌는지: 호출부는 for...of/전개 연산자 같은 "순회 문법"만 사용하고, 실제 순서·종료 조건은 객체가 책임집니다.
해결 접근
목표는 "일반 객체를 이터러블로 만들기"다. 방법은 크게 세 가지로 비교할 수 있다.
1) Symbol.iterator 직접 구현
- 왜: 순회 규칙(순서/필터/종료)을 정확히 제어하려고
- 기대 결과: 호출부가
for...of/전개 연산자로 통일된다
2) Object.values()로 배열 변환 후 순회
- 왜: 빠르게 동작만 만들려고
- 기대 결과: 구현은 단순하지만 변환 로직이 호출부에 남는다
3) 제너레이터(Generator)로 이터레이터 구현
- 왜:
next()/done처리 없이 "내보낼 값"에만 집중하려고 - 기대 결과: 로직이 길어질수록 읽기 쉬워진다
여기서는 1)로 프로토콜의 핵심을 잡고, 3) 대안까지 함께 정리한다.
구현(코드)
1) 최소 구현: 일반 객체에 Symbol.iterator 추가
아래 코드는 "1부터 4까지 순서대로 반환"하도록 이터러블을 만든다.
// 1) 일반 객체 생성 (기본적으로는 순회 불가능)
const obj = { 0: 1, 1: 2, 2: 3, 3: 4 };
// 2) 이터러블 프로토콜 구현: Symbol.iterator 메서드 추가
obj[Symbol.iterator] = function () {
let i = 1; // 클로저로 상태(현재 값) 유지
// 3) 이터레이터 프로토콜 구현: next 메서드를 가진 객체 반환
return {
next() {
return i > 4 ? { done: true } : { value: i++, done: false };
},
};
};
// 4) 순회 테스트
console.log("--- for...of ---");
for (const value of obj) console.log("value:", value);
console.log("--- spread ---");
console.log(...obj);→ 기대 결과/무엇이 달라졌는지: for...of는 Symbol.iterator()로 이터레이터를 얻고 next()를 반복 호출해 done: true가 될 때까지 값을 소비합니다. 전개 연산자도 같은 방식으로 값을 펼칩니다.
2) 실전형: 객체의 "값"을 기준으로 순회 규칙 고정하기
위 예제는 값을 직접 생성하지만, 일반적으로는 "객체에 들어있는 값"을 순회하는 편이 자연스럽다.
const obj = { 0: 1, 1: 2, 2: 3, 3: 4 };
obj[Symbol.iterator] = function () {
const values = Object.values(this);
let idx = 0;
return {
next() {
if (idx >= values.length) return { done: true };
return { value: values[idx++], done: false };
},
};
};
console.log([...obj]); // [1, 2, 3, 4]→ 기대 결과/무엇이 달라졌는지: 순회 결과가 객체 내부 값에 "고정"되어, 호출부는 변환 로직 없이 동일한 문법으로 재사용할 수 있습니다.
3) 대안: 제너레이터로 구현을 단순화
순회 규칙이 복잡해질수록 제너레이터가 가독성을 올린다.
const obj = { 0: 1, 1: 2, 2: 3, 3: 4 };
obj[Symbol.iterator] = function* () {
for (const v of Object.values(this)) yield v;
};
console.log([...obj]); // [1, 2, 3, 4]→ 기대 결과/무엇이 달라졌는지: next()/done 처리를 직접 하지 않아도 되고, "무엇을 어떤 순서로 내보내는지"만 남습니다.
검증 방법(체크리스트)
for...of로 순회가 된다....obj)가 같은 순서를 출력한다.done: true가 반환되어 반복이 끝난다.흔한 실수/FAQ
Q1. for...in과 뭐가 다른가요?
for...in은 주로 프로퍼티 키를 나열합니다. for...of는 이터러블의 값을 소비합니다. 선택 기준은 "키를 돌릴지, 값을 돌릴지"입니다.
Q2. 객체에 Symbol.iterator를 붙이면 항상 좋은가요?
팀 규칙/라이브러리 동작에 따라 "일반 객체는 순회 불가"라는 전제를 깨는 부작용이 있을 수 있습니다. 이 경우 래퍼 객체를 두거나, 특정 타입에만 제한하는 방식이 안전합니다.
Q3. Object.values() 순서는 언제나 안정적인가요?
프로퍼티 순서 규칙을 전제로 합니다. 순서가 중요한 데이터라면, 애초에 배열/Map 같은 "순서가 모델에 포함된 구조"를 고려하는 편이 안전합니다.
요약
- 이터러블은 "값을 꺼내는 계약"을 구현한 객체다.
Symbol.iterator는 계약의 진입점이고, 이터레이터의next()가 흐름을 만든다.- 순회 규칙을 객체에 캡슐화하면 호출부는
for...of/전개 연산자로 단순해진다. - 로직이 길어지면 제너레이터로 읽기 좋은 순회를 만들 수 있다.
결론
순회 로직이 여러 곳에 흩어질수록 실수도 같이 늘어난다. Symbol.iterator를 직접 구현하면 "어떻게 순회할지"를 객체의 책임으로 묶을 수 있고, 호출부는 문법만 남는다.