Javascript

자바스크립트의 배신 타입스크립트는 Iterator 이름 충돌을 어떻게 해결했나

드리프트2 2025. 7. 13. 13:00


자바스크립트의 배신 타입스크립트는 Iterator 이름 충돌을 어떻게 해결했나

조용히 일어난 이름 전쟁

ECMAScript 2025(자바스크립트의 공식 명칭) 명세에, 개발자들의 오랜 숙원이었던 '이터레이터 헬퍼 메서드(iterator helper methods)'를 품은 새로운 `Iterator` 클래스가 추가되었습니다.

이는 `map`, `filter` 등 배열에서나 가능했던 편리한 기능들을 이제 모든 순회 가능한 객체에서 직접 사용할 수 있게 되었다는 반가운 소식입니다.

하지만 이 기쁨도 잠시, 타입스크립트 진영에서는 조용한 비상이 걸렸습니다.

새롭게 표준으로 들어온 `Iterator`라는 이름이, 이미 타입스크립트 생태계에서 핵심적인 역할을 하던 기존의 `Iterator` 타입과 정면으로 충돌했기 때문입니다.

한쪽은 자바스크립트 런타임에 실존하는 '클래스'를, 다른 한쪽은 타입 시스템에만 존재하는 '인터페이스'를 가리킵니다.

이름은 같지만 역할과 실체가 전혀 다른 두 존재가 공존해야 하는 상황이 발생한 것입니다.

이 글에서는 이 '이름 전쟁'이 왜 시작되었으며, 타입스크립트 팀이 기존 코드의 호환성을 지키면서도 새로운 표준을 우아하게 수용하기 위해 어떤 영리한 해결책을 내놓았는지 그 과정을 깊이 있게 파헤쳐 보고자 합니다.

이는 단순히 하나의 문제를 해결하는 과정을 넘어, 언어의 진화와 타입 시스템 설계의 정수를 엿볼 수 있는 흥미로운 여정이 될 것입니다.

이터레이터는 왜 중요할까

본격적인 이야기에 앞서, 왜 우리가 이터레이터 헬퍼 메서드에 열광하는지 잠시 짚고 넘어갈 필요가 있습니다.

기존에는 배열이 아닌 다른 순회 가능한 객체(예: `Map`, `Set`, `Generator`)에서 `map`이나 `filter` 같은 작업을 하려면, 먼저 `Array.from()`이나 전개 구문(`...`)을 사용해 배열로 변환하는 번거로운 과정을 거쳐야 했습니다.

const mySet = new Set([1, 2, 3, 4, 5]);

// 기존 방식: Set을 배열로 변환 후 map 사용
const newArray = Array.from(mySet).map(x => x * 2); 
// [2, 4, 6, 8, 10]

이터레이터 헬퍼 메서드는 이러한 불편함을 해소합니다.

두 가지 큰 장점이 있는데요.

첫째, 모든 순회 가능한 객체에서 '직접' 사용할 수 있는 일관된 API를 제공합니다.

둘째, 중간에 불필요한 배열을 생성하지 않고, 각 요소를 하나씩 처리하는 '지연 평가(lazy evaluation)' 방식으로 동작하여 메모리 효율성이 매우 뛰어납니다.

대용량 데이터를 다룰 때 그 차이는 더욱 극명해집니다.

// 새로운 방식: 이터레이터에서 직접 map 사용
const newIterator = mySet.values().map(x => x * 2);

// 필요할 때 값을 꺼내 씀
console.log(newIterator.next()); // { value: 2, done: false }
console.log(newIterator.next()); // { value: 4, done: false }

이처럼 강력한 기능이 표준으로 들어오면서, 자바스크립트에서의 데이터 처리 방식이 한 단계 진화하게 된 것입니다.

과거의 이터레이션과 타입스크립트

새로운 표준이 등장하기 전, 자바스크립트의 이터레이션 프로토콜은 매우 단순했습니다.

'이터레이터'란 그저 `{ done: boolean, value: any }` 형태의 객체를 반환하는 `.next()` 메서드를 가진 객체일 뿐이었습니다.

타입스크립트는 이러한 구조를 다음과 같은 핵심 인터페이스들로 표현했습니다.

// 과거의 핵심 타입 정의 (간략화)
interface Iterator<T> {
  next(): { done: boolean, value: T };
}

interface Iterable<T> {
  [Symbol.iterator](): Iterator<T>;
}

// 빌트인 이터레이터들은 Iterable이기도 했습니다.
interface IterableIterator<T> extends Iterator<T> {
  [Symbol.iterator](): IterableIterator<T>;
}

여기서 IterableIterator<T>는 배열의 values(), 제너레이터 함수 등이 반환하는 '빌트인 이터레이터'들의 타입이었습니다.

이들은 .next() 메서드를 가진 Iterator이면서, 동시에 for...of 문에서 사용될 수 있도록 Symbol.iterator 메서드도 가지고 있는, 즉 스스로가 '순회 가능한(Iterable)' 존재였습니다.

이 구조는 수년간 타입스크립트 생태계의 기반을 이루고 있었습니다.

새로운 시대, 새로운 충돌, 그리고 해결책

ECMAScript 2025에서 `Iterator` 클래스가 등장하면서 상황은 복잡해졌습니다.

이제 자바스크립트 세상에는 두 종류의 `Iterator`가 존재하게 된 것입니다.

1. **새로운 `Iterator` 클래스:** 전역에 존재하는 실제 값(`value`). 헬퍼 메서드들을 가지고 있습니다. 2. **기존의 `Iterator` 타입:** 타입 시스템에만 존재하는 타입(`type`). `.next()` 메서드만 정의되어 있습니다.

타입스크립트는 이 명백한 이름 충돌을 해결해야만 했습니다.

만약 단순히 전역 Iterator 클래스를 도입하면, 기존에 Iterator 타입을 사용하던 수많은 코드가 모두 망가질 것이기 때문입니다.

고민 끝에 타입스크립트 팀이 내놓은 해결책은 그야말로 절묘했습니다.

그들은 '기존의 것을 최대한 유지하면서, 새로운 것을 교묘하게 끼워 넣는' 전략을 사용했습니다.

해결의 핵심은 '역할의 분리'와 '이름의 재정의'에 있습니다.

첫째, 기존 Iterator 타입을 그대로 둡니다.

가장 중요한 원칙입니다.

과거 Iterator<T> 타입은 그대로 유지하여, 하위 호환성을 완벽하게 보장했습니다.

기존 코드는 아무런 수정 없이 계속 작동할 수 있습니다.

둘째, 새로운 '진짜' 이터레이터 객체를 위한 타입을 만듭니다.

기존 IterableIterator<T>의 역할을 대체할 새로운 타입, 바로 IteratorObject<T>를 도입했습니다.

이름에서 알 수 있듯이, 이는 '실제 이터레이터 객체'의 타입을 의미하며, 새로운 헬퍼 메서드들이 추가될 기반이 됩니다.

// 새롭게 도입된 핵심 타입
interface IteratorObject<T> extends Iterator<T> {
  [Symbol.iterator](): IteratorObject<T>;

  // 앞으로 여기에 헬퍼 메서드들이 추가될 예정
  map<U>(/* ... */): IteratorObject<U>;
  filter(/* ... */): IteratorObject<T>;
}

셋째, 전역 Iterator 클래스와 타입을 교묘하게 연결합니다.

이 부분이 가장 영리한 지점입니다.

타입스크립트는 전역 Iterator라는 '값(클래스)'을 선언하면서, 이 클래스의 인스턴스 타입이 Iterator가 아닌, 위에서 만든 IteratorObject가 되도록 설계했습니다.

이는 일반적인 클래스 선언 방식과는 다른데요, 여러 단계의 타입 선언과 '선언 병합(declaration merging)'이라는 타입스크립트의 고급 기능을 활용하여 이뤄집니다.

간단히 요약하면 다음과 같은 과정을 거칩니다.

  1. 내부적으로만 사용하는 추상 클래스 Iterator를 선언합니다.
  2. 전역에 IteratorObject 인터페이스를 선언하고, 여기에 모든 헬퍼 메서드들을 정의합니다.
  3. '선언 병합'을 통해 내부 Iterator 클래스의 인스턴스가 IteratorObject 인터페이스를 구현하도록 만듭니다.
  4. 마지막으로, 전역 변수 Iterator의 타입을 IteratorObject를 생성할 수 있는 '생성자 타입'으로 정의합니다.

이 복잡한 과정을 통해, 개발자 입장에서는 다음과 같은 명쾌한 결과가 만들어집니다.

// 전역 변수(클래스) Iterator는 '값'으로 존재합니다.
const arr = [1, 2, 3];
const iter = Iterator.from(arr); // OK!

// 이터레이터 객체의 타입은 'IteratorObject'입니다.
// iter 변수는 IteratorObject<number> 타입을 가집니다.
const mappedIter = iter.map(x => x * 2); // OK! 헬퍼 메서드 사용 가능

// 하지만 기존의 Iterator 타입은 그대로 유지됩니다.
function processOldIterator(it: Iterator<number>) {
  // ... 이 함수는 예전처럼 잘 작동합니다.
}

우리의 코드에는 무엇이 달라질까

이러한 변화는 타입스크립트 개발자에게 몇 가지 중요한 시사점을 줍니다.

첫째, 이제부터 이터레이터를 다룰 때 가장 정확하고 강력한 타입은 `Iterator`가 아닌 `IteratorObject`입니다.

새로운 헬퍼 메서드를 타입 시스템의 도움을 받으며 안전하게 사용하고 싶다면, 이 타입을 사용해야 합니다.

둘째, 만약 직접 이터레이터를 만들어야 한다면, 이제는 단순히 `.next()` 메서드만 구현하는 것을 넘어, 그 객체가 `Iterator.prototype`을 프로토타입 체인에 포함하도록 만들어야 합니다.

가장 쉬운 방법은 제너레이터 함수를 사용하는 것입니다.

function* myCustomIterator(): IteratorObject<number> {
  yield 1;
  yield 2;
  yield 3;
}

const iter = myCustomIterator();
const mapped = iter.map(x => x * 10); // OK!

셋째, 가장 중요한 것은, 타입스크립트 팀이 얼마나 '하위 호환성'을 중요하게 생각하는지를 엿볼 수 있다는 점입니다.

그들은 언어 표준의 거대한 변화 앞에서, 기존 생태계를 보호하기 위해 이토록 복잡하고 정교한 타입 시스템 설계를 마다하지 않았습니다.

이러한 노력 덕분에 개발자들은 새로운 기능을 마음껏 즐기면서도, 과거의 코드가 망가질 걱정을 덜 수 있게 된 것입니다.

자바스크립트라는 역동적인 언어 위에서 안정적인 타입 시스템을 구축하는 것이 얼마나 어려운 일인지, 그리고 그것을 위해 얼마나 깊은 고민이 필요한지를 보여주는 훌륭한 사례라고 할 수 있습니다.