LHJ

I'm a FE developer.

12.1 이터레이션 프로토콜

19 May 2020 » js_lj

12.1 이터레이션 프로토콜

이터레이터는 그 자체로 크게 쓸모가 있다기보다는, 더 쓸모 있는 동작이 가능해지도록 한다는데 의미가 있습니다.
이터레이터 프로토콜은 모든 객체를 이터러블(iterable) 객체로 바꿀 수 있습니다.
메시지에 타임스탬프를 붙이는 로그 클래스가 필요하다고 생각해 봅시다.
내적으로 타임스탬프가 붙은 메시지는 배열에 저장합니다.

class Log {
    constructor() {
        this.messages = [];
    }
    add(message) {
        this.messages.push({ message, timestamp: Date.now() });
    }
}

지금까지는 좋습니다만, 로그를 기록한 항목을 순회(iterate)하고 싶다면 어떻게 해야 할까요?
물론 log.messages에 접근할 수는 있지만, log를 배열을 조작하듯 할 수 있다면 더 좋을 겁니다.
이터레이션 프로토콜을 사용하면 가능합니다.
이터레이션 프로토콜은 클래스에 심볼 메서드 Symbol.iterator가 있고 이 메서드가 이터레이터처럼 동작하는 객체, 즉 value와 done 프로퍼티가 있는 객체를 반환하는 next 메서드를 가진 객체를 반환한다면 그 클래스의 인스턴스는 이터러블 객체라는 뜻입니다.
Log 클래스에 Symbol.iterator 메서드를 추가합시다.

class Log {
    constructor() {
        this.messages = [];
    }
    add(message) {
        this.messages.push({ message, timestamp: Date.now() });
    }
    [Symbol.iterator]() {
        return this.messages.values();
    }
}

이제 Log 인스턴스를 배열처럼 순회할 수 있습니다.

class Log {
    constructor() {
        this.messages = [];
    }
    add(message) {
        this.messages.push({ message, timestamp: Date.now() });
    }
    [Symbol.iterator]() {
        return this.messages.values();
    }
}

const log = new Log();
log.add("first day at sea");
log.add("spotted whale");
log.add("spotted another vessel");
// ...

// 로그를 배열처럼 순화합니다!
for (let entry of log) {
    console.log(`${entry.message} @ ${entry.timestamp}`);
}

이 예제에서는 messages 배열에서 이터레이터를 가져와 이터레이터 프로토콜을 구현했지만, 다음과 같이 직접 이터레이터를 만들 수도 있습니다.

class Log {
    constructor() {
        this.messages = [];
    }
    add(message) {
        this.messages.push({ message, timestamp: Date.now() });
    }
    [Symbol.iterator]() {
        let i = 0;
        const messages = this.messages;
        return {
            next() {
                if (i >= messages.length)
                    return { value: undefined, done: true };
                return { value: messages[i++], done: false };
            }       
        }   
    }
}

const log = new Log();
log.add("first day at sea");
log.add("spotted whale");
log.add("spotted another vessel");

const it = log[Symbol.iterator]();
it.next();
it.next();
it.next();
it.next();
// ...

지금까지 살펴본 예제는 책의 페이지나 타임스탬프가 붙은 로그처럼 숫자가 정해진 요소들을 순회했습니다.
하지만 이터레이터는 무한한 데이터에도 사용할 수 있습니다.

단순한 예제로, 피보나치 수열을 만들어 봅시다.
피보나치 수열의 숫자를 구하기는 전혀 어렵지 않습니다.
다만 앞에 있는 숫자가 무엇인지만 알면 됩니다.
피보나치 수열의 숫자는 수열 안에서 자신보다 앞에 있는 두 숫자의 합입니다.
수열은 1과 1로 시작하므로 다음 숫자는 1+1인 2입니다.
그 다음 숫자는 1+2인 3, 그다음 숫자는 2+3인 5, 이런 식으로 계속됩니다.
수열은 이런 형태입니다.

1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ...

피보나치 수열은 무한히 계속되고, 프로그램에서는 몇 번째 숫자까지 계산해야 할지 알 수 없으므로 이터레이터를 사용하기에 알맞습니다.
이 예제와 이전 예제의 차이는 이 예제의 이터레이터가 done에서 절대 true를 반환하지 않는다는 것뿐입니다.

class FibonacciSequence {
    [Symbol.iterator]() {
        let a = 0, b = 1;
        return {
            next() {
                let rval = { value: b, done: false };
                b += a;
                a = rval.value;
                return rval;
            }
        }   
    }
}

for … of 루프로 FibonacciSequence 인스턴스를 계산하면 무한 루프에 빠집니다.
피보나치 수열은 무한하니까요.
무한 루프에 빠지지 않도록, 10회 계산한 뒤 break 문으로 빠져나옵시다.

class FibonacciSequence {
    [Symbol.iterator]() {
        let a = 0, b = 1;
        return {
            next() {
                let rval = { value: b, done: false };
                b += a;
                a = rval.value;
                return rval;
            }
        }   
    }
}

const fib = new FibonacciSequence();
let i = 0;
for (let n of fib) {
    console.log(n);
    if (++i > 9) break;
}