[TIL] for-of에 숨겨진 동작 원리

cheo·2021년 8월 15일
3

TIL

목록 보기
4/5
post-thumbnail

질문

  • Q1. for-of는 어떻게 동작하는가?
  • Q2. [Symbol.iterator]()는 무엇인가?
  • Q3. 이터러블과 이터레이터는 무엇인가?
  • Q4. well-formed 이터레이터란 무엇인가?

답변

TL;DR

  • for-of는 이터러블 인터페이스인 [Symbol.iterator]() 메서드를 호출하고 그 결과 이터레이터가 리턴되어야 정상적으로 동작한다.
  • [Symbol.iterator]() 메서드는 이터러블의 인터페이스로서 이터레이터를 리턴하는 메서드이다.
  • 이터러블은 [Symbol.iterator]() 메서드를 실행하면 이터레이터를 리턴하는 값을 말한다.
  • 이터레이터는 next() 메서드를 실행하면 { value, done } 객체를 리턴하는 값을 말한다.
  • well-formed 이터레이터는 iterator === iterator[Symbol.iterator]()를 만족하는 이터레이터를 말한다.

for-of 동작 원리

const arr = [1, 2, 3];

for (const a of arr) console.log(a); // 1, 2, 3

위 코드를 보면 for...ofarr을 순회하여 요소값을 하나씩 출력하고 있다. 다음 코드를 보자.

const arr = [1, 2, 3];

arr[Symbol.iterator] = null;

// TypeError: arr is not iterable
for (const a of arr) console.log(a); 

에러가 발생하면서for-of가 동작하지 않는다. 에러를 읽어보면 arriterable이 아니라고 한다. iterable은 무엇일까? 단순히 단어 뜻 그대로 순회 가능한 값일까? 그렇다면 자바스크립트 코드에서 정의하는 이터러블의 조건은 무엇일까? 그리고 [Symbol.iterator]는 무엇일까?

Symbol.iterator, 이터러블, 이터레이터

ECMAScript Language Specification - well known symbols에 보면 다음과 같이 나와 있다:

Specification Name[[Description]]Value and Purpose
@@iteratorSymbol.iteratorA method that returns the default Iterator for an object. Called by the semantics of the for-of statement.

Symbol.iterator는 객체의 이터레이터를 리턴하는 메서드의 이름이고 for-of문에 의해 실행된다.

알게 된 내용을 정리해보면 다음과 같다:

  • for-of는 객체로부터 Symbol.iterator 메서드를 호출한다
  • Symbol.iterator 메서드 호출이 성공하지 않으면 for-of는 객체 타입이 이터러블이 아니라는 TypeError: arr is not iterable를 발생시킨다

따라서:

  • 이터러블 객체는 내부에 Symbol.iterator 메서드가 구현되어 있어야 한다

그리고 Specification Name인 @@iterator를 따라가면 ECMAScript Language Specification - iterable interface가 나온다:

PropertyValueRequirements
@@iteratorA function that returns an Iterator object.A method that returns the default Iterator for an object.

따라서:

  • Symbol.iterator 메서드는 이터레이터를 리턴한다

이터레이터의 대한 인터페이스도 마찬가지 문서에서 확인해볼 수 있다. 이터레이터 인터페이스는 next() 메서드를 실행하면 { value, done } 객체를 리턴한다.

지금까지 알게 된 내용을 요약하면 다음과 같다:

  • for-of는 이터러블의 인터페이스인 Symbol.iterator 메서드를 실행한다.
  • 이터러블은 Symbol.iterator 메서드를 실행하면 이터레이터를 리턴한다
  • 이터레이터는 next() 메서드를 실행하면 { value, done } 객체를 리턴한다.

최소한의 로직을 넣어 코드로 구현한 기본 형태는 다음과 같다:

// for-of 순회시 1, 2, 3을 리턴하는 이터러블
const iterable = {
  [Symbol.iterator]() {
    let i = 1;

    return {
      next() {
        return i === 4
          ? { done: true }
          : { value: i++, done: false };
      }
    }
  }
};

참고로 위 이터러블은 아직 well-formed 이터레이터를 리턴하지 않는데 이에 대해서는 좀 더 뒤에 가서 다룬다.

자바스크립트 내장 이터러블 객체

지금까지 알아본 내용에 의하면 for-of는 Symbol.iterator 메서드를 실행한다고 했다. 그렇다면 자바스크립트에서 Symbol.iterator 메서드가 내장으로 구현된 값은 무엇일까? Array, Set, Map이 Symbol.iterator 메서드가 구현된 이터러블 값이다.

코드로 보자:

// Array 객체는 Symbol.iterator 메서드가 구현된 이터러블이다
const arr = [1, 2, 3];

// for-of를 통해 arr을 순회할 수 있다
for (const a of arr) console.log(a); // 1, 2, 3

// arr은 이터러블이므로 [Symbol.iterator]() 메서드를 실행하면 이터레이터를 리턴한다
const iterator = arr[Symbol.iterator]();

// 이터레이터의 next() 메서드를 실행하면 { value, done } 객체를 리턴한다
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

이제 맨 처음에 봤던 코드로 돌아가서 동작하지 않았던 이유를 알아보자:

const arr = [1, 2, 3];

// arr 내부에서 이터레이터를 반환하는 Symbol.iterator를 null로 만든다 
arr[Symbol.iterator] = null;

// for-of는 Symbol.iterator 메서드를 실행하는데 null이므로 실행이 불가하다
// 그러므로 arr이 이터러블이 아니라는 에러가 발생한다
// => TypeError: arr is not iterable
for (const a of arr) console.log(a);

이번에는 이터레이터를 꺼내서 직접 next() 메서드를 실행해보자.

const arr = [1, 2, 3];

// arr은 이터러블이므로 [Symbol.iterator]() 메서드를 실행하여 이터레이터를 꺼낸다
const iterator = arr[Symbol.iterator]();

// 이터레이터 next() 메서드를 실행한다
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

well-formed iterator

well-formed 이터레이터란 이터레이터이자 이터러블인 값을 말한다. 이게 무슨 말이지? 천천히 살펴보자.

  • 이터러블: Symbol.iterator 메서드를 실행하면 이터레이터를 리턴한다
  • 이터레이터: next 메서드를 실행하면 { value, done }을 리턴한다

그렇다면 이터러블이자 이터레이터라는 것은 다음을 의미한다:

  • 이터러블이자 이터레이터:
    • Symbol.iterator 메서드를 실행하면 이터레이터를 리턴한다
    • next 메서드를 실행하면 { value, done }을 리턴한다

이때 이터레이터의 Symbol.iterator 메서드는 이터레이터를 리턴해야 하므로 이미 구현된 이터레이터인 자기 자신을 리턴하면 된다.

코드로 나타내면 다음과 같다:

const iterable = {
  [Symbol.iterator]() {
    let i = 1;

    return {
      next() {
        return i === 4
          ? { done: true }
          : { value: i++, done: false };
      },
      [Symbol.iterator() {
        return this;
      }
    }
  }
};

이제 iterable은 well-formed 이터레이터를 리턴한다.

자바스크립트 내장 값인 Array에서 꺼낸 이터레이터 또한 well-formed 이터레이터이다. 그래서 이터레이터가 곧 이터러블이므로 이터러블인 arr이 아니라 arr에서 꺼낸 이터레이터로 for-of 순회가 가능하다.

const arr = [1, 2, 3];

const iterator = arr[Symbol.iterator]();

for (const a of iterator) console.log(a); // 1, 2, 3

그리고 well-formed 이터레이터의 Symbol.iterator 메서드를 실행하면 자기 자신을 리턴하므로 일치 비교를 하면 참값이 나온다.

console.log(iterator === iterator[Symbol.iterator]()); // true

여기서 생각해볼 점은 well-formed 이터레이터는 자기 자신을 리턴하기 때문에 { value , done }이 어디까지 진행되었는지 기억할 수 있다는 점이다. 코드로 재현해보자.

const arr = [1, 2, 3];

const iterator = arr[Symbol.iterator]();

// arr의 이터레이터가 방금 꺼낸 iterator를 참조하도록 한다
arr[Symbol.iterator] = () => iterator;

// 이터레이터를 한 번 순회한다
console.log(iterator.next()); // { value: 1, done: false }

// arr을 전체 순회한다
// => 1, 2, 3이 아니라 2, 3을 출력한다
for (const a of arr) console.log(a); // 2, 3

우리가 직접 만든 이터러블로도 재현할 수 있다.

// 순회시 1, 2, 3을 리턴하는 이터러블
const iterable = {
  [Symbol.iterator]() {
    let i = 1;

    return {
      next() {
        return i === 4
          ? { done: true }
          : { value: i++, done: false };
      },
      [Symbol.iterator]() {
        return this;
      }
    }
  }
};

const iterator = iterable[Symbol.iterator]();

for (const a of iterator) {
  console.log(a); // 1
  break;
};

for (const a of iterator) {
  console.log(a); // 2, 3
};

어디에 써먹을까

  • 오늘 배운 이터러블/이터레이터 개념을 이해하면 제너레이터 함수를 잘 이해할 수 있다.
  • 제너레이터 함수를 이용하면 지연 평가되는 값을 만들 수 있고 이는 큰 데이터를 다룰 때 메모리 측면에서 매우 효율적이다.
  • 데이터 파이프라인을 만드는 함수형 프로그래밍에서는 리스트 프로세싱이 중요한데 이터러블/이터레이터 개념이 기초가 된다.

회고

TIL을 작성하면서 느낀 점을 이야기합니다.

  • Symbol.iterator에 대해 직접 ECMAScript 공식 문서를 찾아봤고 그로 인해 더욱 정확하게 개념을 알게 되었다.
  • 모르는 개념을 이해하기 위해 ECMAScript를 직접 찾아본 것은 처음이었는데 구체적이고 분명하게 인터페이스가 나와 있어서 머리로 이해하고 코드로 구현하는 과정이 훨씬 수월했다. 앞으로도 자주 찾아봐야겠다는 생각이 들었다.
  • ECMAScript를 보기 전에, well-formed 이터레이터와 non-well-formed 이터레이터를 비교하는 예제를 작성하려고for-of에 non-well-formed 이터레이터를 넣어서 코드를 실행했다가 에러가 발생했다. 왜 에러가 발생하는지 이해가 안되었다. 그러다 원인을 찾았다(for-of는 이터러블 인터페이스인 Symbol.iterator 메서드를 실행하는데 non-well-formed 이터레이터는 해당 메서드가 없으므로 TypeError가 발생한다). 이미 문서를 거의 다 작성한 상태였지만 싹 다 갈아엎고 다시 작성했다. 결과적으로 본 포스팅을 작성하는데 2시간이 넘게 걸렸는데, 그래도 덕분에 오늘 작성한 개념에 대해서는 백지장에서부터 바로 설명할 수 있을 것 같다.

See also

0개의 댓글