JS의 Iterable, Iterator, Generator?

KB LEE·2025년 2월 23일
0

JavaScript 펼쳐보기

목록 보기
5/5
post-thumbnail

목적

이 문서는 자바스크립트애서 Iterable, Iterator, 심화과정인 Generator를 알아보고 정리하기 위한 문서입니다.

이 글에서는 다음과 같은 내용을 다룹니다.

  • Iterable과 Iterator의 개념과 관계, 특징
  • Generator의 개념과 특징
  • 실전 예제

Iterable? Iterator?

Iterable

Iterable은 순회할 수 있는 객체, 반복가능한 객체를 의미합니다.

→ 객체를 for...of 구조에서 반복되는 값과 같은 순회 동작을 정의하거나 사용자 지정 가능

이러한 객체들은 Symbol.iterator 메서드를 구현하여 iterable protocol을 준수해야 합니다.

Symbol.iterator?

  • Symbol.iterator는 ECMAScript에 정의된 Well-Known Symbol 중 하나
  • 객체가 이터러블(Iterable) 로 동작하기 위해 반드시 구현해야 하는 메서드 키
  • 이 값을 프로퍼티 키(property key)로 하는 메서드는, 객체를 순회할 때 사용할 이터레이터(Iterator) 객체를 반환

Iterator

Iterator 객체는 iterator 결과 객체를 반환하는 next() 메서드를 제공하여 iterator protocol에 부합하는 객체

특징

  • next() 메서드를 호출할 때마다 { value, done } 형식의 객체를 반환
  • 더 이상 순회할 값이 없으면 { value: undefined, done: true } 반환

예시

위에서 Iterable은 반복할 수 있는 객체를 의미한다고 했습니다

저희가 알고 있는 가장 대표적인 예시엔 배열이 있습니다.

Q. 그렇다면 배열도 Iterable인가요?
A. 넵, 맞습니다. 배열과 같이 ECMAScript의 일부 표준 내장 객체에는 Symbol.iterator가 구현된 경우가 있습니다.

몇가지 코드 예시를 보겠습니다.

Array

const arr = [1, 2, 3, 4, 5]
for(let n of arr) {
	console.log(n); // 정상출력
}

arr[Symbol.iterator] = null;
for(let n of arr) {
	console.log(n); // TypeError: arr is not iterable
}

String

const str = "My name is KB";
for (const char of str) {
  console.log(char); 
  // 'M', 'y', ' ', 'n', 'a', 'm', 'e', ' ', 'i', 's', ' ', 'K', 'B'
}

Map/Set

const mySet = new Set([1, 2, 3]);
for (const value of mySet) {
	console.log(value);
	// 1, 2, 3
}

const myMap = new Map([
  ["a", 1],
  ["b", 2]
]);
for (const [key, value] of myMap) {
  console.log(key, value);
  // a 1, b 2
}

사용자 정의 Symbol.iterator

const kb_iterator_example = {
	from: 1,
	to: 5
}

kb_iterator_example[Symbol.iterator]: function () {
	return {
		from: this.from,
		to: this.to,
		next() {
			if( this.from <= this.to ) {
				return { value: this.current, done: false };
			} else {
				return { done: true };
			}
		}
	}
}

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

Generator?

아래 Generator의 코드를 확인해보겠습니다.

function* KBGen() {
  yield 1;
  yield 2;
  yield 3;
}

const kb_gen = KBGen();
console.log(kb_gen.next());
console.log(kb_gen.next());
console.log(kb_gen.next());
console.log(kb_gen.next());

const kb_gen = KBGen();
for(const value of kb_gen) {
	console.log(value);
}

Q. 갑자기 웬 코드인가요?

갑자기 Generator 코드를 보면 당황스러울 수 있습니다. 하지만 iterable, iterator에서 보았던 굉장히 익숙한 내용들을 볼 수 있습니다.

  • next() 메서드를 통해 값을 하나씩 반환
  • for...of 구조에서 반복되는 값과 같은 순회 동작

위 내용을 통해 Generator를 통해 반환되는 Generator객체는 제너레이터 함수를 호출하면 반환되는 제너레이터 객체는 Iterable이자 Iterator인 객체임을 알 수 있습니다.

정의

  • Generator(제너레이터)function*(별표가 붙은 함수) 키워드로 정의되는 특수한 함수
  • 일반적인 함수와 달리, 중단(pause)과 재개(resume) 가 가능
  • 호출 시 Iterator이자 Iterable인 객체(제너레이터 객체)를 반환
    • 즉, 한 번 호출로 “이터러블 + 이터레이터” 역할을 동시에 수행할 수 있는 객체를 얻을 수 있습니다.

특징

  • next() 메서드를 호출할 때마다 yield 표현식 혹은 함수 블록의 끝까지 실행
  • yield를 만나면 해당 값을 { value, done: false } 형태로 반환하고 실행이 일시 중단
  • 다시 next()를 호출하면 중단된 지점부터 재개
  • 모든 yield가 소비되거나, 함수 블록 끝에 도달하면 { value: undefined, done: true }가 반환됨

사용예시

Q. Generator가 어떤 것인지 이해했습니다. 그렇다면 Generator는 어떤 용도로 사용할 수 있나요?

특징 중 가장 중요한 것은 중단과 재개 매커니즘입니다.

→ 여러 번에 걸쳐서 함수의 상태를 유지하며 실행할 수 있음을 의미

몇가지 예시를 살펴보겠습니다.

1. Lazy Evaluation (지연평가)

  1. 무한수열
    무한대로 증가하는 시퀀스를 한 번에 전부 메모리에 올릴 수는 없지만, Generator를 사용하면 필요할 때마다 숫자를 하나씩 생성

    // 무한히 증가하는 숫자를 내보내는 제너레이터
    function* infiniteCounter() {
      let i = 0;
      while (true) {
        yield i++;
      }
    }
    
    // 필요한 순간에만 next()로 값을 요청
    const counter = infiniteCounter();
    console.log(counter.next().value); // 0
    console.log(counter.next().value); // 1
    console.log(counter.next().value); // 2
    // ... 계속해서 무한정 꺼낼 수 있음
  2. 대용량 데이터 파일 지연 로드
    수십만 줄짜리 로그 파일(log.txt)을 한꺼번에 읽으면 메모리 문제가 생길 수 있습니다. Generator를 통해 필요한 만큼만 줄 단위로 읽어서 처리

    const fs = require("fs");
    
    function* readLargeFileByLine(filePath) {
      const fileData = fs.readFileSync(filePath, "utf-8"); 
      // 실제 대규모 데이터를 처리할 땐 stream을 사용하거나 chunk 단위로 처리하는 방식을 쓸 수 있음
      // 여기서는 개념 시연을 위해 readFileSync를 예시로 사용
      const lines = fileData.split("\n");
      for (const line of lines) {
        yield line; // 한 줄씩 반환
      }
    }
    
    const lineReader = readLargeFileByLine("log.txt");
    
    // 필요한 시점에만 한 줄씩 가져옴
    console.log(lineReader.next().value); // 파일 첫 번째 줄
    console.log(lineReader.next().value); // 파일 두 번째 줄
    // ...

2. 비동기 처리 패턴

ES6이전(async/await가 도입되기 이전)엔 Generatoryield를 활용하여 Promise 기반 비동기를 마치 동기 코드처럼 순차적으로 작성하는 방식을 선택했습니다.

하지만 async/await가 위와 같은 기능을 대부분 대체했지만, 여전히 제너레이터가 커스텀 러너를 만들거나 복잡한 상태 기계(state machine)를 구현

// ex) Redux-Saga

import { call, put, takeEvery } from 'redux-saga/effects';

function* fetchUserSaga(action) {
  const user = yield call(fetchUserApi, action.payload.userId);
  yield put({ type: "USER_FETCH_SUCCESS", user });
}

function* mySaga() {
  yield takeEvery("USER_FETCH_REQUEST", fetchUserSaga);
}

정리 및 후기

iterable, iterator, generator 개념을 공부해보았습니다.

실제 개발 상황에서는 대부분 async/await로 비동기 로직을 처리하기 때문에, 특별한 요구사항이 없으면 generator 구문을 활용하는 빈도가 낮았습니다.

그러나 이번에 문서를 정리하며, generator가 어떻게 동작하고 iterable, iterator 개념을 어떻게 바탕으로 하는지 구체적으로 이해할 수 있었습니다.

특히 generator만의 지연 실행(lazy evaluation), 상태 제어 등 독특한 특징이 분명한 장점을 제공한다는 점이 흥미로웠습니다.

다음에도 더 알차고 체계적인 정리로 다시 찾아뵙겠습니다.

읽어주셔서 감사합니다!


참고문서

for...of - JavaScript | MDN

Generator - JavaScript | MDN

Iterator - JavaScript | MDN

Iteration protocols - JavaScript | MDN

Lazy Evaluation

profile
한 발 더 나아가자

0개의 댓글