Study

23. JavaScript에서 Iterators와 Generators 사용하기 | 웹 개발 기초

구구 구구 2024. 7. 23. 11:00
반응형

3d 와이어프레임 스타일, dall-e

 

JavaScript Iterators and Generators: 효율적인 반복과 생성기 활용

JavaScript의 Iterators와 Generators는 반복 작업을 효율적으로 처리하고, 고성능 코드를 작성하는 데 중요한 도구입니다. 이 블로그에서는 Iterators와 Generators의 개념, 사용법, 장점 및 한계 등을 다룹니다.

 

01. Iterators의 개념과 사용법

Iterator의 정의와 동작 원리

Iterator는 JavaScript에서 컬렉션(예: 배열, 문자열)의 요소를 순차적으로 접근할 수 있는 객체입니다. Iterator는 next() 메서드를 통해 요소에 접근하며, 각 호출 시 다음 요소를 반환합니다. next() 메서드는 두 개의 속성을 가진 객체를 반환합니다:

  • value: 현재 요소의 값
  • done: 반복이 끝났는지를 나타내는 불리언 값

Iterator는 주로 for...of 루프와 함께 사용되며, 이 루프는 자동으로 next() 메서드를 호출하여 컬렉션의 모든 요소를 순회합니다.

기본 예제와 사용법

// 배열을 이용한 Iterator 예제
let array = [1, 2, 3, 4];
let iterator = array[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: 4, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

위 예제에서 array[Symbol.iterator]()는 배열의 Iterator 객체를 반환합니다. next() 메서드를 호출할 때마다 배열의 다음 요소를 반환하며, 마지막 요소를 반환한 후에는 done 속성이 true가 됩니다.

for...of 루프를 사용하면 더 간결하게 작성할 수 있습니다:

let array = [1, 2, 3, 4];

for (let value of array) {
  console.log(value);
}
// 출력:
// 1
// 2
// 3
// 4

위 예제는 for...of 루프를 사용하여 배열의 모든 요소를 순회합니다. 이 루프는 내부적으로 Iterator의 next() 메서드를 호출하여 요소를 순차적으로 접근합니다.

 

02. Generators의 개념과 사용법

Generator 함수의 정의와 동작 원리

Generator 함수는 JavaScript에서 반복 가능한 Iterator 객체를 생성하는 함수입니다. Generator 함수는 일반 함수와 달리 function* 키워드를 사용하여 정의되며, yield 키워드를 사용하여 일시 중지와 재개가 가능한 실행 컨텍스트를 만듭니다.

Generator 함수는 호출될 때 즉시 실행되지 않고, 대신 Generator 객체를 반환합니다. 이 객체는 next() 메서드를 통해 yield 표현식에서 일시 중지된 실행을 재개할 수 있습니다. 각 next() 호출은 yield 다음의 값을 반환하며, done 속성은 Generator가 끝났는지를 나타냅니다.

Generator 함수 예제

// Generator 함수 정의
function* generatorFunction() {
  yield 1;
  yield 2;
  yield 3;
  return 4;
}

let generator = generatorFunction();

console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: 3, done: false }
console.log(generator.next()); // { value: 4, done: true }

위 예제에서 generatorFunctionyield 키워드를 사용하여 값을 생성합니다. generator.next()를 호출할 때마다 다음 yield 표현식으로 이동하며, 마지막으로 return 문에서 종료됩니다.

Generator 함수를 for...of 루프와 함께 사용할 수도 있습니다:

function* generatorFunction() {
  yield 1;
  yield 2;
  yield 3;
}

for (let value of generatorFunction()) {
  console.log(value);
}
// 출력:
// 1
// 2
// 3

위 예제에서는 for...of 루프를 사용하여 Generator 함수에서 생성된 값을 순회합니다. Generator 함수는 Iterator를 반환하므로, for...of 루프와 자연스럽게 통합됩니다.

Generator 함수는 복잡한 반복 작업을 간단하게 구현할 수 있는 강력한 도구입니다. 예를 들어, 무한 수열을 생성하거나, 비동기 작업의 흐름 제어를 쉽게 할 수 있습니다. 다음은 Fibonacci 수열을 생성하는 Generator 함수의 예제입니다:

function* fibonacci() {
  let [prev, curr] = [0, 1];
  while (true) {
    yield curr;
    [prev, curr] = [curr, prev + curr];
  }
}

let sequence = fibonacci();
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 2
console.log(sequence.next().value); // 3
console.log(sequence.next().value); // 5
console.log(sequence.next().value); // 8

위 예제는 Fibonacci 수열을 생성하는 Generator 함수입니다. next() 메서드를 호출할 때마다 다음 Fibonacci 수를 생성하며, 이는 무한히 계속됩니다. 이 Generator 함수는 yield 표현식을 사용하여 수열의 각 값을 반환하며, prevcurr 값을 갱신하여 다음 값을 계산합니다.

 

03. Iterables과 그 활용

Iterable 객체의 정의

Iterable 객체는 JavaScript에서 반복 가능한 객체를 의미합니다. 즉, Iterable 객체는 for...of 루프를 사용하여 그 요소를 순회할 수 있는 객체입니다. 이러한 객체는 Symbol.iterator 메서드를 구현해야 합니다. Symbol.iterator 메서드는 Iterator 객체를 반환하며, 이 Iterator 객체는 next() 메서드를 통해 요소를 순차적으로 접근합니다.

Iterable 객체 예제

JavaScript의 내장 객체 중 여러 객체가 Iterable입니다. 예를 들어, 배열, 문자열, Map, Set 등이 있습니다. 이러한 객체는 모두 for...of 루프를 사용하여 순회할 수 있습니다. 다음은 배열과 문자열을 사용하는 예제입니다:

// 배열의 Iterable 예제
let array = [10, 20, 30];

for (let value of array) {
  console.log(value);
}
// 출력:
// 10
// 20
// 30

// 문자열의 Iterable 예제
let string = "Hello";

for (let char of string) {
  console.log(char);
}
// 출력:
// H
// e
// l
// l
// o

Iterable 객체를 직접 구현할 수도 있습니다. 다음은 사용자 정의 Iterable 객체의 예제입니다:

let myIterable = {
  *[Symbol.iterator]() {
    yield 1;
    yield 2;
    yield 3;
  }
};

for (let value of myIterable) {
  console.log(value);
}
// 출력:
// 1
// 2
// 3

위 예제에서는 Symbol.iterator 메서드를 Generator 함수로 정의하여 Iterable 객체를 구현하였습니다. for...of 루프를 사용하여 이 객체를 순회할 수 있습니다.

 

04. 고급 Generators 사용법

Generators에서 next() 메서드 활용

Generators의 next() 메서드는 Generator 함수의 실행을 제어하는 중요한 역할을 합니다. next() 메서드는 Generator 함수의 yield 표현식을 기준으로 실행을 일시 중지하거나 재개합니다. next() 메서드를 호출할 때마다 yield 다음의 코드가 실행되며, 새로운 yield 표현식에서 다시 일시 중지됩니다.

또한, next() 메서드는 선택적으로 인자를 받을 수 있습니다. 이 인자는 Generator 함수 내부로 전달되며, yield 표현식의 반환값으로 사용할 수 있습니다. 다음 예제를 통해 이를 살펴보겠습니다:

function* generatorFunction() {
  let result = yield '첫 번째 yield';
  console.log(result);
  result = yield '두 번째 yield';
  console.log(result);
}

let generator = generatorFunction();

console.log(generator.next());       // { value: '첫 번째 yield', done: false }
console.log(generator.next('첫 번째 값'));  // '첫 번째 값' 출력, { value: '두 번째 yield', done: false }
console.log(generator.next('두 번째 값'));  // '두 번째 값' 출력, { value: undefined, done: true }

위 예제에서 generator.next('첫 번째 값') 호출은 yield 표현식의 반환값으로 '첫 번째 값'을 전달합니다. 이는 Generator 함수 내부에서 result 변수에 할당됩니다. 다음 next() 호출도 같은 방식으로 작동합니다.

고급 예제: Fibonacci 생성기

Generators를 활용하여 복잡한 반복 작업을 간단하게 구현할 수 있습니다. 다음은 Fibonacci 수열을 생성하는 고급 예제입니다:

function* fibonacciGenerator() {
  let [prev, curr] = [0, 1];
  while (true) {
    yield curr;
    [prev, curr] = [curr, prev + curr];
  }
}

let fibonacci = fibonacciGenerator();

console.log(fibonacci.next().value); // 1
console.log(fibonacci.next().value); // 1
console.log(fibonacci.next().value); // 2
console.log(fibonacci.next().value); // 3
console.log(fibonacci.next().value); // 5
console.log(fibonacci.next().value); // 8
console.log(fibonacci.next().value); // 13

위 예제는 무한 Fibonacci 수열을 생성하는 Generator 함수입니다. next() 메서드를 호출할 때마다 다음 Fibonacci 수를 생성하며, 이는 무한히 계속됩니다. 이 Generator 함수는 yield 표현식을 사용하여 수열의 각 값을 반환하며, prevcurr 값을 갱신하여 다음 값을 계산합니다.

 

05. Iterators와 Generators의 장점과 한계

Iterators와 Generators의 장점

  • 메모리 효율성: Iterators와 Generators는 데이터의 모든 요소를 한꺼번에 메모리에 올리지 않고 필요한 시점에 하나씩 처리하기 때문에 메모리 사용이 효율적입니다. 특히 큰 데이터 세트나 무한 시퀀스를 처리할 때 매우 유용합니다.
  • 코드 가독성 및 유지보수성 향상: Iterators와 Generators를 사용하면 코드가 더 직관적이고 가독성이 높아집니다. 복잡한 반복문 대신 for...of 루프를 사용하거나, Generator 함수를 사용하여 반복 작업을 간결하게 표현할 수 있습니다.
  • 유연한 흐름 제어: Generators는 yield 키워드를 통해 함수 실행을 일시 중지하고 다시 시작할 수 있는 기능을 제공하여, 복잡한 흐름 제어를 유연하게 처리할 수 있습니다. 이는 비동기 작업을 순차적으로 처리하는 데도 유용합니다.
  • 비동기 처리: Async Generators와 함께 사용하면 비동기 작업을 쉽게 처리할 수 있습니다. for await...of 루프를 사용하면 비동기 이터레이션을 간단하게 구현할 수 있습니다.

한계 및 주의사항

  • 복잡성 증가: Generators와 Iterators는 강력한 도구이지만, 이를 잘못 사용하거나 과용할 경우 코드의 복잡성이 증가할 수 있습니다. 특히 next() 메서드를 남용하면 코드의 흐름을 이해하기 어려워질 수 있습니다.
  • 디버깅 어려움: Generators는 함수 실행을 일시 중지하고 다시 시작하는 특성 때문에 디버깅이 어려울 수 있습니다. yield 지점에서 상태를 추적하고 문제를 파악하는 데 주의가 필요합니다.
  • 브라우저 호환성: 대부분의 현대 브라우저와 Node.js 환경에서는 Generators와 Iterators를 지원하지만, 구형 브라우저에서는 지원하지 않을 수 있습니다. 따라서, 크로스 브라우저 호환성을 고려한 추가 작업이 필요할 수 있습니다.
  • 성능 고려: Generators는 함수 실행을 일시 중지하고 재개하는 오버헤드가 있기 때문에, 성능이 중요한 애플리케이션에서는 주의가 필요합니다. 성능이 중요한 반복 작업에서는 Generators를 신중하게 사용해야 합니다.

 

06. 결론

Iterators와 Generators의 실전 적용 사례

Iterators와 Generators는 다양한 분야에서 효율적인 데이터 처리를 위해 사용됩니다. 다음은 몇 가지 실전 적용 사례입니다:

  • 데이터 스트리밍: Iterators와 Generators는 대용량 데이터를 스트리밍 방식으로 처리할 때 유용합니다. 예를 들어, 파일을 읽거나 네트워크에서 데이터를 수신할 때, 데이터를 한 번에 메모리에 올리지 않고 필요한 부분만 순차적으로 처리할 수 있습니다.
// Node.js에서 파일을 읽는 예제
const fs = require('fs');
const readline = require('readline');

async function* readLines(filePath) {
  const fileStream = fs.createReadStream(filePath);
  const rl = readline.createInterface({
    input: fileStream,
    crlfDelay: Infinity
  });

  for await (const line of rl) {
    yield line;
  }
}

(async () => {
  for await (const line of readLines('example.txt')) {
    console.log(line);
  }
})();
            
  • 비동기 작업의 순차 처리: Async Generators를 사용하여 비동기 작업을 순차적으로 처리할 수 있습니다. 이는 비동기 API 호출, 데이터베이스 쿼리 등을 순차적으로 처리하는 데 유용합니다.
async function* fetchData(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    const data = await response.json();
    yield data;
  }
}

(async () => {
  const urls = ['https://api.example.com/data1', 'https://api.example.com/data2'];

  for await (const data of fetchData(urls)) {
    console.log(data);
  }
})();
            

성능 최적화를 위한 활용 방법

  • 무거운 연산의 지연 처리: Generators를 사용하면 무거운 연산을 지연 처리할 수 있습니다. 이는 연산이 필요한 시점에만 수행하도록 하여 성능을 최적화하는 데 도움이 됩니다.
function* computeHeavyTask() {
  for (let i = 0; i < 1e6; i++) {
    yield i * i;
  }
}

let task = computeHeavyTask();
console.log(task.next().value); // 첫 번째 연산 결과
console.log(task.next().value); // 두 번째 연산 결과
            
  • 대규모 데이터 세트의 메모리 효율적 처리: Iterators와 Generators를 사용하여 대규모 데이터 세트를 메모리 효율적으로 처리할 수 있습니다. 이는 메모리 사용을 최소화하고, 필요한 데이터만 순차적으로 처리하여 성능을 향상시킵니다.
  • 동적 데이터 생성: Generators를 사용하여 동적으로 데이터를 생성할 수 있습니다. 이는 필요한 시점에 데이터를 생성하여 메모리 사용을 최적화하고, 데이터 생성 비용을 분산시킬 수 있습니다.
function* dynamicDataGenerator() {
  let index = 0;
  while (true) {
    yield `Data-${index++}`;
  }
}

let dataGen = dynamicDataGenerator();
console.log(dataGen.next().value); // 'Data-0'
console.log(dataGen.next().value); // 'Data-1'
            

Iterators와 Generators는 JavaScript에서 복잡한 반복 작업을 효율적으로 처리하고, 성능을 최적화하는 데 강력한 도구입니다. 이를 통해 개발자는 더 나은 성능과 유연성을 갖춘 애플리케이션을 구현할 수 있습니다.


관련된 다른 글도 읽어보시길 추천합니다

 

2024.07.12 - [AI] - 17. JavaScript 배열과 타입 배열의 모든 것 | 웹 개발 기초

 

17. JavaScript 배열과 타입 배열의 모든 것 | 웹 개발 기초

JavaScript Indexed Collections: 배열과 타입 배열의 모든 것 01. 배열1) 배열의 정의와 역할배열은 JavaScript에서 가장 중요한 데이터 구조 중 하나로, 여러 개의 값을 하나의 변수로 묶어 관리할 수 있게

guguuu.com

2024.07.12 - [Study] - 16. JavaScript 정규 표현식 완벽 가이드 | 웹 개발 기초

 

16. JavaScript 정규 표현식 완벽 가이드 | 웹 개발 기초

JavaScript 정규 표현식 완벽 가이드: 기본부터 활용까지 01. 서론1) JavaScript 정규 표현식 소개JavaScript 정규 표현식(Regular Expressions, RegEx)은 문자열 내 특정 패턴을 찾고, 일치시키며, 추출하거나 교

guguuu.com

2024.07.11 - [Study] - 15. JavaScript 텍스트 포맷팅 | 웹 개발 기초

 

15. JavaScript 텍스트 포맷팅 | 웹 개발 기초

JavaScript 텍스트 포맷팅 가이드 01. JavaScript 문자열1) JavaScript에서 텍스트 포맷팅의 중요성JavaScript에서 텍스트 포맷팅은 웹 애플리케이션과 사용자 간의 상호작용을 향상시키는 중요한 역할을 합

guguuu.com


읽어주셔서 감사합니다

공감은 힘이 됩니다

 

:)

반응형