학습 공간 개념 · 판단 · 실험

Frontend Performance Lab

JavaScript

이터레이션 프로토콜과 ES2015 문법

순회 가능한 컬렉션(Iterable)과 다음 값을 하나씩 꺼내는 상태 기계(Iterator)의 역할을 구분하고, 제너레이터로 필요한 시점에만 값을 생산합니다.

이럴 때 필요해요

배열뿐만 아니라 대용량 데이터나 커스텀 컬렉션도 메모리에 한꺼번에 올리지 않고 표준 문법(for...of, spread)으로 순회할 수 있습니다.

어떻게 동작하나요?

배열, 문자열, Map, Set은 모두 생김새가 다른데도 똑같이 for...of 반복문이나 전개 연산자([...items])에 넣어 값들을 차례대로 꺼낼 수 있습니다. 이들이 공통으로 이터레이션 프로토콜(Iteration Protocol)이라는 약속을 따르기 때문입니다. 구체적으로 객체에 [Symbol.iterator]() 메서드가 정의되어 있으면 이를 순회 가능한 객체인 이터러블(Iterable)이라고 부르고, 이 메서드가 반환하는 객체는 next()를 호출할 때마다 { value, done } 형태의 결과를 돌려주는 이터레이터(Iterator)가 됩니다. 일반 객체({})는 기본적으로 이 프로토콜이 없지만 직접 [Symbol.iterator]를 구현하면 for...of로 순회할 수 있습니다.

이터레이터를 직접 객체로 작성하려면 내부 상태 관리가 번거로운데, function* 키워드로 선언하는 제너레이터(Generator) 함수를 쓰면 이를 아주 쉽게 만들 수 있습니다. 제너레이터 함수는 호출했을 때 코드를 바로 끝까지 실행하지 않고, 이터레이터이자 이터러블인 제너레이터 객체를 반환합니다. 이후 next()를 부를 때마다 다음 yield 문을 만날 때까지만 실행하고 그 자리에서 멈춘 뒤 값을 내어주므로, 필요한 만큼만 값을 계산하는 지연 평가(Lazy Evaluation)에 적합합니다. 단, 한 번 끝까지 소비한 제너레이터 객체는 다시 처음부터 순회되지 않으므로 새로 순회하려면 제너레이터 함수를 다시 호출해야 합니다.

이 프로토콜과 제너레이터는 ES2015(ES6)에서 let/const, 화살표 함수, class, ES 모듈, 구조 분해 할당, Promise, Map/Set, Symbol 등과 함께 도입되었습니다. 반면 실무에서 함께 자주 쓰는 async/await는 ES2017, 객체 리터럴의 rest/spread는 ES2018에 추가된 기능이므로 모든 최신 문법을 무조건 ES6라고 부르지 않고 대상 런타임의 문법·API 지원 여부를 구분해 확인하는 것이 좋습니다.

코드 예제

동작을 설명하는 읽기 전용 예제입니다.

example.js
function* range(end) {
  for (let value = 0; value < end; value++) yield value;
}
const iterator = range(3);
console.log(iterator.next()); // { value: 0, done: false }
console.log([...iterator]); // [1, 2]
javascript · 읽기 전용6줄

next()로 첫 번째 값 0을 먼저 꺼낸 뒤 [...iterator]로 펼치면, 이미 소비된 0을 제외한 나머지 값 [1, 2]만 배열로 모입니다. 다시 0부터 얻으려면 range(3)을 새로 호출해야 합니다.

자주 헷갈리는 포인트

  • 순회 가능한 대상인 Iterable([Symbol.iterator] 보유)과 진행 상태를 가진 Iterator(next() 보유)를 같은 조건으로 혼동하지 않습니다.
  • 일반 동기 제너레이터(function*)가 실행을 멈췄다 재개한다고 해서 스스로 비동기 I/O나 멀티스레드 병렬 처리를 수행하는 것은 아닙니다.

내 말로 설명해 보기

일반 객체({})를 for...of 반복문으로 순회할 수 있게 만들려면 어떤 메서드와 반환 규칙을 구현해야 할까요?

함께 읽으면 좋은 개념

관련 면접 질문 3개

ES2015에서 도입된 주요 기능과 이후 기능을 어떻게 구분하나요?

ES2015(ES6)는 let/const, 화살표 함수, class, ES 모듈, 템플릿 리터럴, 구조 분해 할당, 기본·rest 매개변수, 배열 spread, Promise, Map/Set, Symbol, 제너레이터 등을 도입하며 현대 JavaScript의 기반을 만든 표준입니다. 다만 async/await(ES2017)나 객체 rest/spread(ES2018) 등은 이후에 추가된 기능이므로, 모든 최신 문법을 ES6로 뭉뚱그리기보다 대상 런타임과 빌드 도구의 지원 범위를 기준으로 구분합니다.

꼬리질문과 참고자료 →

제너레이터는 실행을 어떻게 중단하고 재개하나요?

function*으로 정의하는 제너레이터 함수는 호출 시 본문을 바로 끝까지 실행하지 않고 제너레이터 객체를 반환합니다. 이 객체의 next()를 호출할 때마다 다음 yield 문을 만날 때까지만 실행되어 { value, done }을 돌려주고 그 자리에서 일시 중단됩니다. 다시 next()를 부르면 멈췄던 지점과 지역 변수 상태를 유지한 채 실행을 재개하므로, 대량의 값을 미리 배열로 만들지 않고 필요할 때마다 하나씩 생산하는 지연(lazy) 순회에 유용합니다.

꼬리질문과 참고자료 →

Iterable과 Iterator 프로토콜은 어떻게 협력하나요?

이터레이션 프로토콜은 데이터 컬렉션을 for...of나 전개 연산자(...)로 일관되게 순회하기 위한 약속으로, Iterable과 Iterator 두 프로토콜로 나뉩니다. Iterable은 [Symbol.iterator]() 메서드를 호출했을 때 Iterator 객체를 반환하는 약속이고, Iterator는 next()를 호출할 때마다 { value, done } 객체를 반환하는 약속입니다. 배열, 문자열, Map, Set은 기본 내장되어 있으며 일반 객체도 이 메서드를 구현하면 순회할 수 있습니다.

꼬리질문과 참고자료 →

공식 참고 자료

← JavaScript