현대 JavaScript TC39 제안으로 비동기 백엔드 작업 간소화하기
James Reed
Infrastructure Engineer · Leapcell

소개
JavaScript가 백엔드 개발, 특히 Node.js 생태계 내에서 보편적인 언어로 자리매김함에 따라, 비동기 작업의 효과적인 관리는 강력하고 확장 가능한 애플리케이션 구축의 초석으로 남아 있습니다. 콜백, 프로미스, async/await를 다루는 것은 대부분의 JavaScript 개발자에게 익숙한 일입니다. 그러나 외부 이벤트 리스너나 지연된 프로미스 해석과 관련된 특정 비동기 패턴은 때때로 상용구 코드나 최적이 아닌 가독성으로 이어질 수 있습니다. TC39 위원회에서 주도하는 JavaScript의 지속적인 발전은 새로운 언어 기능을 도입하여 이러한 과제를 해결하기 위해 끊임없이 노력하고 있습니다. 최근의 흥미로운 제안 중 Promise.withResolvers는 특정 비동기 흐름을 간소화하는 데 특히 유망한 후보로 부상하며, 프로미스를 생성하고 관리하는 데 더 간소화된 접근 방식을 제공합니다. 이 글에서는 Promise.withResolvers를 주요 예로 하여 이러한 최신 TC39 제안이 백엔드 서비스의 비동기 코드를 어떻게 크게 간소화하여 유지보수하기 쉽고 이해하기 쉽게 만드는지에 대해 자세히 알아봅니다.
비동기 JavaScript의 기초
Promise.withResolvers의 복잡한 내용으로 들어가기 전에, 그 가치를 이해하는 데 중요한 몇 가지 기본 개념을 빠르게 다시 살펴보겠습니다.
- 프로미스: 핵심적으로 프로미스는 비동기 작업의 최종 완료 또는 실패를 나타내는 객체입니다. 순차적인 작업과 오류 처리를 위한 체이닝(
.then(),.catch())을 활성화하여 비동기 결과를 처리하는 콜백보다 깔끔한 대안을 제공합니다. 프로미스는 보류 중(pending), 이행됨(fulfilled, resolved), 거부됨(rejected)의 세 가지 상태 중 하나에 있을 수 있습니다. - 비동기 코드 패턴:
- 콜백: 비동기 작업이 완료되면 실행될 함수로 인수로 전달됩니다. 근본적이지만, 깊이 중첩된 구조로 "콜백 지옥"을 유발할 수 있습니다.
- Async/Await: 프로미스 위에 구축된 구문 설탕으로, 비동기 코드가 동기 코드처럼 보이게 하고 작동하게 합니다.
async함수는 항상 프로미스를 반환하며,await는async함수의 실행을 프로미스가 안정될 때까지 일시 중지한 다음resolved 값을 풀거나 rejection 이유를 throw합니다.
- TC39 제안: 기술 위원회 39(TC39)는 ECMAScript(JavaScript)를 표준화하는 역할을 합니다. 새 기능은 "Stage 0: Strawperson"에서 "Stage 4: Finished"(ECMAScript 표준에 포함될 준비 완료)까지의 다단계 제안 프로세스를 통해 도입됩니다.
Promise.withResolvers는 현재 Stage 3에 있는 그러한 제안 중 하나입니다.
Promise.withResolvers: 프로미스 관리를 위한 새로운 도구
역사적으로 프로미스의 resolve 및 reject 함수를 실행 함수 외부로 노출해야 하는 프로미스를 만드는 것은 번거로웠습니다. 이벤트 기반 시스템이나 기본적으로 프로미스를 반환하지 않고 작업이 완료될 때 콜백을 트리거하는 레거시 API와 통합하는 시나리오를 생각해 보세요. 일반적으로 새 Promise 생성자로 이를 래핑했을 것입니다.
// resolve/reject를 노출하는 전통적인 방법 let resolver; let rejecter; const myDeferredPromise = new Promise((resolve, reject) => { resolver = resolve; rejecter = reject; }); // 나중에, 아마도 이벤트 핸들러나 다른 함수에서: // resolver('Operation completed successfully!'); // rejecter(new Error('Operation failed!'));
기능적이지만 이 패턴은 외부 범위에서 resolver와 rejecter를 선언해야 하므로 번거롭고 캡슐화가 덜하다고 느껴질 수 있습니다. 이것이 바로 Promise.withResolvers가 해결하려는 문제입니다.
원칙 및 구현
Promise.withResolvers 정적 메서드는 동일한 지연된 프로미스 해석을 달성하기 위한 더 깔끔하고 더 관용적인 방법을 제공합니다. promise, resolve, reject 세 가지 속성을 포함하는 객체를 반환합니다.
// Promise.withResolvers 사용 (Stage 3 제안) const { promise, resolve, reject } = Promise.withResolvers(); // 'promise'는 await하거나 체인할 수 있는 프로미스입니다. // 'resolve'는 프로미스를 이행하는 함수입니다. // 'reject'는 프로미스를 거부하는 함수입니다. // 사용 예: setTimeout(() => { const success = Math.random() > 0.5; if (success) { resolve('Data loaded successfully!'); } else { reject(new Error('Failed to load data.')); } }, 1000); // 백엔드 코드의 다른 부분에서: (async () => { try { const result = await promise; console.log('Async operation result:', result); } catch (error) { console.error('Async operation failed:', error.message); } })();
백엔드 비동기 코드를 간소화하는 방법:
-
이벤트 기반 아키텍처를 위한 더 깔끔한 코드: 백엔드 서비스는 종종 메시지 큐(예: RabbitMQ, Kafka), 웹 소켓 또는 파일 시스템 감시자와 상호 작용하며, 이들은 본질적으로 이벤트 기반입니다.
Promise.withResolvers를 사용하면 중첩된 상용구를 사용하지 않고 이러한 이벤트를 쉽게 "프로미스화"할 수 있습니다.// 시나리오: 메시지 큐에서 특정 메시지 기다리기 // (가상 `mqClient`가 'message' 이벤트를 발생시키는 예시) function waitForMessage(topic) { const { promise, resolve, reject } = Promise.withResolvers(); const messageHandler = (msg) => { if (msg.topic === topic) { mqClient.off('message', messageHandler); // 리스너 정리 resolve(msg.payload); } }; const errorHandler = (err) => { mqClient.off('error', errorHandler); mqClient.off('message', messageHandler); reject(err); }; mqClient.on('message', messageHandler); mqClient.on('error', errorHandler); // 선택 사항: 무한 대기를 방지하기 위한 타임아웃 추가 setTimeout(() => { mqClient.off('message', messageHandler); mqClient.off('error', errorHandler); reject(new Error(`Timeout waiting for message on topic: ${topic}`)); }, 5000); return promise; } // 비동기 백엔드 라우트 핸들러에서의 사용 app.get('/api/message/:topic', async (req, res) => { try { const messageData = await waitForMessage(req.params.topic); res.json({ status: 'success', data: messageData }); } catch (error) { console.error(`Error waiting for message: ${error.message}`); res.status(500).json({ status: 'error', message: error.message }); } });이 예시는
Promise.withResolvers가 프로미스 제어를 중앙 집중화하여 외부 이벤트 핸들러에resolve및reject를 쉽게 연결하는 방법을 명확하게 보여줍니다. -
경쟁 상태 및 타임아웃 간소화: 여러 비동기 작업과 첫 번째 완료(또는 타임아웃)를 기반으로 해결해야 하는 경우
Promise.withResolvers를 사용하면 설정이 간소화될 수 있습니다.Promise.race는 훌륭하지만, 어떤 비동기 작업이 특정 프로미스를 해결하는지에 대한 세밀한 제어가 필요할 때가 있습니다.// 예시: 프로미스 기반이 아닌 네트워크 라이브러리를 통한 요청-응답 패턴 구현 // `networkClient`에 응답 수신 시 콜백을 받는 `send` 메서드가 있다고 가정합니다. function sendRequestWithCorrelationId(requestPayload) { const { promise, resolve, reject } = Promise.withResolvers(); const correlationId = generateUniqueId(); // 이 요청에 대한 고유 ID const responseHandler = (response) => { if (response.correlationId === correlationId) { networkClient.off('response', responseHandler); // 정리 resolve(response.data); } }; const errorHandler = (err) => { networkClient.off('error', errorHandler); networkClient.off('response', responseHandler); reject(err); }; networkClient.on('response', responseHandler); networkClient.on('error', errorHandler); // 요청 전송 (send` 메서드가 결국 'response' 이벤트를 발생시킨다고 가정) networkClient.send({ ...requestPayload, correlationId }); // 선택 사항: 요청 타임아웃 setTimeout(() => { networkClient.off('response', responseHandler); networkClient.off('error', errorHandler); reject(new Error(`Request timed out for correlationId: ${correlationId}`)); }, 3000); // 3초 타임아웃 return promise; } // 서비스 계층에서의 사용: async function processUserData(userId) { try { const userData = await sendRequestWithCorrelationId({ type: 'fetchUser', id: userId }); console.log('User data received:', userData); return userData; } catch (error) { console.error('Failed to fetch user data:', error.message); throw error; // 상위 오류 처리를 위해 다시 throw } } -
레거시 API 통합: 많은 오래된 Node.js 모듈 또는 외부 라이브러리가 프로미스를 직접 반환하지 않을 수 있습니다.
Promise.withResolvers는 콜백 기반 API를 프로미스 인터페이스 내에 래핑하여async/await와 호환되도록 만드는 깔끔한 브리지를 제공합니다. 이는 최신 JavaScript와 이전 코드베이스의 통합 시 인지 부하를 크게 줄입니다.
요컨대, Promise.withResolvers는 프로미스 생성자 함수의 let 선언이나 번거로운 범위 조정 없이 프로미스의 내부 제어 함수(resolve, reject)에 직접 액세스할 수 있도록 제공합니다. 이를 통해 지연된 해석 의도가 훨씬 명확해지며, 특히 진정한 외부의 비프로미스 기반 이벤트 또는 작업과 관련된 경우 더 간결하고 이해하기 쉬운 코드로 이어집니다.
결론
Promise.withResolvers 제안은 JavaScript의 비동기 기능에 대한 신중한 개선을 나타내며, 지연된 프로미스 해석이 필요한 시나리오에 대해 더 깔끔하고 직접적인 API를 제공합니다. promise, resolve, reject 함수에 대한 즉각적인 액세스를 제공함으로써 이벤트 이미터와의 통합, 사용자 정의 요청-응답 주기 관리, 콜백 기반 레거시 코드와의 상호 작용 현대화와 같이 백엔드 개발에서 흔히 볼 수 있는 패턴을 단순화합니다. 이 제안이 표준화로 나아감에 따라 채택은 Node.js 애플리케이션에서 더 읽기 쉽고 유지보수하기 쉬우며 강력한 비동기 코드로 이어질 것이며, 개발자가 더 쉽게 복잡한 백엔드 시스템을 구축할 수 있도록 지원할 것입니다. 이 새로운 기본 기능은 최신 JavaScript 백엔드에서 복잡한 비동기 흐름을 오케스트레이션하는 데 귀중한 도구가 될 것입니다.

