티스토리 뷰





반응형

이터레이터

이터레이터는 '지금 어디 있는지' 파악할 수 있도록 돕는다는 면에서 일종의 책갈피와 비슷한 개념입니다. 배열은 이터러블 객체의 좋은 예입니다. 책에 여러 페이지가 있는 것처럼 배열에는 여러 요소가 들어 있으므로, 책에 책갈피를 끼울 수 있듯 배열에는 이터레이터를 사용할 수 있습니다. 책과 책갈피의 비유를 계속 사용해 봅시다.

 

book이란 배열이 있고, 이 배열의 각 요소는 책의 한 페이지를 나타내는 문자열이라고 합시다. 지면을 생각해서 루이스 캐럴의 이상한 나라의 앨리스에서 발췌한 '반짝 반짝 작은 박쥐' 원문을 사용하겠습니다. 한 페이지에 문장 하나씩만 들어 있는 어린이용 동화책을 상상해 보세요

const book = [
  "반짝 반짝 작은 별",
  "아름답게 비치네",
  "서쪽 하늘에서도",
  "동쪽 하늘에서도",
  "반짝 반짝 작은 별",
  "아름답게 비치네",
];

이제 book 배열에 values 메서드를 써서 이터레이터를 만들 수 있습니다.

이터레이터(보통 it으로 사용)는 책갈피지만, 이 책에만 사용할 수 있습니다. 그리고 아직은 책갈피를 꼿을 수 없습니다. 읽지 않았으니까요. '읽기 시작' 하려면 이터레이터의 next메서드를 호출합니다. 이 메서드가 반환하는 객체value프로퍼티(지금 보이는 페이지)done프로퍼티(마지막페이지를 읽으면 true로 바뀌는)가 있습니다.

it.next();	//{value: "반짝 반짝 작은 별", doen:false}
it.next();	//{value: "아름답게 비치네", doen:false}
it.next();	//{value: "서쪽 하늘에서도", doen:false}
it.next();	//{value: "동쪽 하늘에서도", doen:false}
it.next();	//{value: "반짝 반짝 작은 별", doen:false}
it.next();	//{value: "아름답게 비치네", doen:false}
it.next();	//{value: "undefined", doen:true}
it.next();	//{value: "undefined", doen:true}
it.next();	//{value: "undefined", doen:true}

여기서 중요한 점은 마지막 페이지를 반환했다 해서 끝난 것은 아니랍니다. 책의 비유로는 조금 맞지 않는 부분입니다. 책의 마지막 페이지를 읽었다면 다 읽은 거지만, 이터레이터는 책을 읽는 것보다 훨씬 다양한 상황에 쓰일 수 있고 끝나는 시점을 간단히 결정할 수는 없습니다. 더 진행할 것이 없으면 value는 undefined지만 next()는 계속 호출 할 수 있습니다. 그리고 일단 이터레이터가 끝까지 진행하면 뒤로 돌아가서 다른 데이터를 제공할 수는 없습니다. 이 예제에서 묘사하지는 않았지만 it.next()를 호출하는 중간에 다른일을 할 수 있습니다.

for와 for...of

for루프는 인덱스를 사용해서 해당하는 배열 요소에 접근하여 순회를 돌지만 for...of는 어떻게 동작하는 걸까요? 인덱스 없이 어떻게 돌 수 있느냐면 바로 이터레이터 때문입니다. 이터레이터만 제공할 수 있다면 무엇이든 for..of와 함께 쓸 수 있습니다. 이터레이터와 while문을 사용해서 for..of 흉내를 내봅시다.

const it = book.values();
let current = it.next();
while(!current.done){
  console.log(current.value);
  current = it.next();
}

이터레이션 프로토콜

이터레이터 그 자체로는 크게 쓸모가 있다기보다는, 더 쓸모가 있는 동작이 가능하도록 한다는데 의미가 있습니다. 이터레이터 프로토콜은 모든 객체를 이터러블 객체로 바꿀 수 있습니다. 메시지에 타임스탬프를 붙이는 로그 클래스가 필요하다고 생각해 봅시다. 내적으로 타임스탬프가 붙은 메시지는 배열에 저장합니다.

class Log {
  constructor(){
    this.messages = [];
  }
  add(message){
    this.messages.push({message, timestamp: Date.now() });
  }
}

지금까진 좋습니다만, 로그를 기록한 항목을 순회 하고 싶다면 어떻게 해야할까요? 물론 log.messages에 접근할 수는 있지만, log를 배열 조작하듯 할 수 있다면 더 좋을 겁니다. 이터레이션 프로토콜을 사용하면 가능합니다. 이터레이션 프로토콜은 클래스에 심볼 메서드 Symbol.iterator가 있고 이 메서드가 이터레이터처럼 동작하는 객체, 즉 value와 done프로퍼티가 있는 객체를 반환하는 next 메서드를 가진 객체를 반환한다면 그 클래스의 인스턴스는 이터러블 객체라는 뜻입니다. Log 클래스에 Symbol.iterator 메서드를 추가합시다.

class Log {
  constructor(){
    this.messages = [];
  }
  add(message){
    this.messages.push({message, timestamp: Date.now() });
  }
  [Symbol.interator]() {
    return this.messages.values();
  }
}

이제 Log 인스턴스를 배열처럼 순회할 수 있습니다.

const log = new Log();
log.add("first");
log.add("spotted");

for(let entry of log) {
  console.log(`${entry.message} @${entry.timestamp}`);
}

제너레이터

제너레이터란 이터레이터를 사용해 자신의 실행을 제어하는 함수입니다. 일반적인 함수는 매개변수를 받고 값을 반환하지만, 호출자는 매개변수 외에는 함수의 실행을 제어할 방법이 전혀 없습니다. 함수를 호출하면 그 함수가 종료될 때까지 제어권을 완전히 넘기는 겁니다. 하지만 제너레이터에서는 그렇지 않습니다.

 

제너레이터는 두 가지 새로운 개념을 도입했습니다. 하나는 함수의 실행을 개별적 단계로 나눔으로써 함수의 실행을 제어한다는 것입니다. 다른 하나는 실행중인 함수와 통신하는 것 입니다.

  • 제너레이터는 언제든 호출자에게 제어권을 넘길(yield) 수 있습니다.
  • 제너레이터는 호출한 즉시 실행되지 않습니다. 대신 이터레이터를 반환하고, 이터레이터의 next메서드를 호출함에 따라 실행됩니다.

제너레이터를 만들 때는 function 뒤에 애스터리스트( * )를 붙입니다. 이것을 제외하면 문법은 일반적인 함수와 같습니다. 제너레이터에서 return 외에 yield 키워드를 쓸 수 있습니다. 다음은 무지개 색깔을 반환하는 제너레이터를 만듭시다.

function* rainbow(){
  yield 'red';
  yield 'orange';
  yield 'yellow';
  yield 'green';
  yield 'blue';
  yield 'indigo';
  yield 'violet';
}

다음과 같은 방식으로 값을 반환할 수 있습니다.

const it = rainbow();
it.next();
it.next();
...

for(let color of rainbow()){
  console.log(color);
}

yield 표현식과 양방향 통신

제너레이터와 호출자 사이에 양방향 통신이 가능하다는 것은 이미 언급했습니다. 통신은 yield 표현식을 통해 이뤄집니다. 표현식은 값으로 평가되고 yield는 표현식이므로 반드시 어떤 값으로 평가됩니다. yield 표현식의 값은 호출자가 제너레이터의 이터레이터에서 next를 호출할 때 제공하는 매개변수 입니다. 대화를 이어가는 제너레이터를 만들어봅시다.

function* interrogate(){
  const name = yield 'what your name?';
  const color = yield 'what is your favorite color?';
  return `${name}'s favorite color is ${color}`;
}

next를 호출하면 제너레이터는 첫 번째 행을 실행하려 합니다. 하짐나 그 행에는 yield 표현식이 들어 있으므로 제너레이터는 반드시 제어권을 호출자에게 넘겨야 합니다. 제너레이터의 첫 번째 행이 완료 되려면 호출자가 다시 next를 호출해야 합니다. 그러면 name은 next에서 전달하는 값을 받습니다.

const it = interrogate();
it.next();
it.next('Ethan');
it.next('orange');

제너레이터와 return

yield문은 설령 제너레이터의 마지막 문이더라도 제너레이터를 끝내지 않습니다. 제너레이터에서 return 문을 사용하면 그 위치와 관계 없이 done은 true가 되고, value 프로퍼티는 return이 반환하는 값이 됩니다.

function* abc(){
  yield 'a';
  yield 'b';
  return 'c';
}
const it = abc();
it.next();
it.next();
it.next();

이런 동작 방식이 정확하기는 하지만, 제너레이터를 사용할 때는 보통 done이 true면 value에 주의를 기울이지 않습니다. 예를 들어 이 제너레이터를 for..of에 사용하면 c는 절대 출력되지않습니다. 제너레이터에서 중요한 값을 return으로 반환하려 하지 마십시오. 제너레이터가 반환하는 값을 사용하려할 때는 yield를 써야하고, return은 제너레이터를 중간에 종료하는 목적으로만 사용해야합니다.

 

 

 

 

 

 

반응형
댓글
반응형
최근에 달린 댓글
글 보관함
Total
Today
Yesterday
최근에 올라온 글
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31