Class, Iterator, Generator in JavaScript

CDD·2023년 3월 14일
0

web-develop

목록 보기
5/11
post-thumbnail

사실 JS를 새로 학습하기 시작하면서 생겼던 의문이 있다. object 의 형태가 class와 너무 유사해보여서 class라는 개념이 존재하는지에 대해서도 긴가민가 했다. 이에 대해서 적어보려고 한다.

class VS object

const Person = {
  height: a,
  weight: b,
} // object

class Person {
  height;
  weight;
  #className = 'swTrack';
  constructor(height, weight) {
    this.height = height;
    this.weight = weight;
  } // 생성자
}

const personOne = new Person(174, 70);

미래의 나를 위한 설명이기 때문에 자세한 설명은 생략하고, 간략하게 설명하자면 기존의 다른 객체 지향 언어들과 상당히 유사한 모습을 지녔다. 생성자도 존재하고, 그 이외의 클래스 내 변수들도 존재한다. 앞에 #을 붙이는 경우 private과 동일한 기능을 지니게 되며 스코프 외부에서 참조하는 것이 불가능하게 된다. console.log(personOne.#className);이 안먹힌다는 소리다.

class Element {
  #origin = 'Element';
  ...
}

class Input extends Element {
  constructor() {
    super(); // 부모의 속성을 상속받음
    this.name = 'Input'; // 덮어쓰기
  }
}

상속과 같은 개념도 물론 존재한다. 자식 생성자 부분의 super()는 부모의 데이터들을 상속받는 역할을 한다. 사실 super()을 사용하지 않으면 그냥 문법 오류가 떠버려서 필수적으로 써야하고, 그렇기 때문에 구조상 부모 클래스의 모든 값들을 상속 받을 수 밖에 없다. 일반적인 캡슐화를 위한 private 속성을 구현하기 위해서는 변수 앞에 #을 붙이면 된다. 그 예가 Element 클래스의 #origin이다. 심볼을 사용하는 방식도 존재하긴 하지만 문법적으로 #이 공식화 되었기 때문에 굳이 알아야 할 필요는 없을 것 같다.

이터레이터 (Iterator)

function iter() {
  let num = 1;

  return {
    next: () => {
      const done = num > 10; // num이 10보다 클 시 done = true
      const item = { done };
      if (!done) item.value = num++; // 10보다 작을시 num 값 1 증가

      return item;
    },
  };
}

const numEx = iter();
console.log(numEx.next());
console.log(numEx.next());
console.log(numEx.next());

가장 기본적인 이터레이터 사용 예시이다. 동적인 특성을 지니고 있는데, 그렇기 때문에 메모리 관리 측면에서 유리하다는 장점을 가지고 있다. 이런 카운팅 예제에서 특히 많이 쓰이는 것 같고, 사용 시 next() 메서드를 호출하면 된다.

next() -> { done: true | false, value : ? }

next() 자체의 사용 방식이다. 조건이 충족되어 done 값이 true가 된다면 그 이후에 아무리 함수를 호출하여도 계속해서 같은 값이 반복되어 나온다. 위의 예제를 봐도 이미 10보다 커져버렸으니 값을 증가시킬 필요가 없기 때문이다.

이터러블 (Iterable)

const nums = [1, 2, 3, 4, 5];
const elements = document.querySelectorAll('div');

for (const num of nums) {
  console.log(num);
}

for (const element of elements) {
  console.log(element); // Node 0, 1, 2, 3 ...
}

예제 코드를 보면 elementsquerySelectorAll()을 통해 다중 값들을 가져온 모습을 확인할 수 있다. 이 같은 경우 모든 값들이 배열 형태로 들어왔다는 착각을 할 수 있는데, 저런식으로 가져오게 된다면 Node 형태로 값을 가져오는 모습을 콘솔창으로 확인할 수 있다. iterable은 말 그대로 어떠한 집합이던 간에 for .. of 문법을 적용할 수만 있다면 이터러블(반복 가능)의 예가 될 수 있다는 것이다.

function iter() {
  let num = 1;

  const iterator = {
    next: () => {
      const done = num > 5;
      const item = { done };

      if (!done) item.value = num++;
    },
  };

  return {
    [Symbol.iterator]() {
      return iterator;
    },
  };
}

for (const num of iter()) {
  console.log(num);
}

[Symbol.iterator]은 이터레이터 객체를 반환한다. next()와 저런식으로 나누어 작성할 수 있고, 기능은 사실 iterator 예제와 동일하다고 보면 된다.

class Nums {
  #num = 1;

  next() {
    if (this.#num > 11) {
      return { done: true };
    }
    const value = this.#num;
    this.#num += 1;

    return {
      done: false,
      value,
    };
  }

  [Symbol.iterator]() {
    return this;
  }
}

클래스 내의 iterator 사용도 비슷한 느낌이다. 저 문법을 외운다는 느낌보다는 원리에 대한 이해가 필요할 때마다 찾아서 보면 될 것 같다. 사실 이것보다 훨씬 쉬운 방식이 다음 내용에 나오기 때문이다.

제네레이터 (Generator)

앞의 모든 내용들은 이 제네레이터 개념을 이해하기 위한 빌드업이라고 생각하면 된다. 구현 방법은 간단하지만 원리는 iterator를 어느정도 이해할 수 있어야 한다. 예제 코드를 보자.

function* iter() {
  for (let i = 1; i <= 10; i++) {
    yield i;
  }
}

for (const num of iter()) {
  console.log(num);
}

function*, yield라는 키워드가 익숙치 않을 수 있지만, 각각 반복 함수, 반환 value 값 정도인 것 같다. 그냥 메모리 관리가 필요하거나 저런 식의 카운팅 구현이 필요할 때마다 다시 이 곳을 찾아 방법을 확인하게 될 것 같다.

function* pick(items) {
  for (let i = 0; i < items.length; i++) {
    for (let j = i + 1; j < items.length; j++) {
      yield [items[i], items[j]];
    }
  }
}

const coms = [...pick(["hello", "world", "good", "day"])];
console.log(coms);

위의 예제는 목록들 중 몇개를 선정하여 팀을 짜는 Combination 기능 구현인데, 확실히 제네레이터는 코드 길이와 로직 구현에서 훨씬 짧고 간단하다는 특성이 있다.

function* pick(items, count) {
  yield* _pick([], count, 0);

  function* _pick(state, count, i) {
    for (; i < items.length; i++) {
      const picked = [...state, items[i]];

      if (count === 1) {
        yield picked;
      } else {
        yield* _pick(picked, count - 1, i + 1);
      }
    }
  }
}

const samples = [1, 2, 3, 4, 5, 6, 7];

const coms = [...pick(samples, 5)];

console.log(coms);

이 예제 코드를 이해할 정도면 generator에 대한 이해가 완벽하게 끝났다고 봐도 될 정도라고 한다. 서칭을 해보니 yield* 부분은 iterator()가 끝나는 시점 done, true가 되었을 때의 리턴값을 갖는다고 한다.

먼저 조합을 담을 그릇 state[]로 초기화 시킨 후에 _pick 함수를 실행시켜 조합을 만들게 된다. count의 값은 조합이 늘어날 때마다 1씩 감소되게 하고 i 값은 1씩 증가하면서 계속해서 _pick 함수를 재귀 호출하게 된다.

0개의 댓글