node js 비동기적인 특성으로 인한 문제 해결하기

node js 비동기적인 특성으로 인한 문제 해결하기

상황

요즘 node js를 이용해 백엔드 개발을 진행하는 과정에 node js의 비동기적 특성으로 인해 내가 생각한 순서로 코드가 실행되지 않아 문제가 생기는 경우가 굉장히 많아 해당 상황에 대해 정리함

  1. sql문으로 특정 행 조회
  2. 조회한 데이터를 특정 변수에 저장
  3. 해당 변수를 이용한 로직 구현
  4. 이때 sql문에 문제가 없음에도 불구하고 변수에 값이 없는(undefined) 상태가 발생!

이는 node js의 비동기적인 특성때문이라고 합니다. 즉, sql문을 통해 특정 행을 조회하는 코드를 실행하는 과정이 완전히 끝나지 않았지만, 다음 코드로 넘어가 아직 값을 받지 못한 변수를 이용한 로직을 실행하는 것입니다. 비동기는 빠르다는 장점이 있지만 때로는 이 상황처럼 값을 아직 반환받지 않은 상태에서 다음 단계로 넘어가 문제가 생긴다는 단점도 있습니다.

해결방법

방법1. 콜백(Callback) 사용하기

콜백(callback)은 JavaScript에서 다른 함수에게 인자로 넘겨지는 함수형태입니다. 해당 형태를 이용하면 동기적으로 작동하게 할 수 있습니다. 아래 예시 코드입니다.

app.get('/data', (req, res) => {
    // 1. SQL 쿼리 실행
    connection.query('SELECT * FROM table', function (error, results, fields) {
        if (error) {
            res.status(500).json({ error: 'Internal Server Error' });
            return;
        }
        // 2. 결과를 이용한 작업 수행
        console.log(results);
        res.json(results);
    });
});

sql문을 먼저 실행하고 해당 값이 반환되면 function(error, results, fields)로 시작하는 콜백함수가 실행되어 두 작업이 동기적으로 진행됩니다.

저는 가장 처음 이 방법을 이용했는데 문제가 발생했었습니다. 연속적으로 동기적인 처리를 하는 작업이 필요하면 코드의 깊이이 너무 깊어지는 것입니다. 예를 들면 아래와 같습니다.

Function1(function(result1) {
    Function2(result1, function(result2) {
        Function3(result2, function(result3) {
            // ... 중첩된 콜백 함수가 계속됨 ...
        });
    });
});

많은 사람들이 이런 문제를 겪으며 이를 보통 “콜백지옥”이라고 부릅니다.

콜백지옥

콜백 함수를 중첩하여 사용하면 코드가 길어지고 가독성이 나빠집니다. 또한, 에러 처리도 어려워지며 코드를 이해하고 유지보수하기가 어려워집니다. 이를 위해서 프로미스나 async/await을 사용하면 유지 보수가 용이한 코드를 작성할 수 있습니다.

방법2. 프로미스(Promise) 사용하기

프로미스(Promise)는 비동기 작업을 보다 간결하고 직관적으로 다룰 수 있도록 도와주는 JavaScript 객체입니다. 비동기 작업이 완료되었을 때 성공 또는 실패를 나타내는 객체로, 비동기 작업이 성공하면 해결(resolve)되고, 실패하면 거부(reject)되는 것이 가장 큰 특징입니다.

또한 대기(pending), 이행(fulfilled), 거부(rejected) 3가지 상태를 가질 수 있습니다. 프로미스 객체가 생성되면 대기 상태이며, 비동기 작업이 성공하면 이행 상태가 되고 결과 값을 가지게 되며, 실패하면 거부 상태가 되고 에러를 가지게 됩니다. 아래 예시입니다.

// 비동기 작업을 수행하는 함수를 프로미스로 래핑
function asyncOperation() {
    return new Promise((resolve, reject) => {
        // 비동기 작업 수행
        setTimeout(() => {
            const success = true; // 성공 여부 가정
            if (success) {
                resolve('Success!'); // 성공 시 resolve 호출
            } else {
                reject(new Error('Failed!')); // 실패 시 reject 호출
            }
        }, 1000); // 1초 후에 작업 완료
    });
}

// 프로미스 사용
asyncOperation()
    .then(result => {
        console.log(result); // 성공 시 실행
    })
    .catch(error => {
        console.error(error); // 실패 시 실행
    });

방법3. async, await 사용하기

ES6부터 지원되는 async/await를 사용하여 비동기적인 코드를 동기적으로 작성할 수 있습니다. 아래 예시는 프로미스와 async/await 방식을 같이 사용한 코드입니다.

app.get('/data', async (req, res) => {
    try {
        // async/ await
        const results = await executeQuery('SELECT * FROM table');
        console.log(results);
        res.json(results);
    } catch (error) {
        console.error(error);
        res.status(500).json({ error: 'Internal Server Error' });
    }
});

// 프로미스
function executeQuery(sql) {
    return new Promise((resolve, reject) => {
        connection.query(sql, function (error, results, fields) {
            if (error) {
                reject(error);
                return;
            }
            resolve(results);
        });
    });
}

혹은 프로미스 메서드를 지원하는 경우 다음과 같이 더 간결하게 코드 작성이 가능합니다.(mysql은 프로미스 기능 제공)

app.get('/data', async (req, res) => {
    try {
        // SQL 쿼리 실행
        const results = await connection.promise().query('SELECT * FROM table');
        // 결과를 이용한 작업 수행
        console.log(results[0]);
        res.json(results[0]);
    } catch (error) {
        // 오류 처리
        console.error(error);
        res.status(500).json({ error: 'Internal Server Error' });
    }
});


results matching ""

    No results matching ""