Javascript Generator에 대해
반복자 (iterator) 패턴이란
https://refactoring.guru/ko/design-patterns/iterator
어플리케이션 전체에서 어떤 도메인 모델의 순회가 일어나는 경우 코드의 중복을 줄일 수 있는 디자인 패턴
- 코드가 다른 데이터 구조들을 순회하길 원할 때
- 순회할 구조들의 타입 또는 정보를 미리 전부 알 수 없을 때 사용한다.
사용 방법
Iterator 인터페이스 선언. Iterator 인터페이스는 몇 가지 구성 요소를 가질 수 있다.
- (필수) 컬렉션에서 다음 요소를 가져오는
getNext()같은 메서드 - (선택 사항) 컬렉션에서 전 요소를 가져오는 메서드
- (선택 사항) 컬렉션에서 현재 위치를 추적하는 메서드
- (선택 사항) iterator를 사용한 순회의 끝을 확인하는 메서드
iterator를 구현한 구현체를 통해 특정 객체에 대한 반복을 손 쉽게 할 수 있다는 장점이 있다.
장점
- SRP : 그래프와 같은 순회 알고리즘 작성 시 순회 대상을 별도의 클래스로 추출하여 코드를 정리할 수 있다.
- OCP : 신규 유형의 컬렉션과 Iterator들을 구현함으로써 신규 유형에 대한 순회도 쉽게 할 수 있다.
- 동일 컬렉션 내 병렬 순회 가능 : 각각의 Iterator 객체 내에는 순회 중인 상태를 저장하기 때문에 큰 그래프의 탐색 같은 경우 다수의 worker에 순회를 위임시켜 결과를 병합할 수 있다.
- 순회를 잠시 멈추거나 중개하는 등의 흐름 제어가 가능
단점
- 단순한 컬렉션에 대한 순회만 사용한다면 굳이?
- Iterator 사용 시 때로는 직접 컬렉션의 요소를 순회하는 것보다 비효율적일 수 있음 (RB 트리 같은 경우?)
Javascript의 Iteration Protocol
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols
자바스크립트는 객체들이 어떤 (커스텀) iteration 동작을 정의하는 것을 허용한다.
- 어떤 빌트인 타입들 (
Object) 은 iteration이 불가능하지만 - 어떤 빌트인 타입들 (
Array,Map)은 iteration이 가능하다..
차이는 무엇일까?
Iterable 하기 위해서는
@@iterator메서드를 구현해야 한다.
어떤 객체가 iterable하게 호출된다고 하는 것 == 객체의 @@iterator 메서드가 인수 없이 호출되고 , 반환된 iterator가 다시 반복을 통해 획득할 값들을 얻을 때 사용된다.
자세히 들여다 보면..
ES6 (ECMAScript 2015) 부터는 값들의 sequence를 만드는 표준 방법을 정의하고, 이를 iterator protocol라고 부른다.
객체가 next() 메서드를 가지고 있고, 아래 규칙에 따라 구현되었다면 그 객체는 iterator이다.
next(): 아래 2개의 속성을 가진Object를 반환하는 파라미터 없는 함수done: boolean 타입을 리턴한다.- iterator가 마지막 반복 작업 마쳤을 경우 true 반환
- iterator가 반환할 값이 아직 남아있다면 false 반환
done을 정의하지 않은 경우 false
value: iterator로부터 반환되는 모든 자바스크립트의 값.done이true인 경우 생략 가능
예시
자바스크립트의 String 타입은 빌트인 iterator 객체이다.
let someStr = "hi";
typeof someStr[Symbol.iterator]; // "function"
let iterator = someStr[Symbol.iterator]();
iterator + ""; // "[object String Iterator]"
iterator.next(); // {value : "h", done: false}
iterator.next(); // {value : "i", done: false}
iterator.next(); // {value : undefined, done: true}
Spread 연산자 또한 iteration protocol을 사용하기 때문에 다음과 같이 전개될 수 있다.
// Create a generator function with multiple yields
function* generatorFunction() {
yield 'Neo'
yield 'Morpheus'
yield 'Trinity'
return 'The Oracle'
}
const generator = generatorFunction()
// Create an array from the values of a Generator object
const values = [...generator]
console.log(values) // (3) ["Neo", "Morpheus", "Trinity"]
자바스크립트의 제네레이터 함수
제네레이터 함수는 generator 객체를 반환하는 함수이다.
또한 몇 가지 특징을 가지고 있는데,
function*으로 선언해서 사용한다.- 빠져나갔다가 나중에 다시 돌아올 수 있는 함수
- 이 때 함수 내부의 컨텍스트는 출입 과정에서 변경된 상태로 저장된다.
- 호출되어도 즉시 시행되지 않고, 함수를 위한 iterator 객체가 반환
- 이 iterator의
next()메서드가 호출되면 generator 함수가 실행 yield문을 만날 때까지 진행yield*표현식을 마주치면 다른 제네레이터 함수가 실행을 위임받는다.
- 해당 표현식이 명시하는 iterator의 반환 (
value) 값을 리턴 - 다음
next()메서드가 호출되면 멈췄던 위치에서부터 재실행
- 이 iterator의
function* idMaker(){
var index = 0;
while(index < 3)
yield index++;
}
var gen = idMaker();
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // undefined
아래 예제처럼 yield*를 사용해서 제네레이터 함수 안에서 다른 제네레이터로의 위임이 가능하다.
function* anotherGenerator(i) {
yield i + 1;
yield i + 2;
yield i + 3;
}
function* generator(i){
yield i;
yield* anotherGenerator(i);
yield i + 10;
}
var gen = generator(10);
console.log(gen.next().value); // 10
console.log(gen.next().value); // 11
console.log(gen.next().value); // 12
console.log(gen.next().value); // 13
console.log(gen.next().value); // 20
yield
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/yield
yield 키워드는 제너레이터 함수를 중지하거나 재개하는데 사용된다.
- 제네레이터 함수 안에서
yield키워드를 사용한 곳들을 기준으로 코드가 잘려서 수행된다. - 특정 처리까지 한 후에 잠시 멈췄다가 나중에 다시 해당 상태에 접근할 때 유용하게 사용할 수 있다.
React의 Redux Saga에서의 제네레이터 함수의 사용
Redux-Saga란 액션을 모니터링하고 있다가, 특정 액션 발생 시 이에 따른 특정 작업을 실행한다.
import { delay, put, takeEvery, takeLatest } from 'redux-saga/effects';
// 액션 타입
const INCREASE = 'INCREASE';
const DECREASE = 'DECREASE';
const INCREASE_ASYNC = 'INCREASE_ASYNC';
const DECREASE_ASYNC = 'DECREASE_ASYNC';
// 액션 생성 함수
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
export const increaseAsync = () => ({ type: INCREASE_ASYNC });
export const decreaseAsync = () => ({ type: DECREASE_ASYNC });
function* increaseSaga() {
yield delay(1000); // 1초를 기다립니다.
yield put(increase()); // put은 특정 액션을 디스패치 해줍니다.
}
function* decreaseSaga() {
yield delay(1000); // 1초를 기다립니다.
yield put(decrease()); // put은 특정 액션을 디스패치 해줍니다.
}
export function* counterSaga() {
yield takeEvery(INCREASE_ASYNC, increaseSaga);
// 모든 INCREASE_ASYNC 액션을 처리
yield takeLatest(DECREASE_ASYNC, decreaseSaga);
// 가장 마지막으로 디스패치된 DECREASE_ASYNC 액션만을 처리
}
// 초깃값 (상태가 객체가 아니라 그냥 숫자여도 상관 없습니다.)
const initialState = 0;
export default function counter(state = initialState, action) {
switch (action.type) {
case INCREASE:
return state + 1;
case DECREASE:
return state - 1;
default:
return state;
}
}
https://caileb.tistory.com/197
Redux Saga/effects 에서 제공되는 함수들 (call, put)을 사용한다면
function*함수를 호출 할 때next()가 아닌 액션을 통해 호출한다.- 리액트에서는 액션을 통해
function*함수를 한번 호출하고 난 후에- 내부에 있는 여러
yield함수를 차례로 호출한다.
- 내부에 있는 여러
Redux Saga의 put 함수를 통해 새로운 액션을 디스패치하여 비동기로 실행할 수 있다.
all함수는 redux에서 비동기 처리가 필요한 함수들을 배열 형태로 전달, 동시에 병행으로 처리를 수행한다.takeEvery함수는 특정 액션 타입에 대해 디스패치되는 모든 액션을 처리한다.takeLatest함수는 특정 액션 타입에 대해 디스패치된 가장 마지막 액션만을 처리한다.- 즉 지정한 액션이 실행되는 동안 동일 타입의 새 액션이 디스패치되면 기존에 하던 작업은 무시하고 새 작업 시작 (따닥 방지 등)
왜 제네레이터 + yield 인가
모던 웹 트렌드 : 끊기지 않는 데이터 스트림의 활용이 사용자들에게 보다 깊은 인상을 줌 (주관적인 생각)
제네레이터 등장 이전에 무한한 데이터 스트림을 처리하려면..? 반복 어케하죠? (무한 스크롤, 연속된 음성 재생 등)
promise / async / await 사용해서 API 호출 시 페이지네이션을 적용할 수 있지만.. 패러다임의 불일치 때문에 뭔가 자연스럽지 않다!
제네레이터 함수의 장점
Lazy Eval : 표현식의 실행을 실제로 필요할 때까지 미룰 수 있다.
- 이에 따른 메모리 효율성 증대 : 보다 빠른 코드의 실행. 브라우저 최적화 -> UX 개선
데이터 패러다임의 불일치를 어느정도 해결
- Collection / Stream 타입 데이터를 분리해서 처리해보자!
- 제네레이터 함수를 사용해서 컬렉션 타입의 데이터도 처리할 수 있지만..
- Stream 타입 데이터를 처리할 때 비로소 진가가 나타난다
무한 스크롤이 가지는 장점
https://www.nngroup.com/articles/infinite-scrolling-tips/
- 인터럽션 감소 : 사용자 입장에서 잠깐의 인터럽션을 줄임으로써 잔류 시간을 늘리는데에 큰 역할
- 인터랙션 비용 감소 : 페이지네이션을 사용했을 때 버튼을 클릭하는 등의 부가적인 인터랙션 비용을 줄여, 사용자 입장에서 좀 더 덜 피곤한 웹 페이지를 만들 수 있다.
- 모바일 기기에서의 활약 : 모바일 환경에서 스크롤은 가장 빈번하게 일어나는 인터랙션. 사용자들은 이미 스크롤을 하고 있기 때문에 무한 스크롤 지원 시 모바일에서의 UX가 가장 크게 개선된다
무한 스크롤이 가지는 단점
- 한번 봤던 내용을 다시 보기 어려움 : 내용을 다시 찾으려 할 때 다시 스크롤을 올려야 함
- 페이지의 끝에 도달하기 어려움 : 사용자 입장에서 컨텐츠가 끝났다는 것을 인지하기 어렵다
- Footer 접근 어려움 : 무한 스크롤 사용 시 사용자에게는 Footer가 노출되기 어렵다
- 접근성 문제 : 키보드만 사용하는 (vim만 사용한다거나) 사용자들에게는 너무 불편하다
- 페이지 로드 증가 : 더 많은 정보를 불러와야 하기 때문에, 정보 전달이 지연된다는 느낌을 줄 수 있다.
- SEO 성능 감소 : 무한 스크롤은 SEO 성능을 감소시킨다. 검색 엔진은 첫번째 섹션 다음의 컨텐츠에 항상 접근할 수 있는 것이 아니기 때문에, 노출 수가 중요한 페이지의 경우 사용을 고민해야