Node.js/Javascript

[함수형프로그래밍] 2. 이터러블 이터레이터 프로토콜 근데 이제 range 함수 예제를 곁들인

후뿡이 2024. 3. 26. 09:00

이 강의는 Inflearn 유인동 님의 강의 함수형 프로그래밍과 Javacsript ES6+ 를 수강하고 작성한 글입니다.

 

🐳 이터러블 ? 이터레이터 ?

함수형 프로그래밍은 함수 사용, 불변 데이터를 중심을 두고 있습니다.

이터러블 이터레이터는 이 함수형 프로그래밍 패러다임에서 반복 가능한 프로토콜을 지원하는 모든 데이터 구조에 대해 작동할 수 있는 프로토콜입니다 !

그렇기 때문에 이터러블 이터레이터 프로토콜을 이용해 함수들을 조합하고 재사용하는데 중요한 역할을 하게 됩니다.

그렇다면 Iterable , Iterator는 JS에서 어떤 의미일까 ?

 

이번 포스팅을 통해 Iterable , Iterator에 대해서 알아봅시다 !

 

🐳 Iterable Protocol

먼저 '프로토콜'이란 무엇일까요 ? ( 다양한 분야에서 사용되는 단어지만 이곳에서는 Computing 분야의 의미로 사용합니다 )

통신 및 컴퓨터 네트워킹에서 데이터가 전송되는 방식을 결정하는 일련의 정의된 규칙 및 규정 - Wikipedia

 

프로토콜이란 데이터를 주고받기 위한 정의된 약속이라고 생각하면 될 것 같습니다.

 

이터러블 프로토콜 또한 데이터를 주고 받기 위한 하나의 정의된 약속인 것입니다.

그렇다면 어떠한 약속을 지켜야 할까요 >?

 

이터러블 프로토콜이란 이터레이터를 반환하는 메소드를 제공해야 한다는 약속을 의미합니다.

JavaScript에서 이터레이터를 반환하는 메소드는 [Symbol.iterator] () 가 있습니다.

그렇기 때문에 [Symbol.iterator] () 메소드를 가지고 있는 객체는 이터러블하다. 이터러블 프로토콜을 따른다라고 할 수 있습니다.

 

그렇다면 대표적인 이터러블한 객체인 배열의 모습을 한 번 살펴봅시다.

 

브라우저에서 간단한 배열을 만든 후 로그를 찍어 보면 [[prototype]] 이러는 프로퍼티를 확인할 수 있습니다.

 

 

 

쭉 따라가다 보면 위의 사진처럼 Symbol.iterator 라는 함수 ( f ) 를 만날 수 있습니다.

배열은 prototype을 통해 Symbol.iterator를 가지고 있기 때문에 이터러블 프로토콜을 따르는 이터러블한 객체라고 할 수 있습니다.

 

이제 이터러블 프로토콜은 이터레이터를 반환하는 메소드를 가져야 한다는 것은 알겠습니다.

 

그런데 이터레이터는 무엇일까요 ?

 

🐳 Iterator Protocol

이터레이터 프로토콜이란

객체가 값의 시퀀스를 생산하는 메커니즘을 제공해야 한다는 요구사항입니다.

 

말이 너무 어렵지요 ? 우리는 JavaScript를 사용 중이니 JavaScript에서의 의미를 살펴봅시다.

 

JS에서 이터러블 프로토콜이란 이터레이터를 반환하는 메소드 즉 [Symbol.iterator]() 를 가진 객체라고 설명했습니다.

그러면 [Symbol.iterator]() 에는 뭐가 들어있는지 살펴봅시다.

const array = [1,2,3];

const iterator = array[Symbol.iterator]();

console.log(iterator)

 

 

로그를 찍어보는 next() 라는 함수가 눈에 띕니다.

JS에서는 값의 시퀀스를 생산하는 메커니즘이 next() 메소드를 통해 이루어 집니다.

그렇다면 이번에는 next() 메소드를 호출해 봅시다.

 

 

처음에 우리가 선언한 배열은 const array = [1,2,3] 이었다.
next()함수를 호출할 때마다 순차적으로 아래와 같은 값이 나옵니다.

{value: 1, done: false}
{value: 2, done: false}
{value: 3, done: false}
{value: undefined, done: true}

 

value에는 배열을 반복하면서 값들을 추출하고 있고, 완료여부를 의미하는 done에는 false가 나타납니다.

몇 번 반복한 후 배열의 순회를 마치면 value : undefine, done: true 가 나오는 것을 확인할 수 있습니다.

 

위의 코드들을 통해서 JS에서 이터레이터 프로토콜의 값의 시퀀스를 생산하는 메커니즘은

이터레이터 객체 내부의 next() 라는 메소드를 통해 이루어지고

next() 메소드는 { value:any , done : boolean } 형태의 객체를 반환한다는 것을 확인할 수 있습니다.

 

🐳 Iterable , Iterator Protocol 사용 시 기능

이터러블/이터레이터 프로토콜을 따르는 객체들은 JS에서 아래의 기능들을 사용할 수 있습니다.

  • for ... of 반복문
  • 전개연산자

그렇기 때문에 위의 기능들을 사용할 수 있는 JS 객체들은 객체 내부에 [Symbo.iterator]() 가 정의되어 있는

이터러블한 객체라고 볼 수 있습니다.

 

🐳 직접 이터러블 객체를 만들어 보자 !

0. 목표

이번 예제에서 사용할 코드는 python 사용자라면 익숙하실 range 함수입니다.

JS에서는 함수 자체도 하나의 객체이기 때문에 range함수를 객체로 구현하겠습니다.

 

구현 목표

  1. 이터러블 이터레이터 프로토콜 적용
  2. start,end,step의 인자를 받아 start 이상, end 미만, step 만큼 증가하는 배열 생성
  3. start, end는 입력하지 않을 시에 0,1로 기본값 설정

 

자 출발해 봅시다 !

1. 이터러블하지 않은 일반 객체 생성

먼저 [Symbol.iterator]() 를 가지지 않는 일반 객체를 하나 생성해 줍시다.

이번 목표는 입력한 숫자까지의 값을 만들어 주는 range 객체를 만들어 줄 겁니다.

const range = (start, end, step) => {};

for (const element of range) {
  console.log(element);
}

 

 

위의 range 함수는 [Symbol.iterator]() 를 가지지 않는 객체이기 때문에 위의 코드처럼

for...of 를 사용하면 이 객체는 이터러블 하지 않다는 에러를 발생시킵니다.

 

2. 이터러블한 객체로 만들기 - [Symbol.iterator]() 메소드 구현

 

이 객체를 한 번 이터러블한 객체로 만들어 봅시다 !

이터러블한 객체가 되기 위해선 뭐가 필요할까요 ?

[Symbol.iterator] () 함수를 가져야 합니다 !

이터러블하지 않은 객체인 range가 [Symbol.iterator] () 함수를 가지는 객체를 리턴하도록 구현해 줍시다.

 

const range = (start, end, step) => {
  // 객체를 리턴
  return {
    // 객체는 [Symbol.iterator]를 성분으로 가짐
    // [Symbol.iterator] : 함수
    [Symbol.iterator]: () => {
      let current = start;
      return {
        next: () => {
          if ((step > 0 && current < end) || (step < 0 && current >= end)) {
            const result = { value: current, done: false };
            current += step;
            return result;
          } else {
            return { value: undefined, done: true };
          }
        },
      };
    },
  };
};

 

코드는 위와 같습니다.

range 자체는 하나의 함수이고 이 함수는 Symbol.iterator 메소드를 가지는 객체를 리턴하게 됩니다.

그리고 [Symbol.iterator] () 함수는 내부에 next() 메소드를 가집니다.

내부 구현은 start 이상, end 미만의 수를 step 간격으로 리턴하게 됩니다.

 

그렇다면 이제 한 번 사용해 볼까요 ?

 

const rangeIterator = range(0, 10, 2)[Symbol.iterator]();

console.log(rangeIterator.next());
console.log(rangeIterator.next());
console.log(rangeIterator.next());
console.log(rangeIterator.next());
console.log(rangeIterator.next());
console.log(rangeIterator.next());

 

먼저 range 함수를 호출해 Symbol.iterator를 가지는 이터러블한 객체를 생성해 줍니다.

그리고 [Symbol.iterator] () 메소드를 호출해 이터레이터 객체를 생성합니다.

그러므로 rangeIterator 객체는 이터레이터 객체가 됩니다 !

( 이터러블은 무엇이고 이터레이터는 무엇인지 위의 글을 다시 확인해 주세요 )

 

이터레이터 프로토콜을 따르는 객체는 next() 메소드로 호출이 가능해야겠지요?

next() 메소드를 6번 호출하면서 결과를 확인해 봅시다.

 

 

우리가 원했던 결과인 0 부터 10 미만의 숫자를 2의 간격으로 얻었습니다.

또한 반복이 끝난 후에는 done = true 가 되는 것 또한 확인할 수 있습니다.

 

이처럼 이터러블 , 이터레이터 프로토콜을 따라 [Symbol.iterator] () 메소드를 구현하면

커스텀 이터러블 객체를 생성할 수 있게 됩니다.

 

이제는 조금 더 구체화해서 range(10) 처럼 입력할 경우 start = 0, step = 1 이 되도록 추가 구현을 진행해 봅시다.

 

3. 추가 구현

이번에는 range(10) 처럼 하나의 인수만 입력한 경우 start = 0 , end = 10, step = 1 이 되도록 구현해 봅시다.

const range = (start, end, step) => {

  if (!end) {
    end = start;
    start = 0;
    step = 1;
  }

코드 블럭 상단에 위와 같은 설정을 추가해 주면 될 것입니다.

 

그런 다음 원하는 결과가 나오는지 한 번 확인해 봅시다.

const rangeIterator = range(3)[Symbol.iterator]();

console.log(rangeIterator.next());
console.log(rangeIterator.next());
console.log(rangeIterator.next());
console.log(rangeIterator.next());

 


원하는 결과처럼 0 이상 3 미만의 숫자가 1씩 증가하는 것을 확인할 수 있습니다.

마지막으로 원하는 이터러블한 객체라면 for... of 반복문도 사용할 수 있어야겠지요?

for ... of 반복문까지 가능한지 확인해 봅시다.

 

아래의 반복문 코드를 실행해 봅시다.

for (const num of range(1,7,2)) {
    console.log('num : '+ num)
}

 

 

위의 사진처럼 원하는 for ... of 반복문을 사용 가능하며 원하는 결과가 나오는 것을 확인할 수 있습니다.

 

 

🐳 Jest를 활용해 range함수를 테스트해보자

지금까지는 실제 코드를 실행하면서 확인해 봤다면 이번에는 Jest를 활용해 테스트를 해봅시다.

 

테스트 내용은 아래와 같습니다.

  1. for ... of 반복문이 사용 가능한지
  2. 구조분해 할당이 가능한지
  3. 파라미터라이즈드 테스트 코드

먼저 이터러블 / 이터레이터 프로토콜을 만족하는 경우 구조분해할당과 for ... of 반복문을 사용할 수 있기 때문에 range 함수가 구조분해할당과 for ... of 반복문을 사용할 수 있는지를 테스트합니다.

테스트 코드를 구현해 봅시다 !

 

테스트 코드

const range =require('./file')

describe('range 함수 테스트', function () {
    it('range 함수는 for...of 반복문을 사용할 수 있다.', () => { // create range from 1 to 3
        const sut = range(1,4,1)
        const expected = [1, 2, 3];
        let result = [];

        for (const value of sut) {
            result.push(value);
        }

        expect(result).toStrictEqual(expected);
    });

    it('range 함수는 구조분해할당을 사용할 수 있다.', () => {
        const [first, second, third] = range(1, 4,1); // should destructure to [1, 2, 3]
        expect(first).toBe(1);
        expect(second).toBe(2);
        expect(third).toBe(3);
    });

    it.each([
        [0, 5, 1, [0, 1, 2, 3, 4]],
        [3, 10, 3, [3, 6, 9]],
        [3, undefined, undefined, [0, 1, 2]],
        [0, undefined, undefined, []],
        [0, 10, 3, [0, 3, 6, 9]],
        [0, 0, 1, []],
        [0, 1, 1, [0]],
    ]) (
        'range(%i, %i, %i)를 배열로 만들면 %p를 리턴한다.', (start, end, step, expected) => {
            const result = [...range(start, end, step)];
            expect(result).toEqual(expected);
        }
    )
});

 

위의 테스트목표를 만족시킨 테스트 코드를 구현했습니다.

처음은 range 함수가 이터러블한 객체인지 확인하기 위한 테스트입니다.

그다음 파라미터 라이즈드 테스트를 이용해 다양한 테스트를 생성해 테스트합니다.

테스트코드는 테스트코드 자체로 하나의 문서이고 발생할 수 있는 에러들을 미연에 방지하기 위한 수단이므로
성공할 것 같은 테스트코드만 작성하는 것이 아니라 실패할 것 같은 테스트를 작성하는 것이 중요합니다.
그래서 0 근처의 경곗값 들을 위주로 테스트를 진행했습니다.
( 이번 range 함수는 start, end은 0 이상의 정수, step은 1 이상의 자연수라고 가정하고 진행하겠습니다. )

 

작성한 테스트코드를 실행해 봅시다 !

 

 

모든 테스트가 통과했습니다 !

🐳 후기

이번에는 이터러블 / 이터레이터 프로토콜을 간단한 range 함수 구현과 함께 살펴보았습니다.
( 예외 처리 및 step이 음수인 경우 등은 구현하지는 않았지만 ... )
확실히 그냥 강의 또는 글로 보는 것이 아니라
실제로 코드를 구현하고 설명하려고 하니 이해가 더 잘 되었습니다.

이 글을 끝까지 읽어 주신 여러분들도
이터러블 / 이터레이터 프로토콜을 이해하시는 데 조금이나마 도움이 되셨으면 좋겠습니다.