18. 비동기

18-1. 동기와 비동기 정의

18-1-1. 동기

동기(Synchronous) 코드는 순차적으로 실행되며, 한 번에 하나의 작업만 처리한다. 코드 한 줄이 실행되면 그다음 줄이 실행된다. 다른 작업이 완료될 때까지 현재 작업을 멈추고 기다린다. 이러한 동작은 코드의 진행을 차단하고, 어떤 작업이 끝나길 기다려야 할 때 문제가 될 수 있다.

18-1-2. 비동기

비동기(Asynchronous) 코드는 작업을 백그라운드에서 병렬로 처리하며, 기다리지 않고 코드 실행을 계속할 수 있다. 이벤트 루프를 통해 비동기 작업이 완료되면 콜백 함수나 promise를 통해 처리할 수 있다.

18-2. 가장 많이 사용하는 비동기 처리

가장 많이 사용하는 비동기처리 시스템을 알아보도록 하자, 대표적으로 call back, promise, async/await가 있다.

18-2-1. call back

콜백은 한 작업이 끝나고 다음 작업에 들어가는 가장 근본적인 방법이다.
// 첫 번째 비동기 작업 function 작업1(callback) { setTimeout(function() { console.log('작업1 완료'); callback(); }, 1000); } // 두 번째 비동기 작업 function 작업2(callback) { setTimeout(function() { console.log('작업2 완료'); callback(); }, 1000); } // 첫 번째 작업을 수행하고, 그 후에 두 번째 작업을 수행 작업1(function() { 작업2(function() { console.log('모든 작업 완료'); }); });
위 코드를 보면 알 수 있지만, 작업1 함수 안에 작업2를 넣어서, 작업1을 실행하고 나서 작업2를 실행하게 만든다. 만일 이러한 비동기 코드를 만들었을 때 수백 개의 코드가 있다면 유지보수가 어려워질 것이다.
작업1 완료 작업2 완료 모든 작업 완료

18-2-2. promise

promise는 콜백에서 문제점을 제거하기 위한 패턴이다. then을 이용해서 함수 이후에 실행할 작업을 관리해, 함수 안에 중첩된 함수가 있는 경우를 제어할 수 있다.
// 첫 번째 비동기 작업을 Promise로 구현 function 작업1() { return new Promise(function(resolve, reject) { setTimeout(function() { console.log('작업1 완료'); resolve(); // 작업이 성공적으로 완료됨을 알림 }, 1000); }); } // 두 번째 비동기 작업을 Promise로 구현 function 작업2() { return new Promise(function(resolve, reject) { setTimeout(function() { console.log('작업2 완료'); resolve(); // 작업이 성공적으로 완료됨을 알림 }, 1000); }); } // Promise를 사용하여 비동기 작업 연결 및 순차 실행 작업1() .then(function() { return 작업2(); }) .then(function() { console.log('모든 작업 완료'); }) .catch(function(error) { console.error('에러 발생:', error); }) .finally(function() { console.log('finally는 작업 완료 후 항상 실행됨'); });
promise를 사용하면 위처럼 함수 안에 함수가 있는 것을 방지할 수 있다. then() 이후의 함수를 실행한다. catch()는 에러가 뜰 때만 발생하고 inally() 함수가 끝나거나 에러가 뜰 때, 마지막에 무조건 한번 실행된다.
작업1 완료 작업2 완료 모든 작업 완료 finally는 작업 완료 후 항상 실행됨

18-2-2-1. Thenable

thenable은 가독성을 위해 사용하는 함수이다.
let thenable = { then: function(resolve, reject) { resolve(42); } }; Promise.resolve(thenable).then(function(value) { console.log(value); // 42 });
Promise 객체는 성공, 실패, 현재 진행 상태와 같은 세 가지 상태를 갖는다. then()메서드를 사용하여 Promise의 결과를 처리할 수 있다. 이때, thenable은 then()메서드를 가지고 있어 Promise와 유사한 방식으로 동작할 수 있는 객체를 가리킨다.

18-2-2-2. Promise.all()

Promise.all() 함수는 병렬처리를 하기 위해 사용한다. Promise.all()은 여러 개의 Promise을 동시에 실행하고, 모든 Promise가 성공적으로 완료될 때까지 기다린 후 결과를 배열로 반환한다. 만약 하나의 Promise라도 실패하면 첫 번째로 실패한 Promise의 결과 또는 에러를 반환한다.
const promises = [promise1, promise2, promise3]; Promise.all(promises) .then((results) => { // 모든 Promise가 성공한 경우 실행 console.log(results); }) .catch((error) => { // 하나 이상의 Promise가 실패한 경우 실행 console.error(error); });

18-2-2-3. Promise.race()

여러 개의 Promise를 대기하고, 그중 하나가 먼저 완료되면 해당 Promise의 결과 또는 에러를 반환하는 메서드이다. 이 메서드는 여러 개의 비동기 작업 중에서 가장 먼저 끝난 작업의 결과만 필요할 때 유용하다.
Promise.race() 메서드의 구문은 다음과 같다.
Promise.race(iterable);
  • iterable: Promise 객체의 배열 또는 이터러블(iterable) 객체를 받는다. 여러 개의 Promise를 이 배열 또는 이터러블에 전달하고, 그중 하나가 완료될 때까지 대기한다.
  1. 주어진 Promise 중 하나라도 먼저 완료되면 해당 Promise의 결과 또는 에러를 반환한다.
  1. 나머지 Promise는 계속 백그라운드에서 실행되지만, 그 결과는 무시된다.
  1. 반환된 Promise는 가장 먼저 완료된 Promise에 대한 결과 또는 에러를 가지게 된다.
const promises = [ fetch('url1'), fetch('url2'), // ... ]; Promise.race(promises) .then((result) => { // 가장 먼저 완료된 Promise의 결과를 처리 }) .catch((error) => { // 가장 먼저 실패한 Promise의 에러를 처리 });

18-2-2-4. for-awit-of

JavaScript에서 비동기 반복을 처리하기 위한 구문이다. 기본적으로 for-await-of는 비동기적으로 반복 가능한 객체를 동기적으로 반복하면서 await키워드를 사용하여 각 항목을 처리할 수 있도록 해준다. 병렬처리는 보통 순차적으로 실행하지 않는다 일반적으로 for-await-of는 비동기적으로 처리해야 하는 작업을 담은 Promise 객체의 배열 또는 비동기적으로 데이터를 가져올 수 있는 Iterable 객체를 반복할 때 사용된다. 이 구문은 for…of와 유사하나, 각 항목을 처리할 때 await을 사용할 수 있어 비동기 코드를 더욱 간결하게 작성할 수 있다. 다음은 for-await-of의 기본 구문과 예제이다.
// 비동기 작업을 시뮬레이션하는 함수 async function asyncTask1() { await new Promise((resolve) => setTimeout(resolve, 1000)); return 'Task 1 completed'; } async function asyncTask2() { await new Promise((resolve) => setTimeout(resolve, 1000)); return 'Task 2 completed'; } async function asyncTask3() { await new Promise((resolve) => setTimeout(resolve, 1000)); return 'Task 3 completed'; } // 비동기 작업을 담은 Promise 배열 const promises = [asyncTask1(), asyncTask2(), asyncTask3()]; // 비동기적으로 반복 처리 async function processPromises() { for await (const result of promises) { console.log(result); // 각 Promise의 결과 출력 } } processPromises();
race는 최초의 로직만 실행한다면 for-awit-of 로직은 모든 로직을 실행한다.
Task 1 completed Task 2 completed Task 3 completed

18-2-3. async/await

// 첫 번째 비동기 작업을 Promise로 구현 function 작업1() { return new Promise(function(resolve, reject) { setTimeout(function() { console.log("작업1 완료"); resolve(); // 작업이 성공적으로 완료됨을 알림 }, 1000); }); } // 두 번째 비동기 작업을 Promise로 구현 function 작업2() { return new Promise(function(resolve, reject) { setTimeout(function() { console.log('작업2 완료'); resolve(); // 작업이 성공적으로 완료됨을 알림 }, 1000); }); } } // 비동기 함수를 정의하고 async/await를 사용하여 작업 연결 및 순차 실행 async function main() { try { await 작업1(); await 작업2(); console.log('모든 작업 완료'); } catch (error) { console.error('에러 발생:"', error); } } // main 함수를 호출하여 실행 main();
위 코드는 await로 모든 것을 제어한다. 이에 따라서 코드를 수정할 때 다른 함수에서 찾아갈 필요 없이 간단하게 정리할 수 있다. 단, 이때 async를 function 앞에 달아야 한다. 하지만 무리하게 await를 사용할 경우 라이브러리 등 다른 함수에서 가져온 함수가 비동기를 지원하지 않아서 사용할 수 없을 때 아무런 거부감 없이 사용하면 오히려 자주 에러가 뜰 수도 있다.
작업1 완료 작업2 완료 모든 작업 완료

18-3. 병렬 비동기 처리란

아래에는 병렬 비동기 처리, Web Workers, SharedArrayBuffer, 그리고 Atomics에 관한 간단한 설명과 예시 코드이다.
병렬 비동기 처리: 병렬 비동기 처리는 여러 개의 비동기 작업을 동시에 실행하고, 모든 작업이 완료될 때까지 기다리지 않고 결과를 수집하는 것을 의미한다. 대표적으로 Promise.all() 이 있다.
javascriptCopy code const promises = [ fetch('url1'), fetch('url2'), fetch('url3'), ]; Promise.all(promises) .then((results) => { // 모든 Promise가 성공하면 여기서 결과 처리 }) .catch((error) => { // 하나 이상의 Promise가 실패하면 여기서 에러 처리 });

18-3-1. Web Workers

Web Workers는 웹 애플리케이션에서 병렬로 실행되는 백그라운드 스레드를 생성하고 사용하는데 도움을 주는 기술이다. 아래는 Web Worker를 사용하여 병렬로 스크립트를 실행하는 간단한 예시 코드이다.
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads'); if (isMainThread) { // 메인 스레드에서 실행할 코드 const worker = new Worker(__filename, { workerData: 'Hello from main thread!', }); worker.on('message', (message) => { console.log(`Message from worker: ${message}`);// "Message from worker:Hello from worker thread!" }); worker.postMessage('Hello from main thread!'); } else { // 워커 스레드에서 실행할 코드 const workerData = require('worker_threads').workerData; console.log(workerData); // "Hello from main thread!" parentPort.on('message', (message) => { console.log(`Message from main thread: ${message}`);// "Message from main thread:Hello from main thread!" }); parentPort.postMessage('Hello from worker thread!'); }
위 로직은 isMainThread에서 현재가 메인 스레드인지 판단한다. 메인 스레드는 화면을 그릴 때 사용하는 스레드를 말한다.
const worker = new Worker(__filename, { workerData: 'Hello from main thread!', });
위와 같이 Worker을 생성한다. Worker는 백그라운드에서 실행하며 메인 스레드와 별개의 스레드로 실행하는 로직이다. 이로써 화면을 그리고 있는 cpu가 독립적으로 활동 가능하다.
const workerData = require('worker_threads').workerData;
위에 worker_threads에 저장시켜 둔 workerData를 가져오는 방법이다.
worker.on('message', (message) => { console.log(`Message from worker: ${message}`); }); worker.postMessage('Hello from main thread!'); parentPort.on('message', (message) => { console.log(`Message from main thread: ${message}`); }); parentPort.postMessage('Hello from worker thread!');
worker.on은 백그라운드 스레드에서 통신이 온 것을 받는다. parentPort.on은 부모 스레드에서 통신이 온 것을 받는다. 각각의 통신은 .postMessage을 사용해서 소통한다.

18-3-2. SharedArrayBuffer

SharedArrayBuffer는 여러 웹 워커 사이에서 메모리를 공유하는 데 사용된다. SharedArrayBuffer를 사용하여 데이터를 워커 간에 공유하고 동기화할 수 있다.
// main.js const sharedBuffer = new SharedArrayBuffer(16); // 16바이트 크기의 공유 버퍼 생성 // 첫 번째 웹 워커 const worker1 = new Worker('worker.js'); worker1.postMessage(sharedBuffer); // 두 번째 웹 워커 const worker2 = new Worker('worker.js'); worker2.postMessage(sharedBuffer);
// worker.js onmessage = function (e) { const sharedBuffer = e.data; // 공유 버퍼 가져오기 const sharedArray = new Int32Array(sharedBuffer); // 공유 버퍼로부터 Int32Array 생성 // 공유 버퍼에 데이터 쓰기 for (let i = 0; i < sharedArray.length; i++) { Atomics.store(sharedArray, i, i + 1); } // 쓰여진 데이터 읽어오기 for (let i = 0; i < sharedArray.length; i++) { console.log(`Worker ${i + 1} - Value at index ${i}: ${Atomics.load(sharedArray, i)}`); } };
SharedArrayBuffer은 하나의 Worker를 여러 로직에 사용할 때 사용한다.

18-3-3. Atomics

Atomics 객체는 SharedArrayBuffer와 함께 멀티 스레딩 환경에서 사용된다. SharedArrayBuffer는 여러 로직이 사용하지만 다른 스레드에서 변경을 했을 때 공유 중인 worker 파일이 원자성을 보장받지 못해 그것을 보장하기 위해 Atomics 나왔다.
원자성이란 다른 로직이 변경하는 명령을 하여도 변경되지 않고 원래 자신값을 가지는 것을 의미한다.
// 웹 워커 const { Atomics, SharedArrayBuffer } = require('worker_threads'); const buffer = new SharedArrayBuffer(4); const view = new Int32Array(buffer); Atomics.store(view, 0, 42); // 원자적으로 값을 설정 const result = Atomics.load(view, 0); // 원자적으로 값을 읽음 console.log(result); // 42