Logic in Code,
Freedom in Travel.

인생 뭐 있나 사람 사는거 다 똑같지

Programming/노드 (NodeJS)

[Node.js] 자바스크립트 비동기 처리 - 비동기의 완성 Async/Await

귀찮은 개발자2026. 1. 9. 00:00
목차 (Table of Contents)

비동기를 동기처럼 작성하다 

아주 먼 과거에는 콜백지옥이 있었고 조금 먼 과거에는 프로미스 지옥이 있었다. 조금 먼 과거의 프로미스 지옥은 Promise의 .then() 체이닝이 길어질수록 코드가 복잡해졌고, 특정 스코프의 변수를 공유하기 위해 파라미터를 계속 넘겨줘야 하는 문제가 여전히 남아있었다. 

PHP나 Python, Go와 같은 언어를 다루다 Node.js로 넘어오면서 '위에서 아래로 흐르는 직관적인 코드' 코드를 보기 힘들었던 시절이 있었다. 그리고 ES2017(ES8) 가 등장하면서 자바스크립트는 async/await 새로운 문법을 하사받았다. 

Async/Await

async/await 은 새로운 기술이 아니고 내부적으로는 Promise를 사용하지만, 이를 겉으로 보기에 동기 코드처럼 보이게 만드는 Syntactic Sugar 이다. 함수 앞에 async를 붙이고 Promise를 반환하는 함수 앞에 await를 붙이면 자바스크립트 엔진은 그 Promise가 해결(resolve)될 때까지 기다렸다가 다음 줄을 실행한다. Node.js v7.6부터 이를 네이티브로 지원하면서 서버 사이드 개발이 생산성적으로 증가했다.

Promise 코드의 현대적 리팩토링

async/await 예제 

const fs = require('fs').promises;

// async 키워드로 비동기 함수임을 선언
async function processUserData() {
    // 동기 코드와 동일한 try-catch 에러 처리
    try {
        // 1. 설정 파일 읽기
        const configData = await fs.readFile('config.json', 'utf8');
        const config = JSON.parse(configData);

        // 2. DB 연결
        const connection = await db.connect(config.dbUrl);

        // 3. 쿼리 실행
        const rows = await connection.query('SELECT * FROM users');

        // 4. 결과 저장
        await fs.writeFile('users.log', JSON.stringify(rows));
        
        console.log('완료');
        
        // 리소스 정리
        connection.end();

    } catch (err) {
        // 모든 에러를 여기서 처리함
        console.error('에러 발생:', err);
    }
}

async/await 의 도입으로 then/catch/finally 을 사용하지 않게 되었고 복잡한 괄호도 필요가 없어졌다. 변수 config, connection, rows 가 동기 함수처럼 선언되어 함수 내부 어디서든 자유롭게 접근할 수 있게 되면서 코드의 가독성도 이전보다 훨씬 좋아진게 보인다.

비동기 패턴

async/await 을 사용할 때 주의해야 할 점과 최신 기능

1. Promise.all 을 잘 활용하기 

최근 회사에서 보이는 가장 이해가 되지 않는 것이 모든 곳에 await를 붙여 병렬로 처리할 수 있는 작업까지 직렬로 만드는 것입니다. 서로 의존성이 없는 작업은 Promise.all 과 await 를 조합하는게 Latency 성능에 좋다. 

// Bad: 순차적으로 실행되어 총 3초+ 소요
const user = await getUser();
const posts = await getPosts();
const comments = await getComments();

// Good: 병렬로 실행되어 가장 긴 작업 시간만큼만 소요
const [user, posts, comments] = await Promise.all([
    getUser(),
    getPosts(),
    getComments()
]);

그렇다고 부조건 Promise.all 을 사용하면 데이터베이스에 순간적인 부하(Spike)를 줄 수 있다. 항상 모든곳에서 사용하기보단 상황에 따라 잘 사용해야한다.

무지성 사용은 Connection Pool 의 자원을 고갈시킬 수 있으며 이로 인해 다른 요청들이 Blocking 상태가 발생하거나 Timout 문제가 발생할 수 있다. 또는 너무 무거운 쿼리를 사용하여 데이터베이스의 CPU/Memory 급증으로 서버의 리소스가 빠르게 소진되어 전체 서비스 성능 저하로 빠질 수 있다. 

때문에 서비스가 감당 가능할정도의 소규모 병렬처리때에만 Promise.all 을 사용해야하며 감당은 어렵고 Latency 성능은 지켜야 한다면 Batch 를 사용하가나 Chunk 단위로 나눠 사용해야한다. 또는 동시성 제한을 두고 사용하는것도 하나의 방법이다. 

2. Top-Level Await

과거에는 await를 쓰려면 무조건 async 함수로 감싸야했다. 하지만 Node.js(v14.8+)와 ES Modules 환경에서는 모듈의 최상위 레벨에서 바로 await를 사용할 수 있어 초기화 코드가 훨씬 간단해졌다. 

계속되는 비동기의 진화

콜백 지옥에서 Promise 를 지나 Async/Await 가 도입되는 과정을 되돌아봤다. 이 역사적 과정은 단순히 문법이 편해진것만이 아니다. 개발자가 소프트웨어의 동작 방식(이벤트 루프)에 맞추던 코딩 스타일에서, 소프트웨어가 개발자의 사고방식(절차적 사고)을 이해하는 방향으로 발전해 온 인간 중심의 기술 진화이다. 

최근 2-3년 전부터는 Deno나 Bun 처럼 새로운 런타임들이 등장하며 비동기 I/O 성능을 최대한 끌어올리고 있고, WebAssembly를 통해 다른 언어의 비동기 패턴을 브라우저로 가져오기도한다. 

하지만 도구가 아무리 변해도  "효율적인 자원 관리" 는 앞으로도 계속 발전되고 새로운게 나올 것 같다.