JavaScript의 배열 ( Feat. Iterator )
포스트
취소

JavaScript의 배열 ( Feat. Iterator )

해당 게시글은 자바스크립트 배열의 특징에 대해 정리한 게시글입니다.

📌 밀집 배열

밀집 배열이란 배열의 요소가 모두 같은 타입이고, 메모리상에서 연속적으로 이어져서 배치됩니다.

일반적으로 말하는 배열은 밀집 배열입니다.
따라서 배열의 시작 위치, 배열의 타입, 인덱스만으로 특정 요소에 빠르게 접근이 가능합니다.
( 시작 위치 + 배열 타입 * 인덱스 )와 같은 형태로 메모리에 접근하면 이전/이후 요소를 확인할 필요 없습니다.

하지만 특정 요소를 탐색하는 경우에는 처음부터 끝까지 찾아야하고, 중간에 삽입/삭제하는 경우에는 기준점 이후의 모든 요소들을 이동시켜줘야합니다.
각자 맞는 위치에 연속적으로 배치되어야 하기 때문이죠.

📌 희소 배열

희소 배열은 배열의 요소가 메모리상에서 연속적으로 이어지지 않는 배열입니다.
따라서 배열이라고 해도 같은 타입을 넣어주지 않아도 됩니다.
( 그래도 같은 형태의 타입을 넣는 것이 좋습니다. )

JavaScript의 배열은 사실 객체입니다.
그래서 JavaScript의 타입들중에서도 배열은 포함되지 않습니다. ( 원시 타입을 제외하고 모두 객체 )
그러면 JavaScript의 배열은 객체인데 어떻게 반복할 수 있는걸까요?
이 부분은 뒤 iterator에서 살펴보도록 하겠습니다.

희소 배열은 특정 요소에 접근하는 속도가 비교적 느립니다.
하지만 요소 탐색/삽입/삭제의 속도는 비교적 빠릅니다.
왜냐하면 연속적으로 배치할 필요가 없기 때문에 삽입/삭제의 경우 다음 요소의 주소를 새로운 주소로 바꿔주면 되니까요

1
2
3
4
console.log(typeof []); // "object"

// js에서 배열인지 확인하는 방법
console.log(Array.isArray([])); // true

📌 JS의 배열은 어떻게 동작할까?

JavaScript의 배열은 사실 객체인 것을 앞에서 확인했습니다.
그러면 객체를 어떻게 반복문으로 반복시킬 수 있는 걸까요?
이 방법에 대해서 이해하기 위해서는 선행 지식이 몇 가지 필요합니다.

1. Symbol

SymbolES6에 추가된 원시 타입입니다.
특수한 목적으로 특화된 용도로 주로 사용하기 때문에 일반적으로 사용할 일이 없습니다.
Symbol값은 Objectkey로 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
const uniqueKey = Symbol("unique");
const uniqueKeyCopy = Symbol("unique");

const obj = {
  [uniqueKey]: "uniqueKey로만 접근 가능한 유일한 값"
}

console.log(obj[uniqueKey]);  // "uniqueKey로만 접근 가능한 유일한 값"
console.log(obj[uniqueKeyCopy]);  // undefined
console.log(uniqueKey === uniqueKeyCopy);  // false

일단 Symbol은 유일한 값(주민등록번호 같은)이고 객체의 키로 사용할 수 있다는 점을 알면 됩니다.

2. Symbol.iterator

Symbol.iteratorSymbol 객체의 정적 속성입니다.
Iterator Protocol을 지키기 위해서 Symbol.iterator를 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const arr = [1, 2, 3];

// 배열은 자체적으로 "Iterator Protocol"을 지킵니다. 
// arr[Symbol.iterator]가 이터레이터를 반환하는 함수이기 때문에 바로 실행했습니다.
const iterator = arr[Symbol.iterator]();

// 시작
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
// 끝까지 순환
console.log(iterator.next()); // { value: undefined, done: true }
console.log(iterator.next()); // { value: undefined, done: true }

// 위 arr[Symbol.iterator]을 사용한 것은 배열의 인덱스에 접근하는 방식이 아닌 객체에서 특정 키에 접근하는 방식입니다.
const obj = { n: 10 };
const str = "n";
console.log(obj.n === obj["n"]); // true
console.log(obj.n === obj[str]); // true

정적 속성(static property)이란 Math.PI처럼 클래스 자체가 갖는 속성을 의미합니다.

3. Iterator Protocol

JavaScript에서 for ~ of, ...(spread operator) 등을 사용하기 위해서는 Iterator Protocol을 지켜줘야 합니다.
쉽게 말해서 엔진에서 반복문을 돌릴리면 필요한 것을 미리 규약을 정해놓고 그 규약에 맞게 만들었다면 반복문이 실행되도록 정해놨다는 의미입니다.

Protocol을 설명할 때는 택배를 예시로 많이 사용합니다.
택배를 보내기 위해서는 출발지, 목적지와 같은 필수 정보들이 있겠죠?
누가 와서 목적지도 안알려주고 “택배 보내줘”라고 하면 보낼 수가 없는 것처럼 택배를 보내기 위해 운송장 양식이 존재합니다. 즉, 운송장 양식을 프로토콜이라고 생각하면 됩니다.
이것과 유사하게 반복을 하기위한 규약을 정해놨다고 생각하면 됩니다.

그래서 Iterator Protocol은 아래의 규약을 갖습니다.

  1. 객체가 [Symbol.iterator]라는 키로 메서드를 갖는다. ( 매개변수로는 아무것도 들어오지 않는다. )
  2. 해당 메서드는 객체를 리턴한다.
  3. 해당 객체는 next()라는 메서드를 갖는다.
  4. next()메서드는 객체를 리턴한다.
  5. 객체의 형태는 { value: any, done: boolean }과 같은 형태다.
    5-1. 다음 반복이 있다면 { value: any, done: false }를 반환한다.
    5-2. 반복의 마지막이라면 { value: any, done: true }를 반환한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const obj = {
    0: "z",
    1: "a",
    2: "b",
    3: "c",
    4: "d",
};
// // TypeError: obj is not iterable ( 아직 "Iterator Protocol"을 지키지 않기 때문 )
// for (let key of obj) {
//     console.log(key);
// }

// 1 RValue인 fuction
obj[Symbol.iterator] = function () {
    // 자유 변수 ( 클로저 ) ( 현주제와는 상관 없음 )
    let index = 0;

    // 2 리턴하는 객체 ( next 메서드를 갖는 객체 )
    return {
        // 3 "next()함수" 자체
        next: () => {
            // console.log("next() 실행!");

            if (index >= 5) {
                // 4, 5-2 "next()함수"가 리턴하는 객체
                return { done: true };
            } else {
                // 4, 5-1 "next()함수"가 리턴하는 객체
                return { value: this[index++], done: false };
            }
        },
    };
};

console.log(typeof obj[Symbol.iterator]); // "function" ( 1 )
console.log(typeof obj[Symbol.iterator]()); // "object" ( 2 )
console.log(typeof obj[Symbol.iterator]().next); // "function" ( 3 )
console.log(typeof obj[Symbol.iterator]().next()); // "object" ( 4, 5 )

console.log(...obj); // z a b c d

// "iterable"한 객체 즉, "iteration protocol"을 만족하기 때문에 "for ~ of"문 사용 가능
// 한번의 반복마다 한번의 "next()"를 실행
for (let key of obj) {
    console.log(key);  // "z", "a", "b", "c", "d"
}

4. generator

Iterator에 대해 이야기 하다가 왜 generator를 이야기할까요?
바로 generator가 이터러블하기 때문입니다.
그리고 generatorIterator Protocol을 지킨 코드를 작성할 수 있기 때문입니다.

먼저 generator의 사용법에 대해 살펴보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function* generateSequence() {
  console.log("실행 1");
  yield 1;

  console.log("실행 2");
  yield 2;

  console.log("실행 3");
  yield 3;

  // // "return"과 "yield"의 차이는 직접 비교해보기
  // return 3;
}

// 제네레이터 객체 생성 ( 함수 실행 X )
const it = generateSequence();

// "next()"를 호출하면 가장 가까운 "yield"를 만날 때까지 제너레이터 함수를 실행
console.log(it.next()); // "실행 1" { value: 1, done: false }
console.log(it.next()); // "실행 2" { value: 2, done: false }
console.log(it.next()); // "실행 3" { value: 3, done: true }
console.log(it.next()); // { value: undefined, done: true }

generator의 예시를 보면 Iterator Protocol 규약을 지키는 것으로 보입니다.
제너레이터 객체가 next() 메서드를 갖고, { value:any, done: boolean }형태의 값을 반환합니다.

다음으로 generatoriterable로 사용하는 예시입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 제너레이터도 이터러블
function* generateSequence() {
  console.log("실행 1");
  yield 1;

  console.log("실행 2");
  yield 2;

  console.log("실행 3");
  yield 3;
}

let generator = generateSequence();

console.log(...generator); // "실행 1", "실행 2", "실행 3", 1 2 3

for (const iterator of generator) {
  console.log(iterator); // "실행 1" 1, "실행 2" 2, "실행 3" 3
}

generatorIterator Protocol을 지키기 때문에 iterable합니다.
다음으로 generator를 이용해서 Iterator Protocol을 지키는 객체를 만들어보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const obj = { from: 1, to: 5 };
obj[Symbol.iterator] = function* () {
  for (let value = this.from; value <= this.to; value++) {
    yield value;
  }
};

console.log(...obj);

for (const iterator of obj) {
  console.log(iterator); // "실행 1" 1, "실행 2" 2, "실행 3" 3
}

const obj2 = {
  from: 1,
  to: 5,
  *[Symbol.iterator]() {
    // ... 이런 형식으로 사용 가능
  }
};

🚩 마무리

JavaScript에 관해 공부할 때마다 Symbol, generator, iterator에 대해서 본 적이 있습니다.
하지만 제대로 공부하지는 않았습니다.
왜냐하면 항상 “필수적인 내용은 아니다.”, “나중에 공부해도 된다.”라고 알고 있어서 지금 다른 공부도 부족한데 필수도 아닌 것을 과하게 공부할 필요가 있을까? 해서 말았습니다

요즘 코드스테이츠에서 프론트엔드 코스를 밟고 있는데 배열이 나오고, 최근에 시작한 자바스크립트 완벽 가이드에서 공부하면서 Symbol을 공부하면서 이제 자세하게 공부해봐도 되지 않을까? 라는 생각이 들어서 여러 레퍼런스를 찾아보면서 공부를 시작했습니다.
이번에 정리를 하면서 배열에 대한 불안감이 해소되었습니다.
“항상 배열은 사실 객체다.”, “이터레이터, 이터러블이라는 개념이 있고 그것에 의해서 반복이 된다.”라는 개념만 알고 있고 실질적으로 어떻게 동작하는지는 전혀 모르는 상태였었는데 이번 포스팅으로 인해 어느 정도 해소되고 정리됐습니다.

📮 레퍼런스

  1. poiemaweb - 자바스크립트 배열은 배열이 아니다
  2. MDN - Symbol
  3. javascript info - Symbol
  4. MDN - IterationProtocol
  5. poiemaweb - generator
  6. javascript info - generator
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

Git과 GitHub에 대한 정리

실행 컨텍스트 ( Feat. 호이스팅, 클로저, 스코프 체인 )