카테고리 없음

JS - 이터레이터(Iterator), 제너레이터(Generator)

lamarcK 2025. 3. 28. 03:00

이터러블 객체(Iterable Objects)

이터러블(Iterable)이란, 배열(Array), 문자열(String), Map, Set 등과 같이 for...of 루프를 사용할 수 있는 값들을 의미한다.

배열, Map, Set은 참조 타입(객체)이지만, 문자열은 원시 타입이므로 이터러블 객체의 모든 데이터 타입이 객체인 것은 아니다.

따라서 이터러블 객체는 힙(Heap) 또는 스택(Stack)에 저장될 수 있으며, 원시값인 문자열(string)은 일반적으로 스택에 저장된다.

다만 문자열도 반드시 스택에 저장되는 것은 아니고 길거나 동적으로 생성되는 문자열의 경우 힙에 저장될 가능성이 있다.

이터러블 객체란 정확히 말해 Symbol.iterator 메서드를 구현하여 이터레이터를 생성할 수 있는 값 또는 객체를 의미한다.

 

자바 스크립트의 문자열이 힙에 저장될 가능성

https://lamarck009.tistory.com/102

이터레이터 (Iterator)의 개념

이터레이터(Iterator)는 프로그래밍에서 데이터 컬렉션(Data Collection)의 요소들을 하나씩 순차적으로 접근(Sequential Access)할 수 있도록 해주는 객체(Object) 또는 개념적인 인터페이스(Conceptual Interface)를 의미한다.

 

✨ 데이터 컬렉션 (Data Collection)

더보기

📌 정의

데이터 컬렉션은 여러 개의 데이터 항목(Data Items) 또는 요소(Elements)를 담고 있는 구조(Structure)나 컨테이너(Container)를 일반적으로 지칭하는 용어이다. 데이터를 효율적으로 저장, 관리, 접근하기 위한 목적을 가진다.

 

📌 목적

  • 그룹화: 관련 있는 여러 데이터를 하나의 단위로 묶어 관리한다.
  • 저장 및 검색: 데이터를 체계적으로 저장하고 필요할 때 쉽게 찾거나 접근할 수 있게 한다.
  • 관리: 데이터의 추가, 삭제, 수정 등의 작업을 용이하게 한다.

📌 예시

프로그래밍 언어에서 흔히 볼 수 있는 다양한 자료 구조들이 데이터 컬렉션에 해당한다.

  • 배열 (Array) / 리스트 (List): 순서가 있는 요소들의 모음 (예: 자바스크립트의 Array, 파이썬의 list).
  • 맵 (Map) / 딕셔너리 (Dictionary) / 해시 테이블 (Hash Table): 키(Key)와 값(Value)의 쌍으로 이루어진 요소들의 모음 (예: 자바스크립트의 Map, Object, 파이썬의 dict).
  • 셋 (Set): 중복되지 않는 고유한 요소들의 순서 없는 모음 (예: 자바스크립트의 Set, 파이썬의 set).
  • 튜플 (Tuple): 순서가 있으며 변경 불가능한(Immutable) 요소들의 모음 (예: 파이썬의 tuple).
  • 문자열 (String): 문자의 시퀀스로, 문자의 컬렉션으로 볼 수도 있다.
  • 사용자 정의 객체/클래스: 여러 데이터를 멤버 변수로 가지는 객체도 넓은 의미에서 데이터 컬렉션으로 간주될 수 있다.

핵심: '데이터 컬렉션'은 특정 자료 구조 하나를 지칭하기보다는, 여러 데이터를 담는 다양한 종류의 구조를 포괄하는 일반적인 용어이다. 이터레이터는 이러한 다양한 데이터 컬렉션들을 일관된 방식으로 순회할 수 있게 해준다.

✨ 개념적인 인터페이스 (Conceptual Interface)

더보기

📌 정의

개념적인 인터페이스는 특정 기능이나 동작을 수행하기 위해 기대되는 상호작용 방식이나 규약을 의미한다. 이는 특정 프로그래밍 언어의 interface 키워드처럼 엄격한 문법적 정의라기보다는, 어떤 역할을 수행하기 위해 필요한 기능들의 집합에 대한 개념적 설명에 가깝다.

📌 이터레이터에서의 의미

이터레이터를 "개념적인 인터페이스"라고 표현한 것은, 이터레이터의 핵심 역할이 "다음 요소 가져오기""순회가 끝났는지 확인하기" 라는 개념적인 기능을 제공하는 데 있기 때문이다.

  • 기대되는 동작: 이터레이터라면 next() 와 같은 연산을 통해 다음 값을 제공하고, 더 이상 값이 없을 때 이를 알려주는 동작을 할 것이라고 기대한다.
  • 구현의 다양성: 이 개념적인 요구사항을 만족시키는 구체적인 방법(즉, 실제 인터페이스 구현)은 언어나 상황에 따라 다르다.
    • 자바스크립트에서는 next() 메서드가 { value: ..., done: ... } 객체를 반환하는 이터레이터 프로토콜로 구체화된다.
    • 파이썬에서는 __next__() 메서드가 값을 반환하고, 끝에 도달하면 StopIteration 예외를 발생시키는 이터레이터 프로토콜로 구체화된다.
    • 다른 언어에서는 또 다른 방식(예: hasNext(), getNext() 메서드 쌍)으로 이 개념을 구현할 수 있다.

📌 비유

자동차의 운전 방식을 생각해보자. 어떤 종류의 자동차든 (세단, 트럭, SUV 등) 운전자는 핸들로 조향하고, 가속 페달로 속도를 내고, 브레이크 페달로 멈출 것이라고 기대(Concept)한다. 이것이 자동차 운전의 개념적인 인터페이스이다. 실제 내부 엔진 구조나 변속기 방식(구현)은 달라도, 운전자와 상호작용하는 기본적인 방식(인터페이스)은 유사하다.

핵심: '개념적인 인터페이스'는 "무엇을 할 수 있어야 하는가" 에 대한 추상적인 설명이며, 실제 코드 수준의 인터페이스(Interface) 정의와는 다를 수 있다. 이터레이터의 경우, 순차적 접근이라는 핵심 개념을 제공하는 역할을 강조하기 위해 이 용어를 사용했다.

✨ 핵심 특징

  1. 📌 순차적 접근 (Sequential Access)
    • 이터레이터는 데이터 집합의 요소들을 정해진 순서에 따라 차례대로 탐색한다. 한 번에 하나의 요소에만 접근할 수 있다.
  2. 📌 상태 유지 (Stateful)
    • 이터레이터는 현재 컬렉션의 어느 위치를 가리키고 있는지 내부 상태(Internal State)를 기억한다. next()와 같은 연산을 통해 다음 요소로 이동하면, 이 상태가 갱신된다.
  3. 📌 표준 인터페이스 (Standard Interface)
    • 데이터 구조(예: 배열, 리스트, 맵, 셋 등)의 내부 구현 방식과 상관없이, 요소에 접근하는 통일된 방법(Uniform Method)을 제공한다. 일반적으로 next()와 같은 메서드를 통해 다음 요소를 가져온다.
  4. 📌 종료 신호 (Termination Signal)
    • 컬렉션의 모든 요소를 다 순회했을 때, 더 이상 가져올 요소가 없음을 알리는 신호(Signal)를 보낸다. (예: 자바스크립트의 next() 반환 객체에서 done: true, 파이썬의 StopIteration 예외 발생).

✨ 핵심 목적

이터레이터의 핵심 목적은 다양한 데이터 구조에 대해 일관되고 표준화된 방법으로 요소들을 순회할 수 있는 메커니즘을 제공하는 것이다. 이는 코드의 일반성, 재사용성, 효율성을 높이고, for...of와 같은 강력한 언어 기능을 가능하게 한다.

 

이터레이터 사용법 예시

✨ 1. 암시적 사용: 반복문 활용 (권장 방식)

대부분의 경우, 언어에서 제공하는 반복문(Loop)을 사용하는 것이 가장 간편하고 일반적인 방법이다. 반복문은 내부적으로 이터레이터 획득 및 값 소진 과정을 자동으로 처리해준다.

📌 자바스크립트 (JavaScript) - for...of

ES6(ECMAScript 2015)부터 도입된 for...of 구문은 이터러블 객체에 대해 [Symbol.iterator]()를 호출하여 이터레이터를 얻고, 반환된 객체의 done 속성이 true가 될 때까지 next()를 호출하여 value 속성을 변수에 할당한다.

const mySet = new Set(['red', 'green', 'blue']);

// for...of 루프가 내부적으로 mySet[Symbol.iterator]() 호출 -> 이터레이터 획득
// 이후 next()를 반복 호출하여 value를 color에 할당
for (const color of mySet) {
  console.log(color);
}
// 출력 (순서는 Set 구현에 따라 다를 수 있음):
// red
// green
// blue
  • 목적: 배열, Set, Map, 문자열 등 다양한 이터러블 객체를 일관된 방식으로 순회한다.
  • 장점: for...in (객체의 속성 키를 순회)과 달리 값 자체를 순회하며, Symbol.iterator, next(), done 처리가 자동화된다.

✨ 2. 명시적 사용: 수동 반복

때로는 이터레이션 과정을 더 세밀하게 제어해야 하거나, 반복문 외의 컨텍스트에서 이터레이터를 사용해야 할 수 있다. 이때는 이터레이터를 직접 얻고 next() 메서드를 호출한다.

📌 자바스크립트 (JavaScript) - [Symbol.iterator]()와 next()

이터러블 객체의 [Symbol.iterator]() 메서드를 호출하여 이터레이터를 얻는다. 이후 이터레이터의 next() 메서드를 호출하면 { value: ..., done: ... } 형태의 객체가 반환된다. done 속성이 true가 될 때까지 반복한다.

const myString = "Hi";
const iterator = myString[Symbol.iterator](); // 이터레이터 획득

let result = iterator.next(); // 다음 값 가져오기

while (!result.done) { // done이 false인 동안 반복
  console.log(`값: ${result.value}`);
  // ... 필요한 로직 수행 ...
  result = iterator.next(); // 다음 값 가져오기
}

console.log("이터레이션 종료됨.");

// 출력:
// 값: H
// 값: i
// 이터레이션 종료됨.
  • 목적: 파이썬의 명시적 사용과 유사하게, 이터레이션 단계별 제어가 필요할 때 사용한다. while 루프와 done 플래그 확인을 통해 종료 조건을 처리한다.
  • 주의: next() 호출과 done 플래그 확인 로직을 정확하게 구현해야 한다.

[Symbol.iterator] 만으로는 새 배열이 만들어지는게 아니다.

[Symbol.iterator]()의 역할

  • [Symbol.iterator]() 메서드를 호출하는 것 자체만으로는 새로운 배열(Array)이 만들어지지 않는다.
  • [Symbol.iterator]() 메서드는 해당 객체(예: 배열, 문자열, Set 등)의 요소들을 순차적으로 탐색할 수 있는 이터레이터 객체(Iterator Object)를 반환할 뿐이다.
  • 이 이터레이터 객체는 원본 데이터 컬렉션의 요소들에 접근하는 방법(How)을 알고 있는 포인터커서와 같은 역할을 한다. 원본 데이터는 이미 존재하고 있다.

iterator.next()의 역할과 배열 생성

  • iterator.next()를 호출하는 것은 이터레이터에게 "다음 요소를 내놓으라"고 요청하는 행위이다. 이 메서드는 { value: ..., done: ... } 형태의 객체를 반환하여 다음 (value)과 순회의 종료 여부(done)를 알려준다.
  • next()를 반복 호출하는 것은 원본 데이터 컬렉션의 요소들을 하나씩 순차적으로 접근하거나 읽어오는 과정이지, 그 과정 자체가 새로운 배열을 만드는 것은 아니다. for...of 루프도 내부적으로 이 과정을 수행하며 각 value를 가져와 루프 본문에서 사용할 뿐, 루프 자체가 새 배열을 만들지는 않는다.
  • 별도의 배열을 만들고 싶다면, 이터레이터나 이터러블 객체를 사용하여 명시적으로 배열을 생성하는 코드를 작성해야 한다. 이때 내부적으로는 이터레이터의 next()가 모든 요소가 소진될 때까지 호출된다.

✨ 3. 기타 언어 구조 활용

이터레이터는 반복문 외에도 다양한 언어 구조와 함께 사용될 수 있다.

📌 자바스크립트 (JavaScript)

  • 전개 구문 (Spread Syntax): [...iterable], {...iterable} (객체는 주의 필요), func(...iterable) 등은 이터러블의 요소를 확장하여 배열 리터럴, 함수 인자 등으로 사용한다.
  • Array.from(iterable): 이터러블 객체로부터 새로운 배열을 생성한다.
  • 비구조화 할당 (Destructuring Assignment): const [a, b] = iterable 과 같이 이터러블의 요소를 변수에 할당한다.
  • yield*: 제너레이터 내에서 다른 이터러블/제너레이터에게 반복을 위임한다.
  • Promise.all(iterable), Promise.race(iterable) 등: 프로미스 이터러블과 함께 사용된다.

💡 주요 고려사항

  • 상태 유지 및 소진: 이터레이터는 현재 위치를 기억하는 상태(State)를 가진다. 한 번 순회가 끝나 소진(Exhausted)된 이터레이터는 일반적으로 다시 사용할 수 없다. 다시 순회하려면 원본 이터러블 객체로부터 새로운 이터레이터를 얻어야 한다.
    • 예외: 일부 사용자 정의 이터레이터는 reset과 같은 메서드를 구현하여 재사용 가능하게 만들 수도 있지만, 이는 표준적인 동작은 아니다.
  • 무한 이터레이터: 제너레이터 등을 이용해 무한히 값을 생성하는 이터레이터를 만들 수 있다. 이러한 이터레이터를 사용할 때는 for 루프나 전개 구문 등 종료를 기대하는 구문 내에서 반드시 탈출 조건(Break condition)을 명시해야 무한 루프에 빠지지 않는다.

요약하면, 대부분의 경우 반복문(for/for...of)을 사용하는 것이 가장 편리하며, 이터레이션 과정에 대한 세밀한 제어가 필요할 때 명시적으로 next()를 호출하는 방법을 사용한다. 또한 이터레이터는 다양한 언어 기능과 결합되어 유용하게 활용될 수 있다.

이터러블/이터레이터로부터 배열을 만드는 방법

✨ 1. 전개 구문 (Spread Syntax) ...

가장 간결한 방법 중 하나이다. 이터러블 객체의 모든 요소를 가져와 새 배열의 요소로 펼쳐준다.

const myArray = ['A', 'B', 'C'];
const newArray = [...myArray]; // myArray의 이터레이터를 소진하여 새 배열 생성
console.log(newArray); // 출력: ['A', 'B', 'C']

const mySet = new Set([1, 2, 3]);
const arrayFromSet = [...mySet]; // Set의 이터레이터를 소진하여 새 배열 생성
console.log(arrayFromSet); // 출력: [1, 2, 3]

실적으로 전개구문으로 새 배열을 만들어도 값은 원본과 동일하다. 하지만 이렇게 새 배열을 만드는 이유는 다음과 같다.

https://lamarck009.tistory.com/104

✨ 2. Array.from() 메서드

이터러블 객체나 유사 배열 객체로부터 새로운 배열 인스턴스를 생성한다.

const myString = "Hi";
const arrayFromString = Array.from(myString); // 문자열의 이터레이터를 소진하여 새 배열 생성
console.log(arrayFromString); // 출력: ['H', 'i']

수동으로 배열 만들기

for...of 루프나 while 루프와 next()를 사용하여 이터레이터의 값을 하나씩 가져와 빈 배열에 push할 수도 있다.

const generator = (function*() { yield 10; yield 20; })(); // 제너레이터 (이터레이터)
const manualArray = [];
for (const value of generator) { // 제너레이터 이터레이터를 소진
  manualArray.push(value);
}
console.log(manualArray); // 출력: [10, 20]

💡 결론

  • [Symbol.iterator]()는 이터레이터 객체를 반환한다 (배열 X).
  • iterator.next()는 이터레이터로부터 다음 값을 가져온다 (배열 생성 X).
  • 이터러블/이터레이터의 모든 요소를 담은 새로운 배열을 만들려면 전개 구문(...), Array.from(), 또는 수동 반복 및 push와 같은 별도의 배열 생성 과정이 필요하다. 이 과정에서 내부적으로 이터레이터가 소진된다.
 

제너레이터 (Generator)

제너레이터(Generator)는 이터레이터(Iterator)를 생성하는 특별한 종류의 함수(Function)이다. 일반 함수는 return 문을 만나면 실행이 완전히 종료되지만, 제너레이터 함수는 yield 라는 키워드를 사용하여 함수의 실행을 일시 중지(Pause)하고 중간 값을 반환할 수 있으며, 나중에 실행을 재개(Resume)할 수 있다.

제너레이터 함수를 호출하면 함수 본문이 즉시 실행되지 않고, 대신 제너레이터 객체(Generator Object)가 반환된다. 이 제너레이터 객체는 이터레이터이면서 동시에 이터러블(Iterable)이다.

✨ 핵심 문법

  1. function* 선언: 제너레이터 함수는 function 키워드 뒤에 별표(*)를 붙여 선언한다.화살표 함수로는 제너레이터를 만들 수 없다.
    function* myGenerator() {
      // 제너레이터 함수 본문
    }
    
  2. yield 키워드:
      • 제너레이터 함수의 실행을 일시 중지하고, yield 뒤에 오는 표현식의 값을 반환한다.
      • 정확히는 { value: yieldedValue, done: false } 형태의 이터레이터 결과 객체를 반환한다.
      • 제너레이터 객체의 next() 메서드가 다시 호출되면, 멈췄던 yield 문 다음부터 실행을 재개한다.
      • yield는 표현식이기도 해서, next(value)를 통해 제너레이터 외부에서 내부로 값을 전달받을 수도 있다 (양방향 통신).
    function* basicSequence() {
      yield 1; // 1을 반환하고 멈춤
      yield 2; // 다음 next() 호출 시 2를 반환하고 멈춤
      yield 3; // 그 다음 next() 호출 시 3을 반환하고 멈춤
    }
    
    const gen = basicSequence();
    
    console.log(gen.next()); // 출력: { value: 1, done: false }
    console.log(gen.next()); // 출력: { value: 2, done: false }
    console.log(gen.next()); // 출력: { value: 3, done: false }
    console.log(gen.next()); // 출력: { value: undefined, done: true } (더 이상 yield 없음)

    const gen = basicSequence(); 해당 변수에 제너레이터 함수 할당 및 제너레이터 객체 생성 하지만 제너레이터 객체만 생성될 뿐, basicSequence 함수 내부의 코드는 전혀 실행되지 않는다.

    함수 내부의 코드가 처음 실행되는 것은 1번째 next가 나오는 시점이다.

     

    console.log(gen.next()); → basicSequence()의 제너레이터 객체 참조 → next 메서드로 1번째 yield 부분을 실행 → 출력 { value: 1, done: false }  → console.log(gen.next()); → basicSequence()의 제너레이터 객체 참조 → next로 2번째 yield 부분을 실 → 출력 2~ 식으로 진행된다.

    이렇게 3번째 yield 진행된 이후에 next를 하려고 하면 더이상 다음 yield값이 없기 때문에 { value: undefined, done: true }를 출력하게 된다.

    마지막 객체였기 때문에 done:true로 값이 변경되게 되며 다시 next 메서드를 호출해도 동일한 값이 출력된다.

     

  3. yield* 키워드:
      • 다른 이터러블 객체(예: 배열, 문자열, 다른 제너레이터)에게 실행을 위임(Delegate)한다.
      • yield* 표현식은 해당 이터러블의 모든 값을 하나씩 순서대로 yield한다. 마치 그 이터러블의 값들을 현재 제너레이터 함수 안에서 직접 yield하는 것처럼 동작한다.
    function* generatorA() {
      yield 1;
      yield 2;
    }
    
    function* generatorB() {
      yield 'a';
      yield* generatorA(); // generatorA에게 위임
      yield* [3, 4];      // 배열에게 위임
      yield 'b';
    }
    
    const genB = generatorB();
    
    console.log([...genB]); // 전개 구문으로 모든 값 꺼내기
    // 출력: [ 'a', 1, 2, 3, 4, 'b' ]

📌 목적 및 사용

  • 간편한 이터레이터 생성: 복잡한 상태 관리나 next() 메서드를 직접 구현할 필요 없이, yield를 사용하여 이터레이터를 쉽게 만들 수 있다.
  • 메모리 효율성 (지연 평가, Lazy Evaluation): 이터레이터와 마찬가지로, 필요한 시점에 값을 계산하고 반환하므로 메모리를 효율적으로 사용한다. 대규모 데이터셋이나 무한 시퀀스 처리에 유용하다.
  • 비동기 처리: async function*으로 정의되는 비동기 제너레이터(Async Generator)는 비동기 작업(예: API 호출, 파일 읽기)의 결과를 순차적으로 처리하는 데 매우 효과적이다. for await...of 구문과 함께 사용된다.
  • 상태 관리: 함수의 실행 컨텍스트(변수, 상태 등)가 yield 지점에서 유지되므로, 복잡한 상태를 가진 순회를 쉽게 구현할 수 있다.
// 제너레이터 함수 정의
function* countUpTo(n) {
  let i = 1;
  while (i <= n) {
    yield i; // 실행을 멈추고 i 값을 반환
    i++;    // next()가 다시 호출되면 여기서부터 실행 재개
  }
  // 함수 실행이 끝나면 { value: undefined, done: true } 반환
}

const generatorObj = countUpTo(3); // 제너레이터 객체 생성 (이터레이터이자 이터러블)

// 제너레이터 객체는 이터레이터이므로 next() 사용 가능
console.log(generatorObj.next()); // { value: 1, done: false }
console.log(generatorObj.next()); // { value: 2, done: false }
console.log(generatorObj.next()); // { value: 3, done: false }
console.log(generatorObj.next()); // { value: undefined, done: true }

// 제너레이터 객체는 이터러블이므로 for...of 사용 가능
// (새로운 제너레이터 객체 생성 필요, 이미 위에서 소진했으므로)
for (const num of countUpTo(3)) {
  console.log(num); // 1, 2, 3 순서대로 출력
}

// yield* 예시
function* anotherGenerator() {
  yield 'x';
  yield* countUpTo(2); // countUpTo 제너레이터에게 위임
  yield 'y';
}

const genDelegate = anotherGenerator();
console.log(genDelegate.next()); // { value: 'x', done: false }
console.log(genDelegate.next()); // { value: 1, done: false }
console.log(genDelegate.next()); // { value: 2, done: false }
console.log(genDelegate.next()); // { value: 'y', done: false }
console.log(genDelegate.next()); // { value: undefined, done: true }

 

💾 자바스크립트의 이터레이션 프로토콜

자바스크립트에서는 이터레이션을 위해 두 가지 프로토콜을 정의한다.

  1. 이터러블 프로토콜 (Iterable Protocol):
    • 객체가 Symbol.iterator 메서드를 가지고 있어야 한다.
    • 이 메서드는 이터레이터 객체를 반환해야 한다.
    • Array, String, Map, Set, NodeList 등이 내장 이터러블 객체다.
  2. 이터레이터 프로토콜 (Iterator Protocol):
    • 객체가 next() 메서드를 가지고 있어야 한다.
    • next() 메서드는 { value: any, done: boolean } 형태의 객체를 반환해야 한다.
    • 제너레이터는 이 프로토콜을 구현하는 객체를 쉽게 만들기 위한 특별한 기능이라고 할 수 있다. 
      • value: 현재 순회 요소의 값. done이 true이면 생략될 수 있다.
      • done: 순회가 종료되었으면 true, 아니면 false.