munifico.github.io

munifico's anything page

Follow me on GitHub

이터레이션 프로토콜

이터레이터는 그 자체로 크게 쓸모가 있다기보다는, 더 쓸모 있는 동작이 가능해지도록 한다는데 의미가 있습니다. 이터레이터 프로토콜은 모든 객체를 이터러블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 인스턴스를 배열처럼 순회할 수 있습니다.

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}`);
}

//물론 get 메서드를 만들어 return this.messages; 를 통해 배열을 리턴시켜 순회도 가능함..

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

class Log {
    constructor() {
        this.messages = [];
    }
    //...
    [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");

for(let entry of log){
    console.log(entry);
}

/*
{ message: 'first day at sea', timestamp: 1567401494282 }
{ message: 'spotted whale', timestamp: 1567401494282 }
{ message: 'spotted another vessel', timestamp: 1567401494282 }
*/

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

단순한 예제로, 피보나치 수열을 만들어 봅시다. 피보나치 수열의 숫자를 구하기는 전혀 어렵지 않습니다. 다만 앞에 있는 숫자가 무엇인지만 알면 됩니다. 피보나치 수열의 숫자는 수열 안에서 자신보다 앞에 있는 두 숫자의 합입니다. 수열은 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 문으로 빠져나옵시다.

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

//물론 피보나치 수열은 이렇게도 만들어 볼 수 있습니다.
let ii = 0;
let a = 0, b = 1;
do{
    let c = b;
    b += a;
    a = c;
    console.log(b);
}while(++ii < 9);

이전 : preview
다음 : 제너레이터
목차