원문 : 💡🎁 JavaScript Visualized: Generators and Iterators
ES6는 generator functions를 도입했습니다 🎉. 제가 사람들에게 제너레이터 함수에 대해 물어보면, 보통 이런 반응을 보입니다.
"한 번 써봤는데 어려워서 다신 쓰지 않는다.", "제너레이터에 대한 블로그 글을 많이 봤지만 여전히 이해가 가지 않는다.",
"이해는 하지만 그런 왜 써? 🤔" 저도 오랫동안 이러한 생각을 가지고 있었지만, 제너레이터는 사실 꽤 멋진 녀석입니다.
그렇다면, 제너레이터 함수는 무엇일까요? 먼저 일반적인 구식 기능을 살펴보겠습니다.👵🏼
특별한건 없습니다! 단지 값을 4번 기록하는 일반적인 함수일 뿐입니다. 호출해봅시다!
"하지만 리디아, 왜 이런 평범하고 지루한 함수로 내 시간을 5초 낭비시켰나요?", 좋은 질문입니다. 보통 함수는 run-to-completion 모델이라는 것을 따릅니다.
우리가 함수를 호출하면, 이 함수는 오류가 없는 한 항상 완료될 때까지 실행됩니다. 우리는 함수 내부에 원하는 곳에서 아무렇게나 함수를 정지시킬 수 없습니다.
제너레이터 함수는 run-to-completion모델을 따르지 않습니다! 이건 우리가 원하는 곳에서 함수를 정지시킬 수 있다는 얘기일까요?
음, 부분적으로? 제너레이터 함수가 무엇이고, 어떻게 사용하는지 살펴봅시다.
우리는 function 키워드 뒤에 *을 붙여 제너레이터 함수를 만듭니다.
하지만 제너레이터 함수를 사용하기 위해서 그게 전부는 아닙니다. 이 함수는 일반 함수와 전혀 다른 방식으로 작동하거든요.
*
제너레이터 함수는 제너레이터 객체를 반환하고, 이 객체는 iterator입니다.
*
yield 키워드를 사용해서 제너레이터 함수의 실행을 정지시킬 수 있습니다.
이게 무슨 뜻일까요!?
첫 번째, "제너레이터 함수는 제너레이터 객체를 반환한다" 부터 살펴봅시다. 우리가 일반적인 함수를 호출하면 함수 본문이 실행되고,
결과로 값이 반환됩니다. 하지만 제너레이터 함수를 호출하면 제너레이터 객체가 반환됩니다!
반환된 값을 기록할 때 어떻게 표시되는지 살표봅시다.
이게 좀 압도적으로 보여서 여러분이 비명을 지르는 소리가 들리는거 같네요 🙃. 하지만 걱정하지마세요.
이 로그에 찍히는 속성을 사용할 필요는 없으니까.. 그렇다면 제너레이터 객체는 무엇에 좋은 걸까요?
먼저 우리는 한 걸음 물러서서 제너레이터 함수와 일반 함수의 두 번째 차이점에 답을 해야합니다.
@yield@ 키워드를 사용해서 제너레이터 함수의 실행을 정지시킬 수 있습니다.
yield 키워드가 무엇을 하고 있나요? 제너레이터의 실행은 yield를 만나면 "일시중지" 됩니다.
가장 좋은 점은 다음 번에 이 함수를 실행할 때 이전에 정지되었던 위치를 기억해서,
그 다음부터 실행된다는 것입니다!! 😃 기본적으로 무슨 일이 일어나냐면(걱정마세요, 이에 대한 애니메이션은 뒤에 있습니다)
- 먼저 처음 실행될 때, 첫번째 줄에서 "정지"하고 문자열 값 '✨'를 내보냅니다(yield).
- 두 번째 실행될 때, 이전 yield 키워드의 라인부터 실행합니다. 다음 두 번째 yield까지 실행된 후에 '💕'를 내보냅니다(yield).
- 세 번째 실행될 때, 이전 yield 키워드의 라인부터 실행합니다. return 키워드가 나올 때 까지 모두 실행합니다. 그리고 'Done'을 반환합니다
하지만... 제너레이터 객체를 반환한 제너레이터 함수의 호출을 이전에 보았다면, 우리는 어떻게 그 함수를 호출할 수 있을까요?
🤔여기에서 제너레이터 객체가 활약합니다!
제너레이터 객체는 next 메소드가 포함되어 있습니다(프로토타입 체인에). 이 메소드는 제너레이터 객체를 반복하기 위해 사용될 예정입니다.
하지만 이전에 값을 산출한 후(yield) 떠난 위치를 기억하기 위해서, 우리는 제너레이터 객체에 변수를 할당해야 합니다. generatorObject를 줄여서 genObj라고 부르겠습니다.
아까 봤던 무섭게 생긴 객체와 같습니다. genObj의 next 메소드를 호출하면 무슨 일이 일어나는지 한 번 살펴봅시다!
제너레이터는 첫 번째 yield이 나올 때 까지 실행되었고, 이 키워드는 첫 번째 줄에 있었습니다! value, done 속성을 포함하는 객체를 산출했습니다(yield)
{ value: ... , done: ... }
value 속성은 우리가 산출했던 값과 동일합니다.
done 속성은 boolean 값으로, 제너레이터 함수가 값을 return 했을 때만 true가 됩니다.
제너레이터의 대한 반복을 멈추었고, 이것은 함수가 일시 중지된 것 처럼 보입니다! 얼마나 멋진가요. 이제 다음 next 메소드를 호출해봅시다! 😃
먼저 문자열First log!를 콘솔에 기록합니다. 이것은 yield이나 return 키워드가 아니므로 계속 진행합니다! 그리고 yield 키워드와 '💕' 값을 만납니다.
객체는 '💕'를 값으로 하는 속성 value와, done 속성을 가진체 산출됩니다(yield). 제너레이터가 return되기 전까지 done 속성의 값은 false입니다.
거의 다 왔습니다! 마지막으로 next 메소드를 호출합시다.
콘솔에 문자열 Second log!가 기록됩니다. 그리고 return을 만나 'Done!'을 반환합니다. 객체는 value속성 값이 'Done!'인 채로 반환됩니다.
이번에는 return 되었기 때문에, done 속성은 true입니다!
done속성은 매우 중요합니다. 우리는 제너레이터 객체를 단 한 번 반복할 수 있습니다 뭐라고!? 그렇다면, 어떻게 다음 next 메소드를 부를 수 있을까요??
이것은 영원히 undefined를 반환합니다. 다시 반복하길 원한다면, 우리는 새로운 제너레이터 객체를 만들어야합니다!
방금 본 것처럼, 제너레이터 함수는 iterator(제너레이터 객체)를 반환합니다. 하지만.. 잠깐, iterator ..?
그건 우리가 for of 반복을 할 수 있다는 뜻이고, 반환된 객체에 스프레드 연산자를 사용할 수 있다는 뜻인가요?? 예쓰! 🤩
[...] 구문을 사용해서 산출된 값을 배열로 스프레드 해봅시다
아니면 for of를 사용해볼까요?!
수 많은 가능성이 있습니다!
하지만, 무엇이 iterator를 iterator로 만들까요? 왜냐하면 우리는 array, string, map, set에서 spread 구문을 사용할 수 있잖아요.
정답은 그것들이 interator protocol 인 [Symbol.iterator]를 구현하기 때문입니다. 아래와 같은 값들을 가지고 있다고 가정해봅시다
array, string, generatorObject는 모두 iterator입니다! 그들의 [Symbol.iterator]속성을 한 번 살펴봅시다.
하지만 반복할 수 없는 값의 [Symbol.iterator] 속성의 값은 무엇일까요?
네, 그건 그냥 없습니다. 그렇다면 우리가 [Symbol.iterator] 속성을 수동으로 추가하고, 반복할 수 없는 객체를 반복가능하게 만들 수 있을까요? 가능합니다! 😃
[Symbol.iterator]는 next 메소드를 포함하는 iterator를 반환해야 하며, 이 메소드는 우리가 위에서 본 것과 같은 객체를 반환합니다. { value: '...', done: false/true }
우리는 [Symbol.iterator]의 값을 제너레이터 함수와 동일하게 설정할 수 있고 이것은 기본적으로 iterator를 반환합니다.
객체를 반복 가능하게 만들고, 반환된 값을 전체 객체로 만들어 봅시다.
스프레드 구문, for-of loop를 우리의 object 객체에 적용했을 때 어떤 일이 일어나는지 보세요.
혹은 객체의 키들만 원할 수 있습니다. "오, 그건 쉬워. 우리는 this 대신에Object.keys(this)를 산출하면 될 뿐이야"
흠... 그렇게 해봅시다.
이런! Object.keys(this)는 배열이므로 산출된 값은 배열입니다. 그리고 이 산출된 배열을 다른 배열에 스프레드 시키게 되고,
결과적으로 중첩 배열이 만들어집니다. 이는 우리가 원하는게 아닙니다. 우리는 그저 각각의 기를 산출하고 싶습니다!
좋은 소식이 있습니다! 🥳 우리는 yield 키워드를 사용해서 제너레이터의 내부 iterator로부터 개별적인 값을 산출할 수 있습니다.
그래서 yield 뒤에 별표가 붙습니다! 처음에 아보카도를 산출하는 제너레이터 함수를 가정합시다.
그리고 우리는 개별적으로 다른 iterator(이 경우에는 배열)에 값들은 산출하길 원합니다. @yield@ 키워드로 가능합니다. 그런 다음, 다른 제너레이터에 위임합니다.
위임된 제너레이터의 각 값들은 genObj iterator를 계속 반복하기 전에 산출됩니다.
이게 바로 모든 객체의 키를 개별적으로 가져오기 위해 필요한 작업입니다!
제너레이터 함수의 또 다른 쓰임새는 observer(관찰자) 함수로 사용할 수 있다는 것입니다. 제너레이터는 데이터가 들어오길 기다릴 수 있으며, 데이터가 전달된 경우에만 데이터를 처리합니다.
여기서 큰 차이점은 우리가 위에서 봤던 예시들처럼 yield [value]의 형태가 아니라는 점입니다.
대신에 우리는 second라는 값을 할당하고, 문자열 First!를 산출합니다. 이는 우리가 next 메소드를 사용하면 먼저 산출될 값입니다.
next를 처음 실행하면 어떤 일이 일어나는지 살펴봅시다.
첫 번째 줄에서 yield를 만나고, First!를 산출합니다. 그럼 second 값은 뭘까요?
이 값이 바로 우리가 다음에 그것을 호출할 때 next 메소드에 전달할 값입니다. 이번에는 문자열 'I like JavaScript'를 전달하겠습니다.
여기서 중요한 것은 next메소드의 첫 번째 호출이 아직 어떤 입력도 추척하지 않는 다는 것입니다. 우리는 단지 observer(관찰자)를 처음 호출하며 시작합니다.
제너레이터는 우리의 입력을 기다렸다가 우리가 next 메소드에 전달한 값을 처리할 수 있습니다.
그럼, 왜 제너레이터 함수를 사용할까요?
제너레이터의 가장 큰 장점 중 하나는 lazily evaluated(느린 평가) 입니다. 즉, next 메서드를 호출한 이후 반환된 값은 우리가 그것을 요청한 이후에 계산된다는 것을 의미합니다!
일반 함수는 이렇지 않고 이후 사용될 값이 모두 생성됩니다.
다른 사용 사례도 몇 가지 있지만, 저는 보통 대규모 데이터 셋을 반복할 때 제어력을 높이기 위해 이와 같은 방법을 사용하길 좋아합니다!
우리가 북 클럽 목록을 가지고 있다고 생각해보세요! 📚 이 예시를 간단하게 하기 위해 북 클럽에는 한 명의 맴버만 있다고 하겠습니다. 이 맴버는 현재 book 배열에 있는 여러 권의 책을 읽고 있습니다.
이제 아이디 ey812의 책을 찾아보겠습니다. 이 책을 찾기 위해 우리는 중첩된 for-loop 혹은 forEach를 사용해야할 수도 있지만, 이는 우리가 원하는 값을 찾아도 반복을 계속 진행해야함을 의미합니다.
제너레이터의 멋진 점은 우리가 지시하지 않는 한, 반복이 계속 진행되지 않는다는 것입니다. 즉, 우리는 반환된 아이템을 평가해서, 만약 그 아이템이 우리가 찾는 것이라면 더 이상 next 를 호출하지 않으면 됩니다! 어떻게 보일지 봅시다.
먼저, 각 팀원의 books 배열을 반복하는 제너레이터를 생성합니다. 우리는 팀 맴버들의 book 배열을 제너레이터 함수에 전달하고, 배열을 반복해서, 각 책을 산출합니다!
완벽합니다! 이제 clubMembers 배열을 반복하는 제너레이터를 만들어야합니다. 클럽 회원 자체는 신경쓸 필요 없고, 그들의 책 배열만 반복하면 됩니다.
iterateMembers 제너레이터에서, 그들의 책을 산출하기 위해 iterateBooks iterator를 위임합니다!
거의 다 왔습니다! 마지막 단계는 bookclubs를 반복하는 겁니다. 이전 예시처럼, bookclubs 자체는 신경쓰지 말고 클럽 회원들(특히 그들의 책들)만 신경씁니다.
iterateClubMembers iterator를 위임하고, clubMembers 배열을 그것에 전달합니다.
이 모든 과정을 반복하기 위해 우리는 iterateBookClubs 제너레이터에 bookClub 배열을 전달함으로써, 반복가능한 제너레이터 객체를 얻어야합니다. 일단 제너레이터 객체 it이라고 부르겠습니다.
아이디 ey812의 책을 얻을 때 까지 next 메소드를를 호출합니다.
좋아! 우리는 우리가 찾는 책을 얻기 위해 모든 데이터를 반복할 필요가 없습니다. 대신 우리는 요청에 있을 때만 데이터를 검색했습니다! 물론,
next 메소드를 매번 수동으로 호출하는 것은 매우 비효율적입니다. 그러니 함수화 시켜봅시다!
함수에 찾고 있는 책 id를 함수에 전달합니다. 만약 value.id가 우리가 찾는 id라면, 그 value를 반환하면 됩니다. 그렇지 않다면 next를 호출하면 됩니다!
물론 매우 매우 작은 데이터셋입니다. 하지만 우리가 수 많은 데이터를 가지고 있거나, 하나의 값을 찾기 위해 구문을 분석을 해야하는 상황을 상상해보세요.
일반적으로 구문을 분석하기 위해서는 전체 데이터가 준비될 때 까지 기다려야합니다. 제너레이터 함수를 사용하면 단순히 작은 데이터 청크를 요청하고, 데이터를 확인하고, 값은 next를 호출할 때만 생성됩니다!
아직도 "무슨일이 일어나는지" 모르겠다 하더라도 걱정하지 마세요. 제너레이터 함수는 적절한 케이스에 직접 사용해보기 전까진 혼란스럽습니다. 저는 몇 가지 용어가 좀 더 명확해졌으면 좋겠다고 생각합니다.
'📌 Front End > └ JavaScript' 카테고리의 다른 글
[JavaScript] 자바스크립트 this란? (0) | 2023.03.10 |
---|---|
[JavaScript] 자바스크립트 클로저(Closure)란? (0) | 2023.03.10 |
[JavaScript] 자바스크립트 프로토타입 상속 (0) | 2023.03.10 |
[JavaScript] 자바스크립트 프로토타입 기반의 함수 Class (ES6) (0) | 2023.03.10 |
[JavaScript] 자바스크립트 깊은 복사(Deep Copy)와 얕은 복사(Shallow Copy) (0) | 2023.03.10 |