반응형
Jake Seo
제이크서 위키 블로그
Jake Seo
전체 방문자
오늘
어제
  • 분류 전체보기 (715)
    • 일상, 일기 (0)
    • 백준 문제풀이 (1)
    • 릿코드 문제풀이 (2)
    • 알고리즘 이론 (10)
      • 기본 이론 (2)
      • 배열과 문자열 (8)
    • 데이터베이스 (15)
      • Planet Scale (1)
      • MSSQL (9)
      • 디비 기본 개념 (1)
      • SQLite 직접 만들어보기 (4)
    • 보안 (7)
    • 설계 (1)
    • 네트워크 (17)
      • HTTP (9)
      • OSI Layers (5)
    • 회고 (31)
      • 연간 회고 (2)
      • 주간 회고 (29)
    • 인프라 (52)
      • 도커 (12)
      • AWS (9)
      • 용어 (21)
      • 웹 성능 (1)
      • 대규모 서비스를 지탱하는 기술 (9)
    • 깃 (7)
    • 빌드 도구 (7)
      • 메이븐 (6)
      • 그레이들 (0)
    • Java (135)
      • 이펙티브 자바 (73)
      • 자바 API (4)
      • 자바 잡지식 (30)
      • 자바 디자인 패턴 (21)
      • 톰캣 (Tomcat) (7)
    • 프레임워크 (64)
      • next.js (14)
      • 스프링 프레임워크 (28)
      • 토비의 스프링 (6)
      • 스프링 부트 (3)
      • JPA (Java Persistence API) (5)
      • Nest.js (8)
    • 프론트엔드 (48)
      • 다크모드 (1)
      • 노드 패키지 관리 매니저 (3)
      • CSS (19)
      • Web API (11)
      • tailwind-css (1)
      • React (5)
      • React 새 공식문서 요약 (1)
      • HTML (Markup Language) (5)
    • 자바스크립트 (108)
      • 모던 자바스크립트 (31)
      • 개념 (31)
      • 정규표현식 (5)
      • 코드 스니펫 (1)
      • 라이브러리 (6)
      • 인터뷰 (24)
      • 웹개발자를 위한 자바스크립트의 모든 것 (6)
      • 팁 (2)
    • Typescript (49)
    • 리눅스와 유닉스 (10)
    • Computer Science (1)
      • Compiler (1)
    • IDE (3)
      • VSCODE (1)
      • IntelliJ (2)
    • 세미나 & 컨퍼런스 (1)
    • 용어 (개발용어) (16)
      • 함수형 프로그래밍 용어들 (1)
    • ORM (2)
      • Prisma (2)
    • NODEJS (2)
    • cypress (1)
    • 리액트 네이티브 (React Native) (31)
    • 러스트 (Rust) (15)
    • 코틀린 (Kotlin) (4)
      • 자바에서 코틀린으로 (4)
    • 정규표현식 (3)
    • 구글 애널리틱스 (GA) (1)
    • SEO (2)
    • UML (2)
    • 맛탐험 (2)
    • 리팩토링 (1)
    • 서평 (2)
    • 소프트웨어 공학 (18)
      • 테스팅 (16)
      • 개발 프로세스 (1)
    • 교육학 (1)
    • 삶의 지혜, 통찰 (1)
    • Chat GPT (2)
    • 쉘스크립트 (1)
    • 컴파일 (2)
    • Dart (12)
    • 코드팩토리의 플러터 프로그래밍 (4)
    • 플러터 (17)
    • 안드로이드 스튜디오 (1)
    • 윈도우즈 (1)
    • 잡다한 백엔드 지식 (1)
    • 디자인 패턴 (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • item9
  • 플라이웨이트패턴
  • 추상 팩터리 패턴
  • 이펙티브 자바
  • 메이븐 골
  • 알고리즘
  • 자바스크립트 면접
  • pnpm
  • 객체복사
  • 팩터리 메서드 패턴
  • 자바 디자인패턴
  • rust
  • 러스트
  • NEXT JS
  • 메이븐 라이프사이클
  • bean Validation
  • MSSQL
  • 싱글턴
  • try-with-resources
  • 참조 해제
  • item7
  • 싱글톤
  • 외래키 제약조건
  • 슬로우 쿼리
  • prerendering
  • 자바스크립트
  • 자료구조
  • 싱글톤 패턴
  • Javadoc 자바독 자바주석 주석 Comment
  • 이펙티브 자바 item9
  • 이펙티브자바
  • Pre-rendering
  • 도커공식문서
  • Next.js
  • 프로그래머의 뇌
  • 느린 쿼리
  • Java
  • 자바스크립트 인터뷰
  • 빈 검증
  • serverless computing
  • item8
  • 서버리스 컴퓨팅
  • 스프링 검증
  • 메이븐 페이즈
  • 디자인패턴
  • 자바
  • 토비의 스프링
  • next js app
  • 자바 검증
  • 작업기억공간

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
Jake Seo

제이크서 위키 블로그

자바스크립트/모던 자바스크립트

모던 자바스크립트, 이터러블 (iterable) 과 이터레이터 (iterator)

2023. 2. 28. 19:26

이터레이터(iterator) 란?

  • next() 메서드가 있는 객체이다.
  • next() 를 호출할 때마다 시퀀스의 다음 값과 완료 여부를 나타내는 플래그를 반환한다.
    • ex) {value: 10, done: false}

이터러블(iterable) 이란?

  • 이터레이터를 가져오는 표준 메서드가 있는 객체이다.
    • Symbol.iterator 프로퍼티에서 iterator 를 반환하는 메서드를 구현하면 된다.
      • 프로퍼티로 얻어오는 것이 아니라, 메서드로 얻어오는 이유는 매번 가장 첫번째 원소를 가리키는 iterator 를 생성하여 반환하고 싶기 때문일 것이라 추측한다.

for of 와 iterator

  • iterator 를 구현하여 iterable 객체를 만들면, for of 문에 의해 반복이 가능하다.
const a = ["a", "b", "c"];
for (const x of a) {
  console.log(x);
}
  • 위의 코드는 실제로 for of 구문이 iterator 를 반환받아 반복시키고 value 를 반환하는 동작을 가지고 있다.

이터레이터 명시적으로 사용해보기

const a = ["a", "b", "c"];
const it = a[Symbol.iterator]();
console.log("it", it); // Object [Array Iterator] {}
let result = it.next();
console.log("result", result); // { value: 'a', done: false }

while (!result.done) {
  console.log(result.value);
  result = it.next();
}
  • a[Symbol.iterator]() 를 통해 이터레이터를 가져올 수 있다.
    • ts 에서는 Iterator 인터페이스 를 따른다.
    • next(), return(), throw() 등의 메서드 이용이 가능하다.
    • 메서드의 반환 값은 주로 IteratorResult 이다.
interface Iterator<T> {
  next(value?: any): IteratorResult<T>;
  return?(value?: any): IteratorResult<T>;
  throw?(e?: any): IteratorResult<T>;
}
interface IteratorResult<T> {
  done: boolean;
  value: T;
}

자바스크립트 배열의 이터레이터 (iterator) 살펴보기

const arr = [];
console.log(arr[Symbol.iterator]());
/*
Array Iterator {}
  [[Prototype]]: Array Iterator
    next: ƒ next()
    Symbol(Symbol.toStringTag): "Array Iterator"
    [[Prototype]]: Object
*/

이터레이터 (iterator) 직접 구현해보기

const customArr = {
  cur: 0, // current
  length: 0,
  next() {
    if (this.cur < this.length) {
      return {
        value: this[this.cur++],
        done: false,
      };
    }

    return {
      value: undefined,
      done: true,
    };
  },
  return() {
    console.log("return");
    return { value: undefined, done: true };
  },
  [Symbol.iterator]() {
    return this;
  },
};

customArr[0] = 1;
customArr[1] = 20;
customArr[2] = 3;
customArr[3] = 10;

customArr.length = 4;

for (const e of customArr) {
  console.log(e);
}

Array 의 빌트인 메서드인 values() 를 통해 이터레이터 (iterator) 구현해보기

const customArr = {
  0: 1,
  1: 5,
  length: 2,
  [Symbol.iterator]() {
    return Array.prototype.values.call(this);
  },
};

for-of 를 돌렸을 때, 반대로 순회하는 Array 클래스 만들어보기

만일, 배열의 원소가 10만개 이상이며 프리즈 상태여서 내부의 요소를 수정할 수 없는 상황이면, reverse() 메서드를 통해 배열을 복사하기보다는 원본 배열을 반대로 순회할 수 있게 도와주는 iterator 를 가진 클래스를 만들어 어느정도 이득을 볼 수 있다.

이렇게 Symbol.iterator 를 구현해두면, for-of 뿐만 아니라, 이터레이터를 활용하는 어떠한 메서드에도 이를 적용시킬 수 있다.

class ReversedArray {
  constructor(arr) {
    this.arr = arr;
  }

  [Symbol.iterator]() {
    let index = this.arr.length - 1;

    return {
      next: () => {
        if (index >= 0) {
          return { value: this.arr[index--], done: false };
        }

        return { value: undefined, done: true };
      },
    };
  }
}

const reArr = new ReversedArray([1, 2, 3]);

for (const e of reArr) {
  console.log(e);
}

반복할 수 없는 객체를 반복하려 할 때

const obj = {};
for (const a of obj) {
  console.log(a);
}
  • obj[Symobl.iterator]() 의 반환값이 없는 것을 기준으로 반복할 수 없는 객체라는 것을 판단한다.
  • Uncaught TypeError: obj is not iterable 라는 예외를 출력해준다.

for-of 와 for-in 의 차이

얼핏 보기엔 비슷한 역할을 수행할 것 같은데, 두 구문에는 명확한 차이가 있다.

const a = ["a", "b", "c"];
a.extra = "extra property";

for (const value of a) {
  console.log(value); // a, b, c
}

for (const key in a) {
  console.log(key); // 0, 1, 2, extra
}
  • for-of 는 배열 이터레이터 에 의해 정의된 엔트리의 값 (value) 을 제공한다.
  • for-in 은 enumerable property 를 순회하며, 배열 엔트리 속성 이름 (key) 만 제공한다.

이터레이터는 for-of 에서만 쓰는 건 아니며, 스프레드 구문, 디스트럭처링, Promise.all(), Array.from(), Map, Set 등도 이터레이터를 사용한다.

이터레이터 사용 중 실수 예방하기

done 이 false 인 것 보장하기

const a = ["a", "b", "c"];
const it = a[Symbol.iterator]();
let result;

while (!(result = it.next()).done) {
  console.log(result.value);
}
  • while 안에서 it.next() 를 통한 할당을 수행하도록 약속하면, 무분별한 위치에서 it.next() 가 수행되는 위험을 줄일 수 있다.
    • iterator.next() 가 반환한 객체의 done 이 false 라는 보장이 있는 경우에만 로직을 수행한다.
    • it.next() 가 여러군데에서 사용되는 경우 생길 수 있는 오류를 예방한다.

이터레이터 반복 중지하기

이터레이터에서는 몇가지 반복을 중지하는 방법을 제공할 수 있다.

값을 찾았다면 즉시 종료하기 (return())

const a = ["a", "b", "c"];
const it = a[Symbol.iterator]();
let result;

while (!(result = it.next()).done) {
  if (result.value === "b") {
    if (it.return) {
      it.return();
    }

    break;
  }
}
  • 값을 찾은 시점에서 더이상 루프를 돌지 않고 멈추고 싶다면, return() 메서드를 사용하면 된다.
    • 명시적 return() 시에는 리소스를 정리해야 한다고 알리기 때문에, 혹시나라도 사용하지 않는 자원을 들고 있진 않을까 걱정할 필요가 없다.

for-of 는 iterator 를 이용한다. 그렇다면 for-of 는 중간에 break 로 끝났을 때 계속 메모리를 점유할까? 아니다. 탈출 시에 return() 메서드를 호출하도록 프로그래밍 되어 있다.

예외 던지기 (throw())

  • iterator 에 예외를 발견했다고 호출자가 알려줄 때 사용한다.
  • 간단한 iterator 에 잘 사용되진 않는다.

iterator 를 직접 구현해야 하는 경우

거의 없다.

  • 이터레이터에 기능을 추가하려는 경우
  • 이터레이터를 수동으로 추가해야만 하는 경우 (거의 없다.)

프로토타입을 추가할 때 유의사항

  • 추가한 속성/메서드가 열거할 수 없는지 확인한다.
  • 향후 추가될 수 있는 기능과 충돌할 가능성이 없는 이름인지 확인한다.
  • 대부분의 경우 라이브러리 코드는 프로토타입 수정을 완전히 피해야 한다.

Array Iterator 에 새로운 메서드 추가하기

const iteratorPrototype = Object.getPrototypeOf(
  Object.getPrototypeOf([][Symbol.iterator]())
);

Object.defineProperty(iteratorPrototype, "myFind", {
  value(callback, thisArg) {
    let result;

    while (!(result = this.next()).done) {
      if (callback.call(thisArg, result.value)) {
        break;
      }
    }

    return result;
  },
  writable: true,
  configurable: true,
});

const arr = ["one", "two", "three"];
const it = arr[Symbol.iterator]();

let result;

while (!(result = it.myFind((v) => v.includes("e"))).done) {
  console.log("Found: " + result.value);
}

// {value: 'one', done: false}
// {value: 'three', done: false}

위의 코드는 한번에 잘 안 와닿을 수 있는데, 아래와 같은 프로세스를 거쳤다.

  • [][Symbol.iterator]() 는 빈 배열이 가진 자신의 iterator 를 반환한다.
    • 내부적으로 Array Iterator 를 프로토타입으로 갖는 빈 오브젝트가 들어있다.
  • Object.getPrototypeOf([][Symbol.iterator]()) 를 통하면 프로토타입에 있던 Array Iterator 를 실제로 접근 가능하다.
  • 한 번 더 Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]())); 를 하면, 마침내 모든 Iterator 가 프로토타입으로 갖는 Symbol(Symbol.iterator) 를 가질 수 있다.
    • 여기에 메서드를 추가하면, 모든 iterator 에서 공통으로 해당 메서드를 사용할 수 있다.

일단 위의 이해를 기본으로 한다. 이후에는

  • 최상위 Iterator Prototype 에 myFind 라는 프로퍼티를 추가한다.
  • 이 프로퍼티의 값은 단순 value 가 아닌 메서드 프로퍼티로 지정된다.
  • 이 메서드 프로퍼티는 콜백 함수를 받고, 콜백 함수가 true 를 반환할 때까지 iterator.next() 를 진행한다.
    • 위의 예에서는 콜백에 includes("e") 를 넣어서, e 가 포함되지 않은 경우 스킵된다.
    • 조건이 맞는 경우만 break 에 걸려 return 이 이뤄진다.

iterator 의 프로토타입을 올바르게 설정하여 구현해보기

const a = {
  0: "a",
  1: "b",
  2: "c",
  length: 3,
  [Symbol.iterator]() {
    let index = 0;

    const it = {
      next: () => {
        if (index < this.length) {
          return { value: this[index++], done: false };
        }

        return { value: undefined, done: true };
      },
    };

    return it;
  },
};
  • next 에서 화살표 함수를 사용하지 않으면, this 가 it 자체가 되어버린다.
  • 위의 코드처럼 next() 메서드를 반환값 인터페이스에 맞게 올바르게 구현만 해도 for-of 를 사용하는데는 지장이 없지만, 프로토타입이 %IteratorPrototype% 은 아니다.
  • 위에서 정의했던 myFind() 와 같은 커스텀 iterator 메서드 사용이 불가능하다.
const a = {
  0: "a",
  1: "b",
  2: "c",
  length: 3,
  [Symbol.iterator]() {
    let index = 0;
    const itPrototype = Object.getPrototypeOf(
      Object.getPrototypeOf([][Symbol.iterator]())
    );

    const it = Object.assign(Object.create(itPrototype), {
      next: () => {
        if (index < this.length) {
          return { value: this[index++], done: false };
        }

        return { value: undefined, done: true };
      },
    });

    return it;
  },
};
  • 이제는 프로토 타입이 %IteratorPrototype% 이다.
  • 프로토 타입이 바뀌며, 기존의 프로토타입이 가지고 있던 메서드들도 다 사용할 수 있게 된다.
    • 이전에 정의한 myFind() 메서드도 사용할 수 있다.

LinkedList 이터레이터로 구현하기

class LinkedList {
  constructor() {
    this.head = this.tail = null;
  }

  add(value) {
    const entry = { value, next: null };

    if (!this.tail) {
      this.head = this.tail = entry;
    } else {
      this.tail = this.tail.next = entry;
    }
  }

  [Symbol.iterator]() {
    let current = this.head;
    const itPrototype = Object.getPrototypeOf(
      Object.getPrototypeOf([][Symbol.iterator]())
    );

    const it = Object.assign(Object.create(itPrototype), {
      next() {
        if (current) {
          const value = current.value;
          current = current.next;
          return { value, done: false };
        }

        return { value: undefined, done: true };
      },
    });

    return it;
  }
}

const list = new LinkedList();
list.add("one");
list.add("two");
list.add("three");

for (const e of list) {
  console.log(e);
}

부모 엘리먼트를 차례대로 가져오는 iterator 만들어보기

function parents(element) {
  return {
    next() {
      element = element && element.parentNode;

      if (element && element.nodeType === Node.ELEMENT_NODE) {
        return { value: element, done: false };
      }

      return { value: undefined, done: true };
    },
    [Symbol.iterator]() {
      return this;
    },
  };
}

이터러블 스프레드 문법

이터러블 스프레드 문법은 함수를 호출하거나 배열을 생성할 때 결괏값을 이산 값으로 분산하여 이터러블을 소비하는 방법을 제공한다.

배열을 이산 인수로 제공하고 싶을 때

const a = [1, 2, 3, 4, 5, 6];
Math.min(...a);
Math.min(-1, 0, ...a, 7, 8); // 이것도 유효하다

새로운 배열을 생성할 때

const a = [1, 2, 3, 4];
const b = [5, 6, 7, 8];
const c = [...a, ...b];
console.log(c); // [1, 2, 3, 4, 5, 6, 7, 8]

DOM 요소는 iterable 한가?

  • 최신 브라우저에서 다중 DOM 요소는 NodeList 라는 타입을 갖는다. (크롬, 파이어폭스, 엣지, 사파리)
  • WHAT-WG DOM 스펙은 NodeList 를 HTMLCollection 이 아니라 iterable 로 표시한다.
  • 결과적으로 DOM 요소에도 for-of 혹은 ... 같은 iterator 를 사용하는 문법을 쓸 수 있다.

DOM iterable 하도록 polyfill 작성해보기

(function () {
  if (Object.defineProperty) {
    var iterator =
      typeof Symbol !== "undefined" &&
      Symbol.iterator &&
      Array.prototype[Symbol.iterator];
    var forEach = Array.prototype.forEach;
    var update = function (collection) {
      var proto = collection && collection.prototype;
      if (proto) {
        if (iterator && !proto[Symbol.iterator]) {
          Object.defineProperty(proto, Symbol.iterator, {
            value: iterator,
            writable: true,
            configuration: true,
          });
        }
        if (forEach && !proto.forEach) {
          Object.defineProperty(proto, "forEach", {
            value: forEach,
            writable: true,
            configuration: true,
          });
        }
      }
    };

    if (typeof NodeList !== "undefined") {
      update(NodeList);
    }

    if (typeof HTMLCollection !== "undefined") {
      update(HTMLCollection);
    }
  }
})();

브라우저에 따라 collection 의 prototype 에 iterator 가 구현이 안되있거나, forEach 가 구현되어 있지 않다면, polyfill 을 통해 구현해주는 내용이다.

제너레이터 함수

  • 작업 중간에 일시 정지가 가능하다.
    • 멋진 점은 일시정지된 상태로 진행된 작업 정보를 기억하고 있는 것이다.
  • 값을 생성하고 선택적으로 새 값을 받아들인 다음 필요한 만큼 계속 진행할 수 있다.
  • 내부적으로 제너레이터 함수는 제너레이터 객체를 만들고 반환한다.
  • 이터레이터는 값만 생성하는 반면, 제너레이터는 값을 생성하고 소비할 수 있다.
  • 제너레이터 객체 수동 생성도 가능하지만 보통 function * 문법으로 단순화하여 생성한다.

기본 제너레이터 함수 생성하기

function* simple() {
  for (let n = 1; n <= 3; ++n) {
    yield n;
  }
}

const it = simple();
it.next(); // {value: 1, done: false}
it.next(); // {value: 2, done: false}

for (const e of simple()) {
  console.log(e); // 1, 2, 3
}

console.log(it[Symbol.iterator]());

/*
simple {<suspended>}
[[GeneratorLocation]]: VM43:1
[[Prototype]]: Generator
[[GeneratorState]]: "suspended"
[[GeneratorFunction]]: ƒ* simple()
[[GeneratorReceiver]]: Window
*/
  • simple() 의 결과는 제너레이터 객체이다.
    • next() 메서드를 수행하면, yield 로 값을 반환할 때까지 코드를 수행한다.
    • yield 를 만나지 못하면, {value: undefined, done: true} 를 반환한다.

제너레이터로 iterator 구현하기

const a = {
  0: "a",
  1: "b",
  2: "c",
  length: 3,
  [Symbol.iterator]: function* () {
    for (let index = 0; index < this.length; ++index) {
      yield this[index];
    }
  },
};

for (const value of a) {
  console.log(value);
}
  • 제너레이터를 사용하지 않고 구현하는 것보다 훨씬 간단하다.

LinkedList 클래스의 iterator 를 제너레이터 이용해서 구현해보기

class LinkedList {
  constructor() {
    this.head = this.tail = null;
  }

  add(value) {
    const entry = { value, next: null };

    if (!this.tail) {
      this.head = this.tail = entry;
    } else {
      this.tail = this.tail.next = entry;
    }
  }

  *[Symbol.iterator]() {
    for (let cur = this.head; cur !== null; cur = cur.next) {
      yield cur.value;
    }
  }
}

const list = new LinkedList();
list.add("one");
list.add("two");
list.add("three");

for (const e of list) {
  console.log(e);
}
  • *[Symbol.iterator](){ ... } 메서드명 앞에 * 을 붙여주는 방법을 사용할 수 있다.

제너레이터로 값 소비하기

  • 제너레이터는 이터레이터와 다르게 '값을 소비할 수 있다.'
  • 값을 소비한다는 것은 next() 메서드에 값을 받아 로직에 이용한다는 뜻이다.
function* tellMeAboutYou() {
  const name = yield "what is your name?";
  console.log(`oh your name is ${name}`);
  const age = yield "how old are you?";
  console.log(`oh now you are ${age}`);
}

const gt = tellMeAboutYou();
console.log(gt.next());
// what is your name?
console.log(gt.next("jake seo"));
// oh your name is jake seo
// how old are you?
gt.next("5");
// oh now you are 5
  • 처음부터 무언가 값을 넘기고 싶다면, 제너레이터 함수의 인자를 이용하자.

제너레이터의 용도

  • 기본적으로 상태머신이다.
  • 사용자로부터 값을 무한대로 입력받고, 무조건 마지막 세 수의 합을 구하는 경우 유용하다.
  • 심리테스트 등 사용자가 입력한 정보 값을 기반으로 다음 값을 결정해야 하는 경우 유용하다.
function* sumLastThree() {
  let arr = [];

  while (true) {
    if (arr.length < 3) {
      const num = yield `input ${3 - arr.length} more numbers`;
      arr.push(num);
      continue;
    }

    const num = yield arr.reduce((a, c) => a + c, 0);
    arr = [...arr.slice(-2), num];
  }
}

const gt = sumLastThree();

console.log(gt.next()); // {value: 'input 3 more numbers', done: false}
console.log(gt.next(1)); // {value: 'input 3 more numbers', done: false}
console.log(gt.next(2)); // {value: 'input 3 more numbers', done: false}
console.log(gt.next(3)); // {value: 6, done: false}
console.log(gt.next(4)); // {value: 9, done: false}
console.log(gt.next(100)); // {value: 107, done: false}

제너레이터의 return

function* usingReturn() {
  yield 1;
  yield 2;
  return 3;
}

for (const v of usingReturn()) {
  console.log(v);
}
// 1
// 2
// NOT LOGGED -> {value: 3, done: true}
  • 제너레이터의 return 은 {done: true} 인 오브젝트를 반환한다.
  • for-of 문은 done: true 인 경우의 value 는 보지 않기 때문에, 출력되지 않는다.

yield 의 낮은 우선 순위

function* example() {
  let a = yield +2 + 30;
  return a;
}
  • yield 의 낮은 우선순위 때문에, yield 로 받은 값에 2 와 30 이 더해지는 것이 아니라, +2+30 이 먼저 계산되고 let a = yield 32 와 같이 해석된다.
function* example() {
  let a = yield* 2 + 30;
  return a;
}
  • *2+30 은 올바른 연산이 아니므로 에러가 발생한다.
function* example() {
  let a = yield 2 + 30 + yield;
  return a;
}
  • 아예 문법 에러가 난다.
function* example() {
  let a = (yield) + 2 + 30;
  return a;
}
  • 괄호에 있는 것이 먼저 해석되기 때문에, 이는 올바른 문법이다.
  • yield 의 해석을 먼저 진행한다.

제너레이터의 throw

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

const gen = example();
console.log(gen.next()); // {value: 1, done: false}
console.log(gen.throw(new Error("boom"))); // Uncaught Error: boom
console.log(gen.next()); // not executed.
function* example() {
  while (true) {
    try {
      yield 1;
      yield 2;
      yield 3;
    } catch (e) {
      console.error("generator boom!");
      yield "error";
    }
  }
}

const gen = example();
console.log(gen.next()); // {value: 1, done: false}
console.log(gen.throw(new Error("boom"))); // {value: 'error', done: false}
console.log(gen.next()); // {value: 1, done: false}

제너레이터와 이터러블 넘겨주기: yield*

function* collect(count) {
  const data = [];
  if (count < 1 || Math.floor(count) !== count) {
    throw new Error("count must be an integer >= 1");
  }

  do {
    let msg = "values needed: " + count;
    data.push(yield msg);
  } while (--count > 0);

  return data;
}

function* outer() {
  let data1 = yield* collect(2);
  console.log("data collected by collect(2) =", data1);
  let data2 = yield* collect(3);
  console.log("data collected by collect(3) =", data2);
  return [data1, data2];
}

const outerG = outer();
console.log("next got:", outerG.next());
console.log("next got:", outerG.next("a"));
console.log("next got:", outerG.next("b"));
console.log("next got:", outerG.next("c"));
console.log("next got:", outerG.next("d"));
console.log("next got:", outerG.next("e"));
  • yield* 키워드를 통해 제너레이터를 넘겨주었다.
    • 이를 이용해 내부의 다른 제너레이터 상태머신을 이용할 수 있다.

내부 제너레이터 실행 중 return 하기

function* inner() {
  try {
    let n = 0;
    while (true) {
      yield "inner " + n++;
    }
  } finally {
    console.log("inner terminated");
  }
}

function* outer() {
  try {
    yield "outer before";
    yield* inner();
    yield "outer after";
  } finally {
    console.log("outer terminated");
  }
}

const gen = outer();
let result = gen.next();

console.log(result);

result = gen.next();
console.log(result);
result = gen.next();
console.log(result);
result = gen.return(42);
console.log(result);
result = gen.next();
console.log(result);

/*
{value: 'outer before', done: false}
{value: 'inner 0', done: false}
{value: 'inner 1', done: false}
inner terminated
outer terminated
{value: 42, done: true}
{value: undefined, done: true}
*/

return 예제 2: 바로 return 하면?

function foo() {
  try {
    return "a";
  } finally {
    return "b";
  }
}

console.log(foo()); // b
  • return 을 하는 순간 finally() 로 넘어가기 때문에 바로 "b" 가 반환된다.

return 예제 3: return 으로 값 오버라이드

function* foo(n) {
  try {
    while (true) {
      n = yield n * 2;
    }
  } finally {
    return "override";
  }
}

const gen = foo(2);
console.log(gen.next()); // {value: 4, done: false}
console.log(gen.next(3)); // {value: 6, done: false}
console.log(gen.next(4)); // {value: 8, done: false}
console.log(gen.return(4)); // {value: "override", done: true}

throw 해보기

function* inner() {
  try {
    yield "something";
    console.log("inner - done");
  } finally {
    console.log("inner - finally");
  }
}

function* outer() {
  try {
    yield* inner();
    console.log("outer - done");
  } finally {
    console.log("outer - finally");
  }
}

const gen = outer();
let result = gen.next();
result = gen.throw(new Error("boom"));

/*
inner - finally
outer - finally
Uncaught Error: boom
*/

액션 플랜

이터러블 소비 구문을 활용해보자

  • index 가 필요하다면, 기존의 for 문을 사용하자.
  • index 가 필요 없으면, for-of 를 사용하자.
  • 콜백을 여러번 활용할 필요가 있다면, forEach 도 좋은 선택이다.

DOM 컬렉션 반복 기능 사용

  • for-of 를 통해 DOM 객체를 반복해보자.
  • Array.prototype.slice.call() 과 같은 메서드로 DOM 을 바인딩해 Array 의 메서드를 이용하는 것도 가능하다.

이터러블, 이터레이터 인터페이스 사용

  • 새 클래스나 객체에 반복이 필요한 경우, [Symbol.iterator]() 를 정의해보자.

이터러블 스프레드 구문을 사용하자

  • Math.max.apply("", [1, 2, 3, 1111]); 는 가독성이 좋지 않다.
  • Math.max(...[1, 2, 3, 1111]); 이 더 가독성이 좋다.

제너레이터 사용하기

  • 상태 머신이 필요한 경우 제너레이터를 사용했을 때, 더 명확한 코드를 작성할 수도 있다.
  • 단, 코드 흐름 문법으로 더 잘 모델링되지 않은 상태 머신인 경우에는 제너레이터가 올바른 선택이 아닐 수도 있다.
반응형
저작자표시 비영리

'자바스크립트 > 모던 자바스크립트' 카테고리의 다른 글

모던 자바스크립트, async await  (0) 2023.02.28
모던 자바스크립트, 제너레이터 (Generator)  (0) 2023.02.28
모던 자바스크립트, 프라미스 2 - 유틸 메서드와 작업 패턴 그리고 안티 패턴  (0) 2023.02.15
모던 자바스크립트, 프라미스 1 - 기본 개념  (0) 2023.02.15
모던 자바스크립트, 디스트럭처링 (Desctructuring)  (0) 2023.02.08
    '자바스크립트/모던 자바스크립트' 카테고리의 다른 글
    • 모던 자바스크립트, async await
    • 모던 자바스크립트, 제너레이터 (Generator)
    • 모던 자바스크립트, 프라미스 2 - 유틸 메서드와 작업 패턴 그리고 안티 패턴
    • 모던 자바스크립트, 프라미스 1 - 기본 개념
    Jake Seo
    Jake Seo
    ✔ 잘 보셨다면 광고 한번 클릭해주시면 큰 힘이 됩니다. ✔ 댓글로 틀린 부분을 지적해주시면 기분 나빠하지 않고 수정합니다. ✔ 많은 퇴고를 거친 글이 좋은 글이 된다고 생각합니다. ✔ 간결하고 명료하게 사람들을 이해 시키는 것을 목표로 합니다.

    티스토리툴바