2025-09-22 23:35

  • 자바스크립트 이터레이터는 데이터 컬렉션을 순회하기 위한 통일된 방법을 제공하는 ES6의 핵심 기능이다.

  • 이터러블 프로토콜([Symbol.iterator])과 이터레이터 프로토콜(next() 메서드)이라는 두 가지 규칙을 기반으로 동작한다.

  • 제너레이터(function*)를 사용하면 복잡한 이터레이터를 매우 간결하고 우아하게 만들 수 있으며, 이는 현대 자바스크립트의 중요한 패턴이다.


자바스크립트 이터레이터 완벽 정복 핸드북

자바스크립트 ES6가 등장하며 프로그래밍 패러다임에 많은 변화가 있었다. 그중에서도 이터레이션(iteration, 순회) 방식의 표준화를 가져온 **이터레이터(Iterator)**와 이터러블(Iterable) 개념은 코드의 일관성과 재사용성을 극적으로 높인 핵심 기능이다. 과거 for 루프, forEach, for...in 등 파편화되었던 순회 방식을 어떻게 하나로 통합하고, 더 나아가 비동기 처리와 무한한 데이터 스트림까지 다룰 수 있게 되었을까?

이 핸드북은 이터레이터의 탄생 배경부터 핵심 프로토콜, 커스텀 객체 구현, 그리고 강력한 동반자인 제너레이터(Generator)까지, 자바스크립트 이터레이션의 모든 것을 깊이 있게 탐험한다.


1. 탄생 배경 왜 이터레이터가 필요해졌을까

ES6 이전, 자바스크립트에서 데이터 구조를 순회하는 방법은 통일성이 부족했다.

  • 배열(Array): for 루프를 사용해 인덱스로 접근하거나 forEach 헬퍼 메서드를 사용했다.

  • 객체(Object): for...in 루프를 사용해 객체의 키(key)를 순회했다. 하지만 이는 프로토타입 체인까지 거슬러 올라가 원치 않는 속성을 순회할 위험이 있었다.

  • 유사 배열 객체(Array-like Objects): argumentsNodeList 같은 객체들은 배열이 아니므로 forEach 같은 배열 메서드를 직접 사용할 수 없었다. Array.prototype.slice.call(arguments)와 같은 번거로운 변환 과정이 필요했다.

이처럼 각 데이터 구조는 자신만의 순회 방식을 가지고 있었다. 이는 개발자가 데이터의 종류에 따라 다른 순회 코드를 작성해야 한다는 의미였고, 코드의 복잡성과 오류 가능성을 높였다.

이 문제를 해결하기 위해 ES6에서는 **“데이터 소비자와 생산자를 분리하자”**는 아이디어를 도입했다. 데이터의 구조(생산자)가 어떻든, 데이터를 사용하는 쪽(소비자)에서는 동일한 방식으로 순회할 수 있도록 표준화된 규칙을 만들었는데, 이것이 바로 **이터레이션 프로토콜(Iteration Protocol)**이다.


2. 핵심 개념 이터레이션 프로토콜 파헤치기

이터레이션 프로토콜은 두 가지 핵심 프로토콜로 구성된다. 마치 CD 플레이어로 음악을 듣는 과정과 같다.

  1. 이터러블 프로토콜 (The Iterable Protocol): “이 객체는 순회할 수 있습니다”라고 알리는 규칙.

  2. 이터레이터 프로토콜 (The Iterator Protocol): “다음 데이터를 주세요”라고 요청하고 결과를 받는 규칙.

이터러블 프로토콜 (Iterable Protocol)

어떤 객체가 [Symbol.iterator]라는 특별한 속성(Symbol)을 가지고 있고, 이 속성이 이터레이터를 반환하는 함수라면, 그 객체는 이터러블하다고 말한다.

  • [Symbol.iterator]: 객체가 순회 가능하다는 것을 나타내는 내장 심볼. 이터러블의 핵심이다.

  • 역할: 이터러블 객체는 for...of 루프, 전개 연산자(...), Array.from() 등과 함께 사용될 수 있음을 보장한다.

비유: 이터러블은 ‘음반(CD)‘과 같다. 음반에는 수많은 곡이 담겨 있으며, 중요한 것은 ‘재생(play)’ 버튼이 있다는 사실이다. [Symbol.iterator]가 바로 이 ‘재생’ 버튼 역할을 한다. 이 버튼을 누르면 실제 음악을 한 곡씩 재생해주는 ‘CD 플레이어’가 나온다.

내장 이터러블에는 Array, String, Map, Set, TypedArray, arguments 등이 있다.

JavaScript

const arr = [1, 2, 3];
const str = "hello";

// arr는 [Symbol.iterator] 속성을 가지고 있다.
console.log(typeof arr[Symbol.iterator]); // 'function'

// str도 [Symbol.iterator] 속성을 가지고 있다.
console.log(typeof str[Symbol.iterator]); // 'function'

이터레이터 프로토콜 (Iterator Protocol)

이터레이터는 순회를 실제로 수행하는 객체다. 이 객체는 반드시 next()라는 메서드를 가져야 한다.

  • next() 메서드: 호출될 때마다 데이터 컬렉션의 다음 요소를 가리킨다.

  • 반환 값: next() 메서드는 { value, done } 형태의 객체를 반환한다.

    • value: 현재 순회 요소의 값. donetrue일 때는 생략될 수 있다.

    • done: 순회가 끝났는지를 나타내는 boolean 값. false이면 순회가 진행 중, true이면 순회가 종료되었음을 의미한다.

비유: 이터레이터는 ‘CD 플레이어’다. next() 버튼을 누를 때마다 다음 곡(value)을 재생하고, 마지막 곡까지 재생이 끝나면 ‘재생 완료’(done: true) 신호를 보낸다.

for...of 루프의 내부 동작을 상상해보면 이터레이션 프로토콜을 쉽게 이해할 수 있다.

JavaScript

const numbers = [10, 20, 30];

// 1. for...of는 numbers 객체의 [Symbol.iterator]를 호출하여 이터레이터 객체를 얻는다.
const iterator = numbers[Symbol.iterator]();

// 2. for...of는 루프를 돌 때마다 이터레이터의 next() 메서드를 호출한다.
console.log(iterator.next()); // { value: 10, done: false }
console.log(iterator.next()); // { value: 20, done: false }
console.log(iterator.next()); // { value: 30, done: false }

// 3. done이 true가 될 때까지 반복한다.
console.log(iterator.next()); // { value: undefined, done: true }

for...of는 이 모든 과정을 자동으로 처리해주는 문법적 설탕(Syntactic Sugar)인 셈이다.


3. 커스텀 이터러블 객체 만들기

이제 프로토콜을 이해했으니, 직접 순회 가능한 객체를 만들어보자. 시작 값과 끝 값을 받아 그 사이의 숫자를 순회하는 Range 객체를 구현한다.

JavaScript

const range = {
  from: 1,
  to: 5,

  // 1. [Symbol.iterator] 메서드를 구현한다.
  [Symbol.iterator]() {
    // 2. 이 메서드는 이터레이터 객체를 반환해야 한다.
    return {
      current: this.from,
      last: this.to,

      // 3. 이터레이터 객체는 next() 메서드를 가져야 한다.
      next() {
        // 4. next()는 { value, done } 객체를 반환한다.
        if (this.current <= this.last) {
          return { done: false, value: this.current++ };
        } else {
          return { done: true };
        }
      }
    };
  }
};

// 이제 range 객체는 for...of 루프와 함께 동작한다.
for (const num of range) {
  console.log(num); // 1, 2, 3, 4, 5
}

// 전개 연산자도 사용할 수 있다.
console.log([...range]); // [1, 2, 3, 4, 5]

이 코드는 이터레이션 프로토콜의 모든 요소를 명확하게 보여준다. range 객체는 [Symbol.iterator]를 가졌으므로 이터러블하고, [Symbol.iterator]가 반환하는 객체는 next() 메서드를 가졌으므로 이터레이터다.


4. 심화 과정 제너레이터와 함께 날아오르기

위의 커스텀 이터레이터 코드는 다소 장황하다. current, last와 같은 상태를 이터레이터가 직접 관리해야 하기 때문이다. **제너레이터(Generator)**는 이러한 이터레이터를 훨씬 우아하고 간결하게 만들어주는 ES6의 강력한 기능이다.

제너레이터란 무엇인가?

제너레이터는 실행을 중간에 멈추고 재개할 수 있는 특별한 함수다.

  • 선언: function* 키워드를 사용해 선언한다.

  • yield: 제너레이터 함수의 실행을 일시 중지하고 값을 반환하는 키워드. return과 비슷하지만, 함수를 종료시키지 않는다. 다음에 next()가 호출되면 yield 다음 줄부터 실행을 재개한다.

비유: 제너레이터는 ‘책갈피(bookmark)‘와 같다. 책을 읽다가 yield라는 책갈피를 꽂아두고 잠시 다른 일을 할 수 있다. 다시 책을 읽을 준비가 되면(next() 호출), 책갈피를 꽂아둔 부분부터 즉시 이어서 읽을 수 있다.

제너레이터 함수를 호출하면 코드가 즉시 실행되지 않고, **제너레이터 객체(이터레이터)**가 반환된다. 이 제너레이터 객체는 이터레이터 프로토콜을 완벽하게 준수한다.

제너레이터를 사용한 이터러블 구현

앞서 만든 Range 객체를 제너레이터로 다시 작성해보자.

JavaScript

const rangeGenerator = {
  from: 1,
  to: 5,

  // [Symbol.iterator]가 제너레이터 함수가 되었다.
  *[Symbol.iterator]() {
    for(let value = this.from; value <= this.to; value++) {
      // yield를 만나면 실행을 멈추고 value를 밖으로 내보낸다.
      // 다음에 next()가 호출되면 루프의 다음 단계부터 실행된다.
      yield value;
    }
  }
};

for (const num of rangeGenerator) {
  console.log(num); // 1, 2, 3, 4, 5
}

코드가 놀랍도록 간결해졌다. next() 메서드, { value, done } 객체, 현재 상태(current)를 직접 관리할 필요가 없다. 제너레이터가 이 모든 복잡한 작업을 내부적으로 처리해주기 때문이다. yield 키워드가 next() 호출에 대한 { value, done: false }를 반환하고, 함수 실행이 끝나면 자동으로 { done: true }를 반환해준다.

yield*: 위임하기

yield* 표현식은 다른 제너레이터나 이터러블 객체에 순회를 ‘위임’하는 역할을 한다.1

JavaScript

function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) yield i;
}

function* generatePasswordCodes() {
  // 0~9
  yield* generateSequence(48, 57);
  // A~Z
  yield* generateSequence(65, 90);
  // a~z
  yield* generateSequence(97, 122);
}

let str = '';
for(let code of generatePasswordCodes()) {
  str += String.fromCharCode(code);
}

console.log(str); // 0123...XYZabc...xyz

yield*를 사용하면 여러 이터러블을 자연스럽게 하나로 연결하여 복잡한 순회 로직을 구성할 수 있다.


5. 실전 활용법: 이터레이터는 어디에 쓰일까?

이터레이션 프로토콜은 for...of 외에도 자바스크립트 전반에서 광범위하게 사용된다.

  • 전개 연산자 (Spread Syntax): ...는 이터러블의 모든 요소를 펼쳐준다.

    JavaScript

    const str = "hi";
    console.log([...str]); // ['h', 'i']
    
  • 배열 비구조화 할당 (Array Destructuring): 이터러블의 요소를 변수에 할당한다.

    JavaScript

    const [a, b] = new Set(['a', 'b', 'c']);
    console.log(a, b); // 'a', 'b'
    
  • Array.from(): 이터러블이나 유사 배열 객체로부터 새로운 배열을 생성한다.

    JavaScript

    const map = new Map([['a', 1], ['b', 2]]);
    console.log(Array.from(map)); // [['a', 1], ['b', 2]]
    
  • Map, Set 생성자: 이터러블을 인자로 받아 새로운 Map이나 Set을 초기화한다.

    JavaScript

    const set = new Set("hello"); // Set(4) { 'h', 'e', 'l', 'o' }
    
  • 지연 평가 (Lazy Evaluation): 제너레이터를 사용하면 무한한 시퀀스를 만들 수 있다. 값은 next()가 호출되는 시점에 계산되므로 메모리 낭비가 없다.

    JavaScript

    function* infiniteSequence() {
      let i = 0;
      while (true) {
        yield i++;
      }
    }
    
    const iterator = infiniteSequence();
    console.log(iterator.next().value); // 0
    console.log(iterator.next().value); // 1
    

6. 이터레이터 vs 기존 반복문

구분for 루프forEachfor...infor...of (이터레이터)
대상배열, 유사 배열배열객체의 열거 가능한 속성(key)이터러블 객체 (Array, String, Map, Set 등)
특징break, continue 사용 가능함수형, 중간에 멈출 수 없음프로토타입 체인까지 순회할 수 있음통일된 순회 방식 제공, break, continue 사용 가능
arr[i]로 직접 접근콜백 함수의 인자로 값 전달속성 이름(key)을 문자열로 반환이터러블의 각 요소 값(value)을 직접 반환
용도인덱스가 필요한 일반적인 배열 순회간단한 배열 순회객체의 속성 순회(권장하지 않음)데이터 구조에 상관없이 일관된 순회가 필요할 때

for...offor...in의 단점을 보완하고, forEach의 한계(중간 탈출 불가)를 극복하며, 다양한 데이터 구조를 동일한 문법으로 순회할 수 있는 가장 현대적이고 권장되는 방식이다.


7. 결론 이터레이션의 새로운 표준

자바스크립트 이터레이터와 제너레이터는 단순히 ‘반복문’의 새로운 형태가 아니다. 이는 데이터 구조와 해당 구조를 소비하는 로직을 분리하여, 코드의 재사용성과 조합성을 극대화하는 디자인 패턴이다. 이터레이션 프로토콜이라는 표준화된 약속 덕분에 우리는 어떤 종류의 데이터 컬렉션이든 for...of, 전개 연산자 등 일관된 방식으로 다룰 수 있게 되었다.

처음에는 [Symbol.iterator]yield 같은 개념이 낯설게 느껴질 수 있지만, 그 내부 동작을 이해하고 나면 자바스크립트를 더욱 깊이 있고 유연하게 다룰 수 있는 강력한 무기를 얻게 될 것이다. 이 핸드북이 그 여정의 든든한 가이드가 되기를 바란다.