proposal-iterator-helpers와 지연평가

앤더손씨·2025년 4월 18일
1

🏍 최적화란 아름다워

최근 제로초님의 쇼츠에 올라온 내용을 보다가 "배열에서 특정 요소를 빠르게 가져오는 방법" 으로 소개된 iterator 의 헬퍼 메서드 관련 내용을 알게 되었다.

해당 내용에 대해서 궁금증을 가지게 되어 공부를 하다가 재미난 내용을 많이 공부할 수 있었기 때문에 스쳐 지나가는 지식이 되지 않도록 기록으로 남기기 위해 글을 작성하려고 한다.

🏍 Iterator helper proposal

Iterator helper 제안사항 이 ECMA-262 의 Stage 4가 된 후 병합되어 표준에 채택되어 공식 지원하는 것이 작년에 확정되었었다(짝짝)

이에 대해서 단순히 우왕 빠르다 하고 넘어갈 수 있었지만, 쇼츠 내용을 보면서 지연평가라는 단어가 무슨 의미인지 알고 싶었다. 도대체 지연 평가가 무엇이길래 일반 배열의 filter과는 다르다고 하는걸까.

🏍 "Array.prototype.filter" 와 "Iterator.prototype.filter"

// 과연 여기서 홀수 중 앞의 2개만 필터링을 하려고 한다면?
const array = [1,2,3,....... 수백만개]

자바스크립트의 배열은 객체이고, 이 객체를 형성할 때 prototype에는 표준 내장 객체인 Array 의 prototype 객체를 상속해온다는 것을 알고 있다.
우리가 사용하는 filter, map, every 등의 메서드들은 다 이 프로토타입 메서드들이다.

Image

여기서 대표적으로 지금 볼 "filter" 프로토타입 메서드를 확인해보자.

이 filter 메서드는 내부적으로 원본 객체의 index와 length를 기반으로 동작하는 함수이다.
그리고 결과적으로 filter 함수의 콜백인자에 해당하는 값만 필터링된 새로운 배열을 생성한다.

function myFilter<T>(array: T[], callback: (value: T, index: number, array: T[]) => boolean): T[] {
  const result: T[] = [];

  for (let i = 0; i < array.length; i++) {
    if (i in array) {
      const value = array[i];
      if (callback(value, i, array)) {
        result.push(value);
      }
    }
  }

  return result;
}

위와 같은 동작은 배열에서 곧바로 접근해서 사용할 수 있기에 간편하지만, 전체적인 순회를 막을 수 없다는 단점이 존재한다.
배열이 크지 않다면 상관없지만 만약 배열의 요소가 수백만개 이상일 경우라면 불필요한 순회를 한다는 불편함이 존재한다.

그렇다면 효율적인 방법은 무엇일까?

결론부터 말하자면, 불필요한 순회를 하지 않으면 된다.

JavaScript는 게으른 평가(lazy evaluation)를 가능하게 하는 이터레이터(iterator) 라는 특별한 제어 흐름 도구를 제공한다.

iterator에 대한 자세한 설명은 아래 링크를 통해 확인하는 것을 강력 추천한다.

이터러블과 이터레이터

🔁 Iterator란?

이터레이터(Iterator) 는 값을 순차적으로 하나씩 반환할 수 있는 특수객체이다.
JavaScript에서 이터러블 프로토콜(iterable protocol)을 따르는 객체는 for...of, 전개 연산자(...), 구조 분해 할당 등 다양한 곳에서 사용할 수 있다.

✅ Iterator의 핵심 구성요소

  1. Symbol.iterator 메서드가 존재하는 이터러블 객체

    • 호출 시 이터레이터 객체를 반환해야 함
  2. 이터레이터 객체는 반드시 next() 메서드를 가져야 하며,
    이 메서드는 다음 값을 다음과 같은 형태로 반환합니다:

{
  value: any,    // 다음에 반환할 값
  done: boolean  // 순회가 끝났는지 여부
}

위의 조건을 만족하는 구조를 가진 객체를 "Iterable" 하다고 말한다.

iterator 객체에 대한 내용을 알았으니, 그러면 이제 지연평가가 무엇이고 이것이 iterator와 무슨상관관계가 있는지 명확히 알아보면 좋을 것 같다.

🏍 지연 평가의 의미와 Iterator의 상관관계

지연 평가(Lazy Evaluation)는 값을 필요할 때까지 계산하지 않는 전략이다.
즉, 어떤 값이 실제로 사용되기 전까지는 계산을 미루는 것이다.

반대 개념은 즉시 평가(Eager Evaluation)이며, 이는 코드가 실행될 때 모든 값을 즉시 계산한다.

// 즉시평가의 예시
const result = [1, 2, 3, 4, 5].filter(n => {
  console.log("checking", n);
  return n % 2 === 0;
});
// 콘솔 출력:
// checking 1
// checking 2
// checking 3
// checking 4
// checking 5
// -> result = [2, 4]

위와 같이 Array.prototype.filter은 즉시 원본 배열을 끝까지 순회하면서 평가한 뒤 새로운 배열을 리턴하게 된다.
필요하든 말든 전체를 평가하고 메모리에 저장하는 것이다.

하지만 지연 평가(Lazy Evaluation) 는 필요할 때에만 평가를 하는 방식이다.

// 지연평가의 예시
const iter = [1, 2, 3, 4, 5].values()

// 이제서야 평가가 시작됨
console.log(iter.next()); // checking 1, checking 2 -> { value: 2, done: false }
console.log(iter.next()); // checking 3, checking 4 -> { value: 4, done: false }

Array.prototype.values()의 호출결과는 원본 Array를 기반으로 한 Iterator 객체이다.
Image

즉, values의 호출 자체로는 Iterator 객체만 생성하여 메모리에 저장될 뿐 그 이상의 작업이 일어나지 않는다.
실질적인 평가는 next 메서드의 호출이 이루어질 때에만 비로소 진행되는 것이다.

따라서 필요에 따라 필요한 요소만 뽑아내어 평가하는 것이 가능해진다.

이 Iterator 객체는 prototype에 다시 동일하게 helper 함수들을 갖고 있는 객체이므로 마치 then 체이닝처럼 Iterator 객체를 받아서 next 메서드를 래핑한 새로운 Iterator 객체를 만들 수 있다.

이 iterator 체이닝이 바로 proposal-iterator-helpers 에서 제안하였던 가장 핵심적인 기능이다.

즉, 데이터의 크기와 상관없이 Stream 형태의 처리가 가능해진다는 것이다.

const iter = [10, 20, 30, 40, 50]
  .values() // ✅ 모든 요소를 참조하는 iterator 객체 생성
  .filter(n => n >= 20) // ✅ 특정 조건에 맞는 값만 통과시키는 filter iterator 래퍼 객체 생성
  .take(2); // ✅ 앞의 필터 결과 중 처음 2개까지만 순회 가능한 take iterator 래퍼 객체 생성

// ✅ 위에까지는 메모리에 iterator 객체만 존재할 뿐이다.
// ✅ 실제 평가는 이 iterator을 사용해 로직을 실행시키는 함수가 next를 호출하게 되면서 진행된다.
// ✅ 예를 들어 아래처럼 iterator 전용 메서드인 toArray의 호출은 iterator의 next를 호출하면서 "done:true" 일때까지의 평가를 진행한다.
const resultArray = iter.toArray(); // ✅ [20, 30]

위와 같이 iterator의 helpers 메서드의 추가 덕택에 배열에 대한 처리를 진행할 때 필요한 요소만 선택하여 순차적으로 처리 하는 형태의 연산이 가능해지게 되므로 불필요한 자원을 낭비하지 않을 수 있다는 장점을 갖게 된다.

profile
자라나라 프론트엔드 개발새싹!

0개의 댓글