Node.js에서 비동기 처리는 매우 강력한 기능이지만, 그만큼 실수하기 쉬운 부분이기도 합니다. 이 글에서는 Node.js에서 자주 발생하는 비동기 처리 실수와 그에 대한 해결 방법을 구체적으로 살펴보겠습니다.

1. 콜백 지옥 (Callback Hell)

Node.js의 비동기 처리는 주로 콜백 함수를 통해 이루어집니다. 하지만 여러 개의 비동기 작업을 순차적으로 실행하다 보면, 코드가 복잡해지고 읽기 어려운 상태가 됩니다. 이를 '콜백 지옥'이라고 부릅니다.

예제:

const fs = require('fs');

fs.readFile('file1.txt', 'utf8', (err, data1) => {
    if (err) {
        console.error(err);
        return;
    }
    fs.readFile('file2.txt', 'utf8', (err, data2) => {
        if (err) {
            console.error(err);
            return;
        }
        fs.readFile('file3.txt', 'utf8', (err, data3) => {
            if (err) {
                console.error(err);
                return;
            }
            console.log(data1, data2, data3);
        });
    });
});

해결 방법: Promise 사용하기 Promise를 사용하면 비동기 작업을 더 깔끔하게 처리할 수 있습니다.

예제:

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

async function readFiles() {
    try {
        const data1 = await fs.readFile('file1.txt', 'utf8');
        const data2 = await fs.readFile('file2.txt', 'utf8');
        const data3 = await fs.readFile('file3.txt', 'utf8');
        console.log(data1, data2, data3);
    } catch (err) {
        console.error(err);
    }
}

readFiles();

2. 에러 처리 미비

비동기 작업에서 발생하는 에러를 처리하지 않으면, 예기치 않은 문제를 초래할 수 있습니다. 특히 Promise를 사용할 경우, .catch()를 사용해야만 에러를 처리할 수 있습니다.

예제:

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

fs.readFile('nonexistent.txt', 'utf8')
    .then(data => {
        console.log(data);
    })
    .catch(err => {
        console.error('파일을 읽는데 실패했습니다:', err);
    });

3. 비동기 함수의 반환값 처리

비동기 함수는 항상 Promise를 반환합니다. 이를 간과하고 일반적인 값을 기대하면 문제가 발생할 수 있습니다. 비동기 함수의 결과를 사용하려면 반드시 await를 사용해야 합니다.

예제:

async function getData() {
    return 'Hello, World!';
}

const result = getData(); // Promise가 반환됨
console.log(result); // 출력: Promise { <pending> }

해결 방법: await 사용하기

async function printData() {
    const result = await getData(); // Promise 해결 후 값 사용
    console.log(result); // 출력: Hello, World!
}

printData();

4. 비동기 동기화 문제

비동기 작업이 완료되기 전에 코드가 진행되면, 의도하지 않은 결과가 발생할 수 있습니다. 이를 방지하기 위해 Promise.all을 사용할 수 있습니다.

예제:

async function fetchData() {
    const data1 = await fetch('https://api.example.com/data1');
    const data2 = await fetch('https://api.example.com/data2');
    return [data1, data2];
}

// 문제 발생
fetchData().then(data => {
    console.log(data);
});

// 해결 방법
async function fetchAllData() {
    const [data1, data2] = await Promise.all([
        fetch('https://api.example.com/data1'),
        fetch('https://api.example.com/data2')
    ]);
    return [data1, data2];
}

fetchAllData().then(data => {
    console.log(data);
});

5. 순환 참조로 인한 무한 루프

가끔 비동기 메서드 내부에서 다른 비동기 메서드를 호출할 때, 서로가 서로를 호출하여 무한 루프를 발생시킬 수 있습니다. 이런 상황에서는 호출 구조를 명확히 하고, 조건을 설정하여 루프를 방지해야 합니다.

예제:

async function funcA() {
    await funcB();
}

async function funcB() {
    await funcA(); // 무한 루프 발생
}

// 해결 방법: 조건 추가
let isLooping = false;

async function safeFuncA() {
    if (isLooping) return;
    isLooping = true;
    await safeFuncB();
    isLooping = false;
}

async function safeFuncB() {
    await safeFuncA(); // 조건에 따라 방지
}

마무리

비동기 처리는 Node.js의 강력한 기능이지만, 그만큼 주의해야 할 점이 많습니다. 콜백 지옥을 피하고, 에러 처리를 철저히 하며, Promise와 async/await를 활용하여 코드의 가독성을 높이는 것이 중요합니다. 위에서 다룬 실수를 피하면서 여러분의 Node.js 애플리케이션을 더욱 견고하고 세련되게 만들어 보시기 바랍니다.