[JS 딥다이브] Iterable 객체
개요
JS 개발을 하다보면, Iterator, Array-like 같은 키워드를 볼 수 있고, 이런 자료형이어야 작동하는 프로세스들을 볼 수 있었을 것이다.
JS ES6+ 에는 새로운 문법이나 Built-in Object 뿐만 아니라, protocols(표현법들)도 추가되었다. 이 protocol은 일정 규칙만 충족한다면, 어떠한 객체에서 의해서도 구현될 수 있다.
이러한 프로토콜은 2가지가 있다.
- Iterable protocol
- Iterator protocol
공통된 동일한 반복가능한(Iterable, 이터러블) 특징을 가진, 객체는 배열을 일반화한 객체이다.
이터러블 이라는 개념을 사용하면, 어떤 객체에든 for - of 문 같은 반복문을 적용할 수 있다.
- 배열이 가장 대표적인 이터러블에 속하고
- 배열이 아닌 객체, 이 객체가 어떤 것들의 컬렉션(Map, Set 등)을 나타내고 있는 경우, for - of 문을 적용할 수만 있다면, 컬렉션을 순회하는데 유용할 것이다.
이것을 가능하게 할 수 있도록 지켜야하는 Iterable, Iterator protocol 을 지켜서 생성되는 이터러블객체(Iterable) 에 대해 알아본다.
본론
📍 Iterable Protocol
Iterable protocol 은 Javascript 객체들이, for~of 문 같은 어떠한 value 들이 loop 되는 것과 같은 iteration 동작을 정의하거나, 사용자 정의하는 것을 허용하는 것에 대한 약속이다.
기본적으로 일반 JS Object type 에서는 이런 반복문 기능을 지원하지 않는 반면에, 특정 Array, Map 과 같은 Built-in Object는 default 가 Iteration 동작으로 허용된, Built-in iterables 이다.
그래서, for~of 문 같은 것은, 사용할 수 있는 것이다.
/**
* 기본적인 Object type 은 Iterable 이 아니다.
* 그래서, for-of 문을 지원하지 않는다.
*/
let range = {
from: 1,
to: 5,
};
for (let num of range) {
console.log(num);
}
// TypeError: range is not iterable
Symbol.iterator 🔍
iterable(반복 가능) 하기 위해서, object는 @@iterator 메소드를 구현해야하고 이는 곧, Symbol.iterator 프로퍼티가 존재해야 한다는 것이다. 이를 만족하는 반복 가능한 객체가, iterable 이다.
- Symbol.iterator 키에 대한 값은 Function 이다.
- 이 function 은 Object를 반환
- arguments(인자)가 없다.
- iterator protocol을 따른다.
결국, 어떤 object(객체)가 반복가능(iterate)되어야 한다면, 이 객체의 @@iterator 메소드가 인수없이 호출되고, 반환된 iterator는 반복을 통해서 획들할 값들을 얻을 때 사용된다.
내장된 Built-in Object 중 Iterable 객체를 default 로 만들어내는 것에는 다음이 같다.
String
Array
TypeArray
Map
Set
그리고, 어떤 객체가 Iterable 이라면, 그 객체에 대해 지원하는 기능들은 대표적으로 다음과 같다.
for ~ of 문
spread 연산자
Destructuring Assignment
- 기타
iterable 을 arguments 로 받는 함수
/**
* String Object 도 default Iterable 객체 중 하나이다.
* 고로, for ~ of 문 같은 기능을 지원한다.
*/
const myStr = "hi";
console.log(myStr[Symbol.iterator]); // [Function: [Symbol.iterator]]
for (let chr of myStr) {
console.log(chr);
}
// h
// i
이어서 그럼, 부가설명에 있는 것 중에 "Iterator protocol을 따른다." 에 대해 알아야 한다.
- 💡 Iterable 과 Iterator 를 혼동할 수 있으니, 구분을 잘 해야한다.
📍 Iterator Protocol
앞에서
iterable 객체는 iterable protocol 을 만족한다. 이것은 즉,
Symbol.iterator
프로퍼티에 특별한 형태의 함수가 저장되어 있다. 라는 것과 같다라고 했다.
Iterable protocol 을 만족하려면, Symbol.iterator
속성에 저장되어 있는 함수는 Iterator 객체를 반환 해야한다.
🔍 Iterator 객체
iterator protocol은 value(finite Or Infinite) 들의 Sequence 를 만드는 표준 약속을 정의한다.
- 이는, 객체가 next( ) 메소드를 가지고 있고, 다음 규칙에 따라 구현되어만 있다면, 그 객체는 Iterator 이다.
/**
* 일반 객체 -> 이터러블로 변환
*/
let range = {
from: 1,
to: 5,
// Symbol.iterator 프로퍼티 존재 🔍
[Symbol.iterator]() {
this.current = this.from;
return this;
},
// next 메소드 프로퍼티 존재
// for - of 반복마다, 이 next( ) 가 호출 됨 🔍
next() {
if (this.current <= this.to) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
},
};
// 객체 -> 이터러블 변환 후, 이터러블 기능 중 for - of 문 사용가능
// range 객체 -> range 이터러블
for (let num of range) {
console.log(num);
}
// 1
// 2
// 3
// 4
// 5
위에 코드에는 부가적인 설명이 필요하다.
이터러블 객체를 생성하는데 핵심은
관심사의 분리(SoC, Separation of Concern
에 있다.
위에 코드는 이를 고려한 것이다.
range
객체에는 메소드next()
가 없다.- 대신
range[Symbol.iterator]()
를 호출해서 만든 이터레이터 객체와 이 객체의 메소드인next()
에서 반복에 사용될 값을 만들어낸다. - 이렇게 하면, 이터레이터 객체와 반복 대상인 객체를 분리할 수 있다.
또 다른 예를 보자.
/**
* "generator" 로 생성한 iterable 테스트
*/
let strIterator = "hello"[Symbol.iterator]();
while (true) {
let result = strIterator.next();
if (result.done) break;
console.log(result.value);
}
// h
// e
// l
// l
// o
- 명시적으로 이터레이터를 호출하는 경우는 거의 없긴 하다.
- 하지만, 이 방법을 사용하면,
for-of 문
을 사용하는 것보단 반복 과정을 더 잘 통제 할 수 있다는 장점이 있다. - 물론,
for-of문
도중에도break
같은 키워드로 중간제어 등이 가능하긴 하다.
📍 Generator 함수
위에 관심사의 분리(SoC)
방식으로, iterable 객체를 만드는 방법을 확인했었다.
그런데, 이렇게, Iterator 객체를 반환하는 규칙을 지켜서 정석으로 Iterable 객체를 생성하면 코드자체가 길고, 더 복잡해 질 수 있다.
그래서 Generator 함수 를 사용하면 좀 더 간편하게 Iterable 객체를 생성할 수 있다.
Generator 함수 역시, 직접 Iterable 객체를 생성할 수 있는 방법 이다. 즉, Generator 함수는 iterable 객체를 반환 하는 특별한 함수이다.
Generator 함수 정의 🔍
// generator 함수 선언하기
function* gen1() {
// ...
}
// 표현식으로 사용하기
const gen2 = function* () {
// ...
}
// 메소드 문법으로 사용하기
const obj = {
* gen3() {
// ...
}
}
- Generator 함수를 호출하 객체가 생성되는 데, 이 객체는 Iterable protocol를 만족한다.
- 즉,
Symbol.iterator
프로퍼티를 갖고 있다.
function* gen1() {
// ...
}
// `gen1`를 호출하면 iterable이 반환됩니다.
const iterable = gen1();
iterable[Symbol.iterator]; // [Function]
yield
키워드 🔍
Generator 함수 안에서는 yield
라는 특별한 키워드를 사용할 수 있다.
yield
키워드는return
키워드와 유사한 역할을 한다.- iterable의 기능을 사용할 때,
yield
키워드 뒤에 있는 값들을 순서대로 넘겨준다.
/**
* Generator 예제
* [형식]
* - function* GenName() {}
*/
function* numberGen() {
yield 1;
yield 2;
yield 3;
}
let myNumberGen = numberGen();
console.log(myNumberGen); // Object [Generator] {}
for (let n of myNumberGen) {
console.log(n);
}
// 1
// 2
// 3
yield*
표현식을 사용하면, 다른 Generator 함수에서 넘겨준 값을 대신 넘겨줄 수도 있다.
function* numberGen1() {
yield 1;
yield 2;
}
function* numberAndStrGen() {
yield* numberGen1();
yield "a";
yield "b";
}
let myGen = numberAndStrGen();
console.log(myGen); // Object [Generator] {}
for (let value of myGen) {
console.log(value);
}
// 1
// 2
// a
// b
Generator 사용시 주의점 🔍
- Generator 함수로부터 생성된 iterable 은 한 번만 사용할 수 있다.
- Generator 함수 내부에서 정의된 일반 함수에서는
yield
키워드를 사용할 수 없다.
// Generator 함수로부터 생성된 iterable은 한 번만 사용될 수 있습니다.
function* gen() {
yield 1;
yield 2;
yield 3;
}
const iter = gen();
for (let n of iter) {
// 잘 출력됩니다.
console.log(n);
}
for (let n of iter) {
// `iter`는 한 번 사용되었으므로, 이 코드는 실행되지 않습니다.
console.log(n);
}
// Generator 함수 내부에서 정의된 일반 함수에서는 `yield` 키워드를 사용할 수 없습니다.
function* gen2() {
// 아예 문법 오류가 납니다. (Unexpected token)
function fakeGen() {
yield 1;
yield 2;
yield 3;
}
fakeGen();
}
📍 Generator 와 Iterator
Generator 함수로부터 만들어진 객체는 일반적인 iterable처럼 쓸 수 잇지만, iterator와 관련된 특별한 성질을 갖고 있다.
iterable protocol 과 iterator protocol을 동시에 만족한다.
즉,
Symbol.iterator
프로퍼티를 생성해서, iterator 를 생성하는 과정 같은 것이 필요없이, 바로next
메소드를 호출할 수 있다.
function* gen() {
// ...
}
const genObj = gen();
genObj[Symbol.iterator]().next === genObj.next; // true
function* gen() {
yield 1;
yield 2;
yield 3;
}
const iter = gen();
iter.next(); // { value: 1, done: false }
iter.next(); // { value: 2, done: false }
iter.next(); // { value: 3, done: false }
iter.next(); // { value: undefined, done: true }
- generator 함수 안에서
return
키워드를 사용하면, 반복이 바로 끝나면서next
메소드에서 반환되는 객체의 속성에 앞의 반환값이 저장된다. 다만,return
을 통해 반환된 값이 반복 절차에 포함되지는 않는다.
function* gen1() {
yield 1;
return 2; // generator 함수는 여기서 종료
yield 3;
}
const iter1 = gen1();
iter.next(); // { value: 1, done: false }
iter.next(); // { value: 2, done: true }
iter.next(); // { value: undefined, done: true }
for (let v of gen1()) {
console.log(v);
}
// 1
- generator 함수로부터 생성된 객체의
next
메소드에 인자를 주어서 호출하면, generator 함수가 멈췄던 부분의yield
표현식의 결과값은 앞에서 받은 인자가 된다.
function* gen2() {
const received = yield 1;
console.log(received);
}
const iter2 = gen2();
console.log(iter2.next()); // { value: 1, done: false }
console.log(iter2.next("hello"));
// hello << 내부 generator 에서의 console 출력
// { value: undefined, done: true } << 기존에 실질적인 iterator 객체순회는 끝났었으므로, undefined
📍 유사배열 과 이터러블
비슷해 보이지만, 헷갈리기 쉬운 두 개념이 있다. 짚고가자.
이터러블(Iterable)
:Symbol.iterator
가 구현된 객체유사배열(Array-like)
: 인덱스와length
프로퍼티가 있어서 배열처럼 보이는 객체
JS로 문제를 해결할 때, 이터러블 객체나, 유사 배열 객체 또는 둘 다인 객체를 만날 수 있다.
- 둘 다인 경우는 이터러블 객체이면서, 유사배열 객체인
String
이 대표적 - 이터러블 객체라고, 유사 배열 객체가 되는 건 아니고
- 유사 배열 객체라고, 이터러블 객체가 되는 것도 아니다.
- 이터러블 객체에는
length
프로퍼티 같은 것은 없고 - 유사 배열 객체에는
for-of문
같은 iterable 제공기능을 사용할 수 없는 것과 같다.
또한 이터러블, 유사배열은 Array
가 아니기 때문에, Array 메소드등을 원래는 지원하지 않는다.
하지만, 평소에 개발하면서, Array 에서 제공하는 메소드들을 사용해야 하는 경우가 많다. 이터러블, 유사배열에서도 마찬가지인데, 이때 어떻게 할 수 있을까 ?
📍 Array.from
범용 메소드 Array.from
은 이터러블이나 유사배열을 받아 진짜 Array
를 만들어준다.
이 메소드 처리를 해주면, 이터러블이나 유사배열에 배열 메소드들을 사용할 수 있다.
/**
* Array.from(Iterable 또는 Array-like)
*/
function* myGenerator1() {
yield 1;
yield 2;
}
let myGeneratorInst = myGenerator1();
let myArrayLike = {
0: "a",
1: "b",
length: 2,
};
// myGeneratorInst.push(1); // TypeError: myGeneratorInst.push is not a function
// myArrayLike.push("c"); // TypeError: myArrayLike.push is not a function
let arrFromMyGeneratorInst = Array.from(myGeneratorInst);
let arrFromMyArrayLike = Array.from(myArrayLike);
arrFromMyGeneratorInst.push(3); // 성공
arrFromMyArrayLike.push("c"); // 성공
console.log(arrFromMyGeneratorInst); // [ 1, 2, 3 ]
console.log(arrFromMyArrayLike); // [ 'a', 'b', 'c' ]
Array.from
은 객체를 받아 이터러블이나 유사 배열인지 조사한다.- 넘겨 받은 인자가 이터러블이나 유사 배열인 경우, 새로운 배열을 만들고 객체의 모든 요소를 새롭게 만들 배열로 복사한다.
Array.from
매핑(Mapping) 🔍
Array.from(obj[, mapFn, thisArg])
mapFn
을 두 번째 인자로 넘겨주면, 새로운 배열에obj
요소를 추가하기 전에 각 요소를 대상으로mapFn
을 적용할 수 있다.- 새로운 배열엔
mapFn
을 적용하고 반환된 값이 추가된다. thisArg
는 **각 요소의this
를 지정할 수 있도록 해주는 것이다.
// 각 숫자를 제곱
let arr = Array.from(range, num => num * num);
console.log(arr); // 1,4,9,16,25
결론
쉬운 개념은 아니라서, 글이 길어졌다. 정리하자.
for - of
문 같은 몇몇 반복가능한 기능들을 객체에서 할 수 있도록 만들어진 객체를 이터러블(iterable) 객체라고 한다.- 이터러블객체에는 반드시
Symbol.iterator
프로퍼티가 반드시 구현되어 있어야한다. Symbol.iterator
메소드의 결과를 이터레이터(Iterator)객체 라고하고, 이터레이터는 이어지는 반복 과정을 처리한다.- 이터레이터 객체에는
{done: Boolean, value: any}
를 반환하는 메소드인next()
가 반드시 구현되어 있어야 한다. - 이터레이터와 이터러블을 동시에 만족시켜, 이터러블객체를 비교적 간편하게 만들수 있는
Generator 함수
가 있다. - 이터러블이나
유사배열
은Array
가 아니다. 이것을 해결해줘서, Array 처럼 사용할 수 있게 하는 범용 메소드인Array.from()
이 있다.
기본적으로 반복의 개념이 없는 일반 Object 에 대해, 반복이 가능하도록 생긴 개념인 이터러블(Iterable)은 JS를 개발하면서 사용하는 것들 중간중간에 많이 숨어있다.
그만큼 한 번 정리하고가면 좋은 이슈라 포스팅했다.
다음은, 잠시 언급한 느낌만 났던, 유사 배열(Array-like)에 대해 정리한다.