[ JS ] 이터레이터, 제너레이터, 지연평가 ( Iterator, Generator, Lazy Evaluation )

UI SEOK YU·2023년 1월 18일
2

JavaScript

목록 보기
2/6
post-thumbnail

비동기 처리에 이어서..

1.이터레이터(Iterater)


1-1. Iteration Protocols


1-1-1. Enumerable

  • 객체에 자동으로 생성되는 propery attribute 에는 [[enumerable]] 라는 속성이 있다.
  • [[enumerable]] 은 프로퍼티의 열거 가능여부를 나타낸다.
  • [[enumerable]]true 인 객체는 프로퍼티의 요소를 열거할 수 있다.

1-1-2. Iteration

  • 직역하면 '반복' 이다.
  • 객체의 프로퍼티 조회를 '반복' 하기 위해서는 [[enumerable]]true 이어야 한다.
  • 아래는 for..infor..of 에 대한 문법. 대상이 enumerable 해야 함을 알 수 있다.


1-1-2 Iteration Protocols

  • 왜 생겨났는가 ?
    다양한 데이터 공급자 ( array, string, map/set, DOM Collection ...) 로 부터 순차적인 데이터를 가져오기 위한 효율적인 규칙이 필요해서 탄생.
    (저 예시들의 데이터를 불러올 때마다 각자 다른 규칙과 문법이 쓰인다고 생각하면 정말 비효율적이다.)

  • '반복' 에 관한 프로토콜들. 2 종류가 있다.

  • iterable protocol : 순회 가능 한.

  • iterator protocol : 순회 기능이 있는.

1-2. Iterable Protocol

  • 직관적으로 이해하기 위해 "순회 가능 한" 으로 표현했다.
  • 명시적 규칙은 다음과 같다.

    [Symbol.iterator] (=@@iterator) 라는 메서드가 있어서,
    [Symbol.iterator]를 호출하면 iterator 객체를 반환한다.

  • 위 규칙을 만족하는 객체를 "iterable 한 객체" 라고 한다.

1-3. Iterator Protocol

  • 직관적으로 이해하기 위해 "순회 기능이 있는" 으로 표현했다.

  • 명시적 규칙은 다음과 같다.

    next() 메서드를 가지고 있으며, next() 호출 시,
    {value, done} 형태의Iterator Result를 반환한다.

  • value 에는 현재 순회하는 위치의 값을 나타내며,
    done 은 해당 객체의 프로퍼티를 모두 순회했는지의 여부를 나타낸다.

  • 위 규칙을 만족하는 객체를 "iterator 객체" 라고 한다.

1-4. Well Formed Iterable

  • 위 두 조건을 동시에 만족하는 객체를 Well Formed Iterable 객체라고 한다.
  • iterable 한 객체[Symbol.iterator]()를 실행하여,
  • iterator 를 생성하여 사용한다. (객체가 이터레이터화 되어 반환됨)
  • iteratornext() 메서드를 통해 사용할 수 있다.

  • 일반적인 배열을 예시로 들어보면,
const arr = [1, 2, 3]

const iter = arr[Symbol.iterator]() // Array Iterator 객체 생성

console.log(iter.next())
console.log(iter.next())
console.log(iter.next())
  • [Symbol.iterator]()로 이터러블한 배열을 가지고 이터레이터 객체를 생성
    (현재 값 value와 완료여부 done을 가진) 한 다음,
  • 이터레이터 객체의 next()메서드를 활용하여 순회한다.
  • 참고로 전개연산자 (...) 를 사용하게 되면, 이터러블 객체의 이터레이터를 순회하면서, next() 로 각 요소를 하나씩 받아와 나열한다.


2. 제너레이터 (Generator)


2-1. 정의


2-1-1. 제너레이터란

  • 코드블록의 실행을 일시 정지했다가 필요한 시점에 재개할 수 있는 함수
  • 함수 호출자에게 실행의 제어권을 양도 가능하며, 함수의 상태를 주고 받을 수 있음
  • 제너레이터 함수를 호출하면 제너레이터 객체를 반환

( = 함수를 이터레이터로 만들기 )
( = 심지어 평가되기를 미뤄둔 이터러블 이터레이터 )
( = 그것이, 제너레이터 )

2-1-2. 제너레이터 형태

  • 위에서 설명한 '일시 정지' 를 위해서는 yield를 사용한다.
  • yield 를 사용하기 위해서는 함수를 generator function 로 바꾼다.
  • generator functionfunction * 으로 표기한다.
// 제너레이터 함수선언문
function* genDecFunc(){
  yield 0;
}

// 제너레이터 함수 표현식
const genExpFunc = function * (){
  yield 0;
};

// 제너레이터 메서드
const obj = {
	* genObjMethod(){
      yield 0;
  }
};

// 제너레이터 클래스 메서드
class genClass{
	* genClassMethod(){
      yield 0;
    }
}

2-1-2. 제너레이터 객체

  • 제너레이터 함수를 호출하면 제너레이터 객체가 반환된다.
  • 제너레이터 객체는 iterator 이면서, iterable 이다.
  • 즉, 내부에 포함된 next() 메서드로 yield 를 넘길 수 있다.
    ( * '첫 yield 전 까지' 의 실행은 next() 메서드가 호출되어야 비로소 실행.
    처음에는 이터러블 상태인 제너레이터 객체 그 자체만 반환 됨 )
  • next()yield 이후를 실행, 실행한 값을 value로 받음,
    next(){value, done} 형태의 iteratorResult를 반환한다.

  • yield는 한 박자 늦는 return 이라고 생각하면 될 듯 하다.

  • 제너레이터 함수는 제너레이터 객체를 만들어 주는 역할일 뿐이고,
  • 함수 호출자가 next()로 제너레이터 객체의 값을 하나씩 넘기면서 실질적으로 활용한다.

2-2. 활용

  • 제너레이터를 사용하는 이유는 이터레이터를 더 쉽게 구현하기 위해서다.
  • 이터레이터를 사용하는 이유는 자료를 순회하며 활용하기 위해서다.
  • 순회 할 때, 다음 자료로 넘어가는 단계를 제어하여 프로그램의 효율을 높일 수 있다.
  • 순회보다 연산를 먼저 하도록 순서를 변경하는 것을 지연평가, Lazy Evaluation 이라고 한다.



3. 지연평가

" 계산은 나중에 한다 "


3-1. 정의


3-1-1. Lazy Evaluation

Lazy Evaluation : 은 계산의 결과값이 필요할 때까지 "계산을 늦추는 기법".

3-1-2. 단어로 이해하기

  • 지연 : 미루어서 (delay, lazy)
  • 평가 : JS 에서 '평가'란, "식을 해석해서 값으로 생성/참조" 하는 것.
  • "식을 해석해서 값으로 생성/참조" ...
    => 런타임에서, 변수에 초기화 되어있는 undefined 대신
    계산한 (또는 그것의 주소) 을 할당하는 과정.
  • 변수 : 메모리상의 공간을 구별하는 '식별자'다.
  • 즉, 지연평가란,
    식별된 메모리 공간, 메모리 상에 계산된 값 (또는 그것의 주소)을 할당하는 과정을
    최대한 미루는 행위이다.


3-2. 사용


3-2-1. 지연평가의 사용이유

  • 불필요한 계산을 하지 않는다.
  • 무한 자료구조를 사용할 수 있다.
  • 복잡한 수식에서 오류 상태를 피할 수 있다.

3-2-2. 불필요한 계산의 방지

  • 아래는 1부터 n까지의 수가 들어 있는 배열 중, 짝수만을 더해 반환하는 함수이다.
// 일반적인 함수
function originalFunction(size){
  function newArr(n) {
    let i = 1;
    const res = [];
    while (i < n) res.push(i++);
    return res;
  }

  let sum = 0;
  const numArr = newArr(size);
  for(let i = 0; i < numArr.length; i++){
    if(numArr[i] % 2 === 0){ sum += numArr[i]; }
    else if(sum >= 100) break;
  }
  return sum 
}
  • 처음에 배열을 입력받은 size 만큼 '전부' 생성해서 숫자를 넣은 후,
  • 짝수를 더하다가 합이 100을 넘자 바로 반환했다.
  • 합(sum)이 100 이후로는 배열의 숫자들이 필요가 없었다.
  • 즉, 20 이후의 21~size 는 의미없이 할당과정을 거친 것..
  • size가 커지면 커질 수록 비효율은 커진다.
// 지연평가 함수로 작성
function lazyFunction(size){
  function * numArr(n){
    let i = 1;
    while (i < n) yield i++;
  }

  let sum = 0;
  for(const item of numArr(size)){
    if(item % 2 === 0){ sum += item; }
    else if(sum >= 100) break;
  }
  return sum;
}
  • yield 가 나타나면, next()를 이용해 다음 단계로 넘기기 전까지 멈춘다.
  • 위 코드에서는 for of 에 내장된 next()를 통해 한 단계씩 끊어서 하고 있다.
  • 즉, 1부터 size 까지 배열에 숫자를 할당하는 과정을,
  • '1'만 넣고 돌려보고, 그 다음 '2'넣고 돌려보고 하는 식으로 진행한다.
  • 아까처럼 20 이후의 필요없는 숫자들을 할당하는 과정없이 함수가 끝난다.
  • size에 10000000 을 넣고 시간을 찍어서 확인해보면,
console.time('originalFunction 의 소요시간');
console.log("originalFunction : ", originalFunction(10000000));
console.timeEnd('originalFunction 의 소요시간');

console.time('lazyFunction 의 소요시간');
console.log("lazyFunction : ", lazyFunction(10000000));
console.timeEnd('lazyFunction 의 소요시간');
originalFunction :  110
originalFunction 의 소요시간: 445.461ms
lazyFunction :  110
lazyFunction 의 소요시간: 0.366ms
  • 시간이 배로 차이남을 확인할 수 있다.

3-2-3. 무한 자료구조의 사용

  • 위 예시에서는 100개, 10000개의 배열과 같이, 정해진 수 만큼 배열을 생성하고 연산을 했다.
  • 하지만 만일 무한한 데이터를 불러와서 특정 작업을 해야한다면?
  • 혹은 필요한 데이터의 끝이 어딘지 정확히 모른다면?
  • 만약에 기존 함수의 작동방식대로 한다면 ( 지연평가를 사용하지 않고 ), 모든 데이터를 한 번에 불러와 놓고 시작해야하므로 결과 값을 받으려면 한 세월이 걸릴 것이다.
  • 반면에 지연평가를 활용하면, 데이터 하나를 불러와서 연산을 하고, 끝나면 다시 다음데이터를 불러와서 연산하는 과정을 무한하게 이어갈 수 있다.
// 일반 반복문으로 작성한 피보나치 수열
const originalFibo = function () {
    let [pre, cur] = [0, 1];

    while (true) {
        [pre, cur] = [cur, pre + cur];
        if (cur > 10000) break;
        console.log(cur);
    }
}

originalFibo()

//------------------------------------------

// 제너레이터를 활용한 피보나치 수열
const generatorFibo = function* () {
    let [pre, cur] = [0, 1];

    while (true) {
        [pre, cur] = [cur, pre + cur];
        yield cur;
    }
}

for (const num of generatorFibo()) {
    if (num > 10000) break;
    console.log(num)
}
  • 위 두개의 함수는 같은 역할을 하는 것 처럼 보인다.
  • 제너레이터를 사용하지 않고 그냥 반복문으로 돌릴 수도 있는데
    왜 굳이? 라는 생각이 들 수도 있다.
  • 일반 반복문은 돌아가는 상황을 외부에서 제어할 수 없지만, 제너레이터는 가능하다.
  • 이 말은 즉, 함수의 역할을 분리할 수 있고, 더욱 간결한 코드를 작성할 수 있다.
  • 더 간결하게 표현하되, 무한의 개념을 들고 올 수 있는 것이다.
  • 위 코드도 자세히 보면 originalFibo 에서 console.log 를 안에서 찍고, break 문을 걸어줘서 그렇지, 피보나치의 수를 구하는 자체의 함수 만으로는 무한하게 돌아버린다..
    메모리를 다 쓸때까지..
  • 반면에 generatorFibo 피보나치 수열을 구하는 함수는 무한이지만, 외부에서 제어가 가능하기 때문에 활용이 가능하다.

3-2-4. 오류상태 제거

(작성 중)

3-2-5. Generator & Promise

  • generatorPromise 를 조합해서 쓰면 async/await 형태가 된다.

  • Promise가 실행되서 반환된 Promise 객체then() 등을 통해 그 다음단계로 넘어가는데, generatornext() 를 이용해 각각의 promise가 한 단계 넘어 갈 때마다 변수로 그 결과를 받아두면, async 함수 안에서 const aaa = await promiseObj 형태로 변수에 저장하는 것과 같다.

3-3. 성능



0. 부록

참고한 자료

더 공부해야 할 것

  • 프로퍼티 어트리뷰트
  • Symbol

0개의 댓글