한국어

JavaScript 비동기 이터레이터 헬퍼를 탐색하여 스트림 처리를 혁신하세요. map, filter, take, drop 등을 사용하여 비동기 데이터 스트림을 효율적으로 처리하는 방법을 배워보세요.

JavaScript 비동기 이터레이터 헬퍼: 최신 애플리케이션을 위한 강력한 스트림 처리

최신 JavaScript 개발에서 비동기 데이터 스트림을 다루는 것은 흔한 요구사항입니다. API에서 데이터를 가져오든, 대용량 파일을 처리하든, 실시간 이벤트를 처리하든, 비동기 데이터를 효율적으로 관리하는 것이 중요합니다. JavaScript의 비동기 이터레이터 헬퍼는 이러한 스트림을 처리하는 강력하고 우아한 방법을 제공하며, 데이터 조작에 대한 함수형 및 조합형 접근 방식을 제공합니다.

비동기 이터레이터와 비동기 이터러블이란 무엇인가?

비동기 이터레이터 헬퍼에 대해 알아보기 전에, 기본 개념인 비동기 이터레이터와 비동기 이터러블에 대해 이해해 보겠습니다.

비동기 이터러블(Async Iterable)은 값들을 비동기적으로 순회하는 방법을 정의하는 객체입니다. 이는 비동기 이터레이터(Async Iterator)를 반환하는 @@asyncIterator 메서드를 구현함으로써 이루어집니다.

비동기 이터레이터(Async Iterator)next() 메서드를 제공하는 객체입니다. 이 메서드는 두 가지 속성을 가진 객체로 귀결되는 프로미스(promise)를 반환합니다:

다음은 간단한 예제입니다:


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 500)); // 비동기 작업을 시뮬레이션합니다
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  for await (const value of asyncIterable) {
    console.log(value); // 출력: 1, 2, 3, 4, 5 (각각 500ms 지연)
  }
})();

이 예제에서 generateSequence는 비동기적으로 숫자 시퀀스를 생성하는 비동기 제너레이터 함수입니다. for await...of 루프는 비동기 이터러블로부터 값을 소비하는 데 사용됩니다.

비동기 이터레이터 헬퍼 소개

비동기 이터레이터 헬퍼는 비동기 이터레이터의 기능을 확장하여 비동기 데이터 스트림을 변환, 필터링 및 조작하기 위한 메서드 세트를 제공합니다. 이는 함수형 및 조합형 프로그래밍 스타일을 가능하게 하여 복잡한 데이터 처리 파이프라인을 더 쉽게 구축할 수 있도록 합니다.

핵심 비동기 이터레이터 헬퍼는 다음과 같습니다:

각 헬퍼를 예제와 함께 살펴보겠습니다.

map()

map() 헬퍼는 제공된 함수를 사용하여 비동기 이터러블의 각 요소를 변환합니다. 변환된 값을 가진 새로운 비동기 이터러블을 반환합니다.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

const doubledIterable = asyncIterable.map(x => x * 2);

(async () => {
  for await (const value of doubledIterable) {
    console.log(value); // 출력: 2, 4, 6, 8, 10 (100ms 지연)
  }
})();

이 예제에서 map(x => x * 2)는 시퀀스의 각 숫자를 두 배로 만듭니다.

filter()

filter() 헬퍼는 제공된 조건(predicate 함수)에 따라 비동기 이터러블에서 요소를 선택합니다. 조건을 만족하는 요소만 포함하는 새로운 비동기 이터러블을 반환합니다.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(10);

const evenNumbersIterable = asyncIterable.filter(x => x % 2 === 0);

(async () => {
  for await (const value of evenNumbersIterable) {
    console.log(value); // 출력: 2, 4, 6, 8, 10 (100ms 지연)
  }
})();

이 예제에서 filter(x => x % 2 === 0)는 시퀀스에서 짝수만 선택합니다.

take()

take() 헬퍼는 비동기 이터러블에서 첫 N개 요소를 반환합니다. 지정된 수의 요소만 포함하는 새로운 비동기 이터러블을 반환합니다.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

const firstThreeIterable = asyncIterable.take(3);

(async () => {
  for await (const value of firstThreeIterable) {
    console.log(value); // 출력: 1, 2, 3 (100ms 지연)
  }
})();

이 예제에서 take(3)은 시퀀스에서 첫 세 개의 숫자를 선택합니다.

drop()

drop() 헬퍼는 비동기 이터러블에서 첫 N개 요소를 건너뛰고 나머지를 반환합니다. 나머지 요소를 포함하는 새로운 비동기 이터러블을 반환합니다.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

const afterFirstTwoIterable = asyncIterable.drop(2);

(async () => {
  for await (const value of afterFirstTwoIterable) {
    console.log(value); // 출력: 3, 4, 5 (100ms 지연)
  }
})();

이 예제에서 drop(2)는 시퀀스에서 첫 두 개의 숫자를 건너뜁니다.

toArray()

toArray() 헬퍼는 전체 비동기 이터러블을 소비하고 모든 요소를 배열로 수집합니다. 모든 요소를 포함하는 배열로 귀결되는 프로미스를 반환합니다.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  const numbersArray = await asyncIterable.toArray();
  console.log(numbersArray); // 출력: [1, 2, 3, 4, 5]
})();

이 예제에서 toArray()는 시퀀스의 모든 숫자를 배열로 수집합니다.

forEach()

forEach() 헬퍼는 비동기 이터러블의 각 요소에 대해 제공된 함수를 한 번 실행합니다. 새로운 비동기 이터러블을 반환하지 않으며, 부수 효과(side-effect)를 위해 함수를 실행합니다. 이는 로깅이나 UI 업데이트와 같은 작업을 수행하는 데 유용할 수 있습니다.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(3);

(async () => {
  await asyncIterable.forEach(value => {
    console.log("Value:", value);
  });
  console.log("forEach completed");
})();
// 출력: Value: 1, Value: 2, Value: 3, forEach completed

some()

some() 헬퍼는 비동기 이터러블의 요소 중 적어도 하나가 제공된 함수로 구현된 테스트를 통과하는지 테스트합니다. 불리언 값(적어도 하나의 요소가 조건을 만족하면 true, 그렇지 않으면 false)으로 귀결되는 프로미스를 반환합니다.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  const hasEvenNumber = await asyncIterable.some(x => x % 2 === 0);
  console.log("Has even number:", hasEvenNumber); // 출력: Has even number: true
})();

every()

every() 헬퍼는 비동기 이터러블의 모든 요소가 제공된 함수로 구현된 테스트를 통과하는지 테스트합니다. 불리언 값(모든 요소가 조건을 만족하면 true, 그렇지 않으면 false)으로 귀결되는 프로미스를 반환합니다.


async function* generateSequence(end) {
  for (let i = 2; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(4);

(async () => {
  const areAllEven = await asyncIterable.every(x => x % 2 === 0);
  console.log("Are all even:", areAllEven); // 출력: Are all even: true
})();

find()

find() 헬퍼는 제공된 테스트 함수를 만족하는 비동기 이터러블의 첫 번째 요소를 반환합니다. 테스트 함수를 만족하는 값이 없으면 undefined가 반환됩니다. 찾은 요소 또는 undefined로 귀결되는 프로미스를 반환합니다.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  const firstEven = await asyncIterable.find(x => x % 2 === 0);
  console.log("First even number:", firstEven); // 출력: First even number: 2
})();

reduce()

reduce() 헬퍼는 비동기 이터러블의 각 요소에 대해 사용자가 제공한 "리듀서" 콜백 함수를 순서대로 실행하며, 이전 요소에 대한 계산의 반환 값을 전달합니다. 모든 요소에 대해 리듀서를 실행한 최종 결과는 단일 값입니다. 최종 누적 값으로 귀결되는 프로미스를 반환합니다.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  const sum = await asyncIterable.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
  console.log("Sum:", sum); // 출력: Sum: 15
})();

실용적인 예제 및 사용 사례

비동기 이터레이터 헬퍼는 다양한 시나리오에서 유용합니다. 몇 가지 실용적인 예제를 살펴보겠습니다:

1. 스트리밍 API 데이터 처리

스트리밍 API로부터 데이터를 수신하는 실시간 데이터 시각화 대시보드를 구축한다고 상상해 보세요. API는 지속적으로 업데이트를 보내고, 최신 정보를 표시하기 위해 이러한 업데이트를 처리해야 합니다.


async function* fetchDataFromAPI(url) {
  let response = await fetch(url);

  if (!response.body) {
    throw new Error("ReadableStream not supported in this environment");
  }

  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) {
        break;
      }
      const chunk = decoder.decode(value);
      // API가 개행 문자로 구분된 JSON 객체를 보낸다고 가정합니다
      const lines = chunk.split('\n');
      for (const line of lines) {
        if (line.trim() !== '') {
          yield JSON.parse(line);
        }
      }
    }
  } finally {
    reader.releaseLock();
  }
}

const apiURL = 'https://example.com/streaming-api'; // API URL로 교체하세요
const dataStream = fetchDataFromAPI(apiURL);

// 데이터 스트림 처리
(async () => {
  for await (const data of dataStream.filter(item => item.type === 'metric').map(item => ({ timestamp: item.timestamp, value: item.value }))) {
    console.log('Processed Data:', data);
    // 처리된 데이터로 대시보드 업데이트
  }
})();

이 예제에서 fetchDataFromAPI는 스트리밍 API에서 데이터를 가져와 JSON 객체를 파싱하고 비동기 이터러블로 반환(yield)합니다. filter 헬퍼는 메트릭만 선택하고, map 헬퍼는 대시보드를 업데이트하기 전에 데이터를 원하는 형식으로 변환합니다.

2. 대용량 파일 읽기 및 처리

고객 데이터가 포함된 대용량 CSV 파일을 처리해야 한다고 가정해 보겠습니다. 전체 파일을 메모리에 로드하는 대신, 비동기 이터레이터 헬퍼를 사용하여 청크 단위로 처리할 수 있습니다.


async function* readLinesFromFile(filePath) {
  const file = await fsPromises.open(filePath, 'r');

  try {
    let buffer = Buffer.alloc(1024);
    let fileOffset = 0;
    let remainder = '';

    while (true) {
      const { bytesRead } = await file.read(buffer, 0, buffer.length, fileOffset);
      if (bytesRead === 0) {
        if (remainder) {
          yield remainder;
        }
        break;
      }

      fileOffset += bytesRead;
      const chunk = buffer.toString('utf8', 0, bytesRead);
      const lines = chunk.split('\n');

      lines[0] = remainder + lines[0];
      remainder = lines.pop() || '';

      for (const line of lines) {
        yield line;
      }
    }
  } finally {
    await file.close();
  }
}

const filePath = './customer_data.csv'; // 파일 경로로 교체하세요
const lines = readLinesFromFile(filePath);

// 라인 처리
(async () => {
  for await (const customerData of lines.drop(1).map(line => line.split(',')).filter(data => data[2] === 'USA')) {
    console.log('Customer from USA:', customerData);
    // 미국 고객 데이터 처리
  }
})();

이 예제에서 readLinesFromFile은 파일을 한 줄씩 읽고 각 줄을 비동기 이터러블로 반환합니다. drop(1) 헬퍼는 헤더 행을 건너뛰고, map 헬퍼는 줄을 열로 분할하며, filter 헬퍼는 미국 고객만 선택합니다.

3. 실시간 이벤트 처리

비동기 이터레이터 헬퍼는 웹소켓과 같은 소스에서 발생하는 실시간 이벤트를 처리하는 데에도 사용할 수 있습니다. 이벤트가 도착할 때마다 이벤트를 방출하는 비동기 이터러블을 생성한 다음, 헬퍼를 사용하여 이러한 이벤트를 처리할 수 있습니다.


async function* createWebSocketStream(url) {
  const ws = new WebSocket(url);

  yield new Promise((resolve, reject) => {
      ws.onopen = () => {
          resolve();
      };
      ws.onerror = (error) => {
          reject(error);
      };
  });

  try {
    while (ws.readyState === WebSocket.OPEN) {
      yield new Promise((resolve, reject) => {
        ws.onmessage = (event) => {
          resolve(JSON.parse(event.data));
        };
        ws.onerror = (error) => {
          reject(error);
        };
        ws.onclose = () => {
           resolve(null); // 연결이 닫힐 때 null로 귀결됩니다
        }
      });

    }
  } finally {
    ws.close();
  }
}

const websocketURL = 'wss://example.com/events'; // 웹소켓 URL로 교체하세요
const eventStream = createWebSocketStream(websocketURL);

// 이벤트 스트림 처리
(async () => {
  for await (const event of eventStream.filter(event => event.type === 'user_login').map(event => ({ userId: event.userId, timestamp: event.timestamp }))) {
    console.log('User Login Event:', event);
    // 사용자 로그인 이벤트 처리
  }
})();

이 예제에서 createWebSocketStream은 웹소켓에서 수신된 이벤트를 방출하는 비동기 이터러블을 생성합니다. filter 헬퍼는 사용자 로그인 이벤트만 선택하고, map 헬퍼는 데이터를 원하는 형식으로 변환합니다.

비동기 이터레이터 헬퍼 사용의 이점

브라우저 및 런타임 지원

비동기 이터레이터 헬퍼는 아직 JavaScript의 비교적 새로운 기능입니다. 2024년 후반 기준으로, TC39 표준화 과정의 3단계에 있으며, 이는 가까운 미래에 표준화될 가능성이 높다는 것을 의미합니다. 그러나 아직 모든 브라우저와 Node.js 버전에서 기본적으로 지원되지는 않습니다.

브라우저 지원: Chrome, Firefox, Safari, Edge와 같은 최신 브라우저들은 점차적으로 비동기 이터레이터 헬퍼에 대한 지원을 추가하고 있습니다. Can I use...와 같은 웹사이트에서 최신 브라우저 호환성 정보를 확인하여 어떤 브라우저가 이 기능을 지원하는지 볼 수 있습니다.

Node.js 지원: 최신 Node.js 버전(v18 이상)은 비동기 이터레이터 헬퍼에 대한 실험적 지원을 제공합니다. 이를 사용하려면 --experimental-async-iterator 플래그를 사용하여 Node.js를 실행해야 할 수 있습니다.

폴리필(Polyfills): 기본적으로 지원하지 않는 환경에서 비동기 이터레이터 헬퍼를 사용해야 하는 경우, 폴리필을 사용할 수 있습니다. 폴리필은 누락된 기능을 제공하는 코드 조각입니다. 비동기 이터레이터 헬퍼를 위한 여러 폴리필 라이브러리가 있으며, 널리 사용되는 옵션으로는 core-js 라이브러리가 있습니다.

사용자 정의 비동기 이터레이터 구현하기

비동기 이터레이터 헬퍼는 기존 비동기 이터러블을 처리하는 편리한 방법을 제공하지만, 때로는 자신만의 사용자 정의 비동기 이터레이터를 만들어야 할 수도 있습니다. 이를 통해 데이터베이스, API 또는 파일 시스템과 같은 다양한 소스의 데이터를 스트리밍 방식으로 처리할 수 있습니다.

사용자 정의 비동기 이터레이터를 만들려면 객체에 @@asyncIterator 메서드를 구현해야 합니다. 이 메서드는 next() 메서드를 가진 객체를 반환해야 합니다. next() 메서드는 valuedone 속성을 가진 객체로 귀결되는 프로미스를 반환해야 합니다.

다음은 페이지네이션된 API에서 데이터를 가져오는 사용자 정의 비동기 이터레이터의 예입니다:


async function* fetchPaginatedData(baseURL) {
  let page = 1;
  let hasMore = true;

  while (hasMore) {
    const url = `${baseURL}?page=${page}`;
    const response = await fetch(url);
    const data = await response.json();

    if (data.results.length === 0) {
      hasMore = false;
      break;
    }

    for (const item of data.results) {
      yield item;
    }

    page++;
  }
}

const apiBaseURL = 'https://api.example.com/data'; // API URL로 교체하세요
const paginatedData = fetchPaginatedData(apiBaseURL);

// 페이지네이션된 데이터 처리
(async () => {
  for await (const item of paginatedData) {
    console.log('Item:', item);
    // 항목 처리
  }
})();

이 예제에서 fetchPaginatedData는 페이지네이션된 API에서 데이터를 가져와 각 항목을 검색할 때마다 반환(yield)합니다. 비동기 이터레이터가 페이지네이션 로직을 처리하므로 데이터를 스트리밍 방식으로 쉽게 소비할 수 있습니다.

잠재적인 과제 및 고려 사항

비동기 이터레이터 헬퍼는 수많은 이점을 제공하지만, 몇 가지 잠재적인 과제와 고려 사항을 인지하는 것이 중요합니다:

비동기 이터레이터 헬퍼 사용을 위한 모범 사례

비동기 이터레이터 헬퍼를 최대한 활용하려면 다음 모범 사례를 고려하십시오:

고급 기술

사용자 정의 헬퍼 구성하기

기존 헬퍼를 조합하거나 처음부터 새로 만들어 자신만의 사용자 정의 비동기 이터레이터 헬퍼를 만들 수 있습니다. 이를 통해 특정 요구에 맞게 기능을 조정하고 재사용 가능한 컴포넌트를 만들 수 있습니다.


async function* takeWhile(asyncIterable, predicate) {
  for await (const value of asyncIterable) {
    if (!predicate(value)) {
      break;
    }
    yield value;
  }
}

// 사용 예제:
async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(10);
const firstFive = takeWhile(asyncIterable, x => x <= 5);

(async () => {
  for await (const value of firstFive) {
    console.log(value);
  }
})();

여러 비동기 이터러블 결합하기

zip이나 merge와 같은 기술을 사용하여 여러 비동기 이터러블을 단일 비동기 이터러블로 결합할 수 있습니다. 이를 통해 여러 소스의 데이터를 동시에 처리할 수 있습니다.


async function* zip(asyncIterable1, asyncIterable2) {
    const iterator1 = asyncIterable1[Symbol.asyncIterator]();
    const iterator2 = asyncIterable2[Symbol.asyncIterator]();

    while (true) {
        const result1 = await iterator1.next();
        const result2 = await iterator2.next();

        if (result1.done || result2.done) {
            break;
        }

        yield [result1.value, result2.value];
    }
}

// 사용 예제:
async function* generateSequence1(end) {
    for (let i = 1; i <= end; i++) {
        yield i;
    }
}

async function* generateSequence2(end) {
    for (let i = 10; i <= end + 9; i++) {
        yield i;
    }
}

const iterable1 = generateSequence1(5);
const iterable2 = generateSequence2(5);

(async () => {
    for await (const [value1, value2] of zip(iterable1, iterable2)) {
        console.log(value1, value2);
    }
})();

결론

JavaScript 비동기 이터레이터 헬퍼는 비동기 데이터 스트림을 처리하는 강력하고 우아한 방법을 제공합니다. 데이터 조작에 대한 함수형 및 조합형 접근 방식을 제공하여 복잡한 데이터 처리 파이프라인을 더 쉽게 구축할 수 있도록 합니다. 비동기 이터레이터와 비동기 이터러블의 핵심 개념을 이해하고 다양한 헬퍼 메서드를 마스터함으로써 비동기 JavaScript 코드의 효율성과 유지보수성을 크게 향상시킬 수 있습니다. 브라우저 및 런타임 지원이 계속 증가함에 따라, 비동기 이터레이터 헬퍼는 현대 JavaScript 개발자에게 필수적인 도구가 될 것입니다.