프로필사진
움짤로 보는 자바스크립트 동작 원리(4) - Generators and Iterators

2020. 5. 19. 09:33🔴 FE/JavaScript

300x250

원문 (영어) : https://dev.to/lydiahallie/javascript-visualized-generators-and-iterators-e36
저자 : Lydia Hallie

👩🏻‍💻Thanks to Lydia for this amazing article about Javascript!


ES6은 generator functions(제너레이터 함수)를 소개했다.
generator functions는 무엇일까?
먼저 기존에 자주 썼었던 함수를 살펴보자.

이 코드에는 확실히 뭔가 특별하지는 않다.
그저 4번 로그를 찍는 평범한 함수일 뿐이다. 이를 호출한다면,

이렇게 나올 것이다.

"하지만 왜 당신은 나의 5초의 시간을 이 지루하고 평범한 함수를 보는 것에 낭비하게 만들었나요?"라고 물을 수 있다.
(으억 엄청난 번역투..😢)

보통의 함수들은 run-to-completion 모델을 따른다 :
우리가 함수를 호출하면, 이는 끝날때까지 동작할 것이다(에러가 발생하지 않는 이상..)
따라서 우리는 중간에 함수를 랜덤하게 멈추게 할 수 없다.

여기 흥미로운 사실이 있다 : generator functions는 run-to-completion 모델을 따르지 않는다!
👉🏻이말은 즉, generator function이 돌아가고 있을 때 우리가 랜덤 하게 스톱시킬 수 있다는 뜻인가?
뭐, 어느정도 맞다.

generator functions가 무엇인지, 우리가 어떻게 사용할 수 있는지 알아보자.


우리는 'function'키워드 뒤에 '*'를 써서 generator function을 생성할 수 있다.

하지만 generator functions를 사용하기 위해서 우리가 해야 하는 게 이게 다가 아니다!
generator functions는 사실 보통의 함수들과 완전히 다른 방식으로 돌아간다:

- generator function을 호출하면 generator object을 리턴한다 (= iterator)
- yield라는 키워드를 generator function에 사용해서 실행을 멈추게 할 수 있다.

이것이 도대체 무슨 뜻일까?

먼저 첫번째 항목부터 보자 : generator function을 호출하면 generator object을 리턴한다
우리가 보통의 함수를 호출하면, 그 함수의 body는 실행이 되고, 마지막으로 값을 리턴한다.
하지만, 만약 우리가 generator function을 호출한다면, generator object이 리턴된다!

로그를 통해 어떻게 생겼는지 살펴보자

당신이 속으로 소리 지르는 것을 들을 수 있을 것 같다, 이게 조금 부담스럽게 보이기 때문이지!
하지만 걱정하지 말길, 우리는 여기 로그에 찍힌 속성들을 하나도 쓸 필요가 없다.
그렇다면 generator object의 장점이 무엇일까?

먼저 우리는 다시 돌아가서, 두 번째 항목에 대해 답해야 한다.
일반 함수와 generator 함수의 차이 : yield라는 키워드를 generator function에 사용해서 실행을 멈추게 할 수 있다.

generator functions에서 아래와 같이 작성할 수 있다 (genFunc = generatorFunction)

그렇다면 yield 키워드는 무슨 역할을 할까?
generator를 실행하던 중에 yield라는 키워드를 만나면 실행이 멈춘다.
이것의 장점은 다음에 우리가 해당 함수를 실행할 때, 이전에 어디에서 멈췄는지 기억하고 거기서부터 실행한다는 것이다.
기본적으로 이렇게 실행이 된다 :

1. 처음 실행이 될 때, 첫번째 라인에서 멈추고 '✨'이라는 문자열을 yield 한다.
2. 두 번째 실행이 될 때, 일전에 멈췄던 yield 키워드에서부터 시작한다. 그리고 밑으로 실행이 되면서 두번째 yield 키워드를 마주치고, '💕'라는 값을 yield 한다.
3. 세 번째 실행이 될 때, 직전의 yield 키워드에서부터 시작한다. 밑으로 가면서 return 키워드를 마주하고, 'Done!'이라는 값을 리턴한다.

우리는 앞전에 generator function을 호출하면 generator object을 리턴하는 것을 봤는데, 그렇다면 우리는 함수를 어떻게 호출해야 하는 것일까?
👉🏻바로 generator object가 나설 차례다!

generator object에는 next method(prototype chain안에)이 있다.
이 메소드로 우리는 generator object를 반복해서 사용할 것이다.(iterate)
하지만 앞전에 해당 함수가 값을 yield 한 뒤 남긴 상태를 기억하기 위해선 generator object을 변수에 할당해야 한다.
저자는 이것을 genObj(generatorObject)라고 부를 것이다.

역시, 앞에서 우리가 봤던 object의 모습을 볼 수 있다.
만약 우리가 next 메소드를 genObj에서 호출하면 어떻게 되는지 보자.

generator은 첫 번째 yield 키워드를 만나기 전까지 실행된다. (첫 번째 라인)
그리고 value 속성과 done 속성을 가지고 있는 객체를 yield 한다.

{ value : ... , done : ... }

value 속성은 우리가 yield 한 값과 동일하다.
done 속성은 boolean 값이며, generator function이 값을 리턴할 때만(yield가 아님) true로 세팅되게끔 했다.

그리고 우리는 generator가 반복되는 것을 멈추게 했다.
이건 마치 해당 함수가 멈춘 것처럼 보인다!

next 메소드를 다시 한번 호출해보자

먼저, 'First log!'라는 로그가 콘솔에 찍혔다.
이것은 yield나 return 키워드가 아니기에 실행이 멈추지 않는다.
그리고 '💕'라는 값을 가진 yield 키워드를 마주쳤다. 객체는 '💕'속성과 done속성의 값을 yield 하게 된다.
아직 generator가 리턴한게 없으므로 done속성의 값은 false이다.

거의 다 왔다! next 메소드를 마지막으로 한번 더 호출해보자

'Second log!'라는 문자열 값을 로그에 찍었다. 그리고 'Done!'이라는 값을 가지고 있는 return 키워드를 만났다. 
그리고 'Done!'이라는 value 속성을 가진 객체가 리턴된다.
우리는 실제로 리턴을 받았기 때문에, done의 값은 true로 세팅된다.

done 속성은 사실 굉장히 중요하다.
우리는 generator object를 한 번만 반복할 수 있다.

읭? 그러면 next 메소드를 한번 더 호출하면 무슨 일이 일어날까?

간단하게 undefined를 계속 리턴받을 것이다.
새로운 generator object를 생성해야지만 다시 반복할 수 있다.


이제까지 우리는 generator function이 반복자(iterator)를 리턴한다는 것을 보았다.
잠깐.. 반복자라면 리턴받은 객체에 for of 루프나 spread operator를 사용할 수 있다는 얘기인가?
👉🏻Yaaaaas!!

[... ] 문법을 써서 yield 된 값들을 배열로 spread 해보자

아니면 for of 루프를 사용해 볼 수 있다.

사용 가능한 것들이 너무 많다!


반복자를 반복자로 만드는 것은 무엇일까?

왜냐하면 우리는 for-of 루프와 spread 문법을 배열, 문자열, map, set에 사용할 수 있기 때문이다.
사실 그들은 iterator 프로토콜을 가지고 있기 때문이다 - [Symbol.iterator].

우리가 아래의 값들을 가지고 있다고 가정해보자

배열, 문자열, generatorObject 모두 반복자이다!
그렇다면 그들이 가지고 있는 [Symbol.iterator] 속성의 값을 살펴보자

하지만 반복적이지 않은 값들에 대한 [Symbol.iterator]의 값은 어떠할까?

맞다, 존재하지 않다.
그렇다면 단순히 [Symbol.iterator] 속성을 직접 추가해서 반복적이지 않은 것을 반복적이게 만들 수 있을까?
Yes, we can !!

[Symbol.iterator]은 next 메소드를 포함한 반복자를 리턴해야한다.
우리가 아까 본 { value : '...' , done : false/true } 이러한 객체들을 리턴해야한다는 것이다.

간단하게 우리는 [Symbol.iterator]의 값을 generator function과 동일하도록 세팅할 수 있다.
이렇게 된다면 이는 디폴트로 반복자를 리턴할 것이다.

객체를 반복적이게 만들고 객체 전체를 yield 하게 만들어보자

해당 객체에 spread 문법이나 for-of 루프를 썼을 때 어떤 일이 일어나는지 보자

만약 객체의 키값만 받고 싶다면 this 대신 Object.keys(this)를 yield 하면 된다.

Object.keys(this)는 배열이기 때문에 yield 된 값들은 배열이다.
그렇다면 우리는 이 배열을 다른 배열로 spread 하여 nested array로 만들 수 있다.
하지만 우리가 원한 건 이런 것이 아니다. 우리는 단순히 개별적인 key 값을 받고 싶은 것이다.

다행히도 우리는 generator 안에서 yield* 키워드를 통해 반복자의 개별적인 값들을 가져올 수 있다.
generator function 안에서 첫 번째로 🥑를 yield 한다고 치자.
그다음에 우리는 다른 반복자(여기서는 배열)에 있는 값들을 하나씩 yield 하길 원한다.
우리는 yield* 키워드를 사용해서 다른 generator에게 위임할 수 있다.

genObj 반복자를 반복하기 전에 위임받은 generator의 값들은 하나씩 yield 된다.

이게 바로 모든 객체의 key들을 하나씩 가져오기 위해 우리가 해야 하는 것이다!


generator functions의 다른 사용법은 이를 observer function(와 같이..)으로 사용하는 것이다.
generator은 들어오는 데이터를 기다릴 수 있다.
그리고 그 데이터가 들어온 다음에 그 데이터를 실행할 것이다.

예를 들면

여기서 가장 큰 차이점은 이전 예제처럼 yield [value]를 할 필요 없다는 것이다.
대신에 우리는 second라는 값을 배정하고, 문자열 'First!'를 yield 한다.
이 값이 바로 우리가 next 메소드를 처음 호출했을 때 yield받을 값이다.

iterable에 next 메소드를 처음 호출했을 때 어떤 일이 일어나는지 보자

첫 번째 라인에서 yield를 마주치자 'First!'라는 값을 yield 했다.
그렇다면 second 변수의 값은 무엇일까?
그 값은 바로 우리가 next 메소드에 전달한 값이다!

이번에는 'I like JavaScript' 문자열을 전달해보자

여기서 중요한 것은, 처음 next 메소드를 호출했을 때 어떠한 input도 주지 않았다는 것이다.
우리는 단순하게 처음에 next 메소드를 호출하면서 observer를 시작했다.
generator은 계속해서 실행되기 전까지 우리의 input을 기다리고, 우리가 next 메소드에 전달한 값을 처리할 수 있다.


그렇다면 왜 당신은 generator functions를 사용하고 싶은 것인가?

제일 큰 장점 중 하나는 바로 generators가 느긋하게 계산되기(lazily evaluated) 때문이다.
즉, next 메소드를 호출해서 리턴받은 값은 우리가 명확히 요구할 때만 산출된다는 뜻이다!
보통의 함수들은 이런 기능이 없다, 그래서 모든 값들이 생성된다. (나중에 언젠간 해당 값이 사용된다는 경우를 위해)

다른 generator 사용법들도 많지만 저자는 주로 큰 데이터셋을 반복할 때 이를 컨트롤하기 위해 사용한다.

북 클럽 list를 가지고 있다고 상상해보자.
짧고 크지 않은 코드를 유지하기 위해 각 북 클럽은 한 명의 멤버만 있어야 한다.
각 멤버는 여러 권의 책을 읽고 있고, 이는 books라는 배열로 표현된다.

우리는 ey812라는 아이디를 가진 책을 찾고 있다.
그것을 찾기 위해서 우리는 아마 곂쳐진 for-leep나 forEach를 사용할 수도 있을 것이다.
하지만 이는 곧 우리가 찾고 있는 팀 멤버를 찾은 뒤에도 데이터를 반복하고 있을 수도 있다는 뜻이다.

generators의 놀라운 점은 우리가 명령하기 전까지 계속 돌지 않는다는 것이다.
이 말은 즉, 리턴된 아이템들을 하나씩 살펴보고, 만약 찾던 아이템이 나오면 단순히 next를 호출하지 않으면 된다는 것이다!

이것이 어떤 모습인지 살펴보자

먼저, 각 팀 멤버의 books 배열을 반복하는 gernerator를 생성한다.
해당 배열을 함수에 전달하고, 배열을 반복해서 각 책을 yield 한다.

그리고 우리는 clubMembers 배열을 반복하는 generator를 생성한다.
클럽 멤버 자체는 신경 쓸 필요는 없으며 그들의 책들만 반복하면 된다.
iterateMembers generator에서 iterateBooks 반복자에게 위임하여 그들의 책들을 yield 한다.

거의 다 왔다! 마지막 단계는 북클럽 들을 반복하는 일이다.
이전 예제와 같이 우리는 북클럽 자체를 신경 쓸 필요는 없고 클럽 멤버들만(특히 그들의 책들) 신경 쓰면 된다.
iterateClubMembers 반복자에게 위임하고 clubMembers 배열을 전달하자.

모두 반복하기 위해서는 iteraterBookClubs generator에 bookClub 배열을 전달하여 generator object를 반복할 수 있도록 해야 한다.
여기서 저자는 generator object를 it이라고 부르겠다.

ey812라는 아이디를 가진 책을 찾을 때까지 next 메소드를 호출해보자

우리는 해당 책을 찾기 우해서 모든 데이터를 반복해서 돌릴 필요가 없다. 대신, 우리는 필요한 만큼만 데이터를 살펴보면 되는 것이다!
next 메소드를 수동으로 매번 호출하는 것은 당연히 효율적이지 않다.
그러므로 함수를 만들어서 사용해보자!

우리가 찾고 있는 책의 id를 함수에 전달한다.
만약 value.id가 우리가 찾고 있는 그 id라면 간단하게 전체 value(book object)를 리턴한다.
만역 id가 맞지 않다면 next를 다시 호출한다.

현재 이건 적은 양의 데이터지만, 이제 수백 개 ~ 수천 개의 데이터이거나 하나의 value를 찾기 위해 쉬지 않고 들어오는 stream을 잘라야 한다고 상상해봐라.
보통의 경우에는 parsing을 하기 위해 모든 데이터셋이 준비된 상태가 될 때까지 기다려야 한다.

generator functions를 사용하면 우리는 적은 양의 데이터를 위해 데이터를 체크하고 next 메소드를 호출할 때만 값을 만들어 낼 수 있다.

300x250