이 강의는 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함수를 객체로 구현하겠습니다.
구현 목표
- 이터러블 이터레이터 프로토콜 적용
- start,end,step의 인자를 받아 start 이상, end 미만, step 만큼 증가하는 배열 생성
- 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를 활용해 테스트를 해봅시다.
테스트 내용은 아래와 같습니다.
- for ... of 반복문이 사용 가능한지
- 구조분해 할당이 가능한지
- 파라미터라이즈드 테스트 코드
먼저 이터러블 / 이터레이터 프로토콜을 만족하는 경우 구조분해할당과 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이 음수인 경우 등은 구현하지는 않았지만 ... )
확실히 그냥 강의 또는 글로 보는 것이 아니라
실제로 코드를 구현하고 설명하려고 하니 이해가 더 잘 되었습니다.
이 글을 끝까지 읽어 주신 여러분들도
이터러블 / 이터레이터 프로토콜을 이해하시는 데 조금이나마 도움이 되셨으면 좋겠습니다.
'Node.js > Javascript' 카테고리의 다른 글
[함수형프로그래밍] 3. Reduce 함수 ( with Test ) (2) | 2024.04.01 |
---|---|
[함수형프로그래밍] 0. 함수형 프로그래밍이란 ? (0) | 2024.03.14 |
[함수형프로그래밍] 1. 일급함수란 ? (0) | 2024.03.12 |
[Javascript] 날짜 구하기 / 문자열 앞에 문자 더하기 (0) | 2023.06.04 |