개발/JavaScript

[JavaScript] 콜백 지옥(Callback Hell) 탈출을 위한 코드작성 - promise, async/await

반응형

❓ 콜백 함수 (callback function)

  • 콜백(callback) 또는 콜백 함수(callback function)는 다른 코드의 인수로서 넘겨주는 실행 가능한 코드를 말한다.
    콜백을 넘겨받는 코드는 이 콜백을 필요에 따라 즉시 실행할 수도 있고, 아니면 나중에 실행할 수도 있다.
function sum(a, b, callback) {
  callback(a + b);
}

sum(2, 5, (result) => {
  console.log(result);
});

위 코드는 sum이라는 함수의 인자로 정수 a,b 와 콜백 함수를 넘겨 주며 콜백 함수에서는 두 정수 a와 b를 합한 결과인 result를 콘솔로 출력한다.


❓ 콜백 지옥 (callback hell)

  • 콜백 지옥이란 JavaScript에서 비동기 프로그래밍시 흔히 발생하는 문제로 여러개의 콜백함수가 연속적으로 사용되어 반복되는 들여쓰기로 인해 코드의 가독성이 떨어지는 현상을 의미한다. 코드의 들여쓰기가 마치 피라미드 모양과도 같아 운명의 피라미드(Pyramid of doom)라고 불리기도 하며 이러한 형태의 코드는 유지보수에도 어려움을 주게 된다.
a((resultA) => {
    b(resultA, (resultB) => {
        c(resultB, (resultC) => {
            d(resultC, (resultD) => {
                e(resultD, (resultE) => {
                    f(resultE, (resultF) => {
                        g(resultF, (resultG) => {
                            console.log(resultG);
                        });
                    });
                });
            });
        });
    });
});
  • 아래 코드는 콜백 지옥의 간단한 예시이다. 알파벳 소문자로 작성된 hello.txt 파일을 읽고 해당 문자열을 대문자로 변환한 후 다시 텍스트 파일로 저장하는 코드이다.
const fs = require('fs');

function convert(data, callback) {
    // 알파벳 문자열을 대문자로 변환
    const converted = data.toUpperCase();
    // 1초 후에 콜백 함수 실행
    setTimeout(() => {
        callback(converted);
    }, 1000);
}

// 텍스트 파일 읽기
fs.readFile('hello.txt', 'utf-8', (err, data) => {
    if (err) {
        console.log('Could not read file!');
    } else {
        // 대문자 변환 함수 실행
        convert(data, (result) => {
            // 변환된 문자열을 파일로 저장
            fs.writeFile('output.txt', result, (err) => {
                if (err) {
                    console.log('Could not write file!');
                } else {
                    console.log('File successfully written');
                }
            });
        });
    }
});

💡 콜백 지옥 탈출을 위한 promise

  • 콜백 지옥을 탈출하기 위한 첫 번째 방법으로 promise의 사용이 있다.
    promise는 어떤 연산에 대한 결과값을 반환하는데 이 결과값은 최종 결과 값이 아니고, 미래의 어떤 시점에 결과를 제공하겠다는 '약속'(프로미스)를 반환한다.
  • promise를 지원하지 않는 함수에서 promise를 사용하기 위해서는 먼저 new Promise 생성자를 이용하여 새로운 promise객체를 생성하고 resolve와 reject를 매개변수로 받는 함수를 작성한다.
    이 때 resolve는 주어진 값으로 이행되는 promise 객체를 반환하며 then 메서드에서 최종 상태를 결정한다.
    reject의 경우 주어진 사유로 인해 거부하는 promise 객체를 반환하며 catch 메서드에서 거부된 결과에 대한 후 처리 작업을 진행한다.
  • 위의 예시를 promise를 이용하여 리팩토링 하기 위해서는 우선 convert함수, 파일을 읽는 함수, 파일을 저장하는 함수를 promise화 해야한다. 각각의 함수를 promise화한 결과는 아래와 같다.
const fs = require('fs');

function convert(data, callback) {
    return new Promise((resolve) => {
        // 알파벳 문자열을 대문자로 변환
        const converted = data.toUpperCase();
        // 1초 후에 콜백 함수 실행
        setTimeout(() => {
            resolve(converted);
        }, 1000);
    });
}

// 텍스트 파일 읽기
function readFilePromise(fileName) {
    return new Promise((resolve, reject) => {
        fs.readFile(fileName, 'utf-8', (err, data) => {
            if (err) reject('Could not read file!');
            else resolve(data);
        });
    });
}

// 텍스트 파일 쓰기
function writeFilePromise(fileName, data) {
    return new Promise((resolve, reject) => {
        fs.writeFile(fileName, data, (err) => {
            if (err) reject('Could not write file!');
            else resolve(data);
        });
    });
}
  • promise화 된 함수를 바탕으로 코드를 리팩토링하게 되면 다음과 같이 코드가 간결해지고 가독성이 높아지게 된다.
    함수가 추가되더라도 들여쓰기의 깊이는 더 이상 깊어지지 않고 마치 열차처럼 연결된 형태로 확장이 가능해진다.
// Promise를 이용한 코드 리팩토링
readFilePromise('hello.txt')
    .then((data) => convert(data))
    .then((converted) => writeFilePromise('output.txt', converted))
    .then(() => console.log('File successfully written'))
    .catch((err) => console.log(err));

💡 콜백 지옥 탈출을 위한 async / await

  • JavaScript ES8버전부터는 promise를 이용한 비동기 코드를 더 쉽게 작성하고 관리할 수 있도록 도와주는 async와 await의 사용이 가능해졌다.
  • 함수 앞에 async를 선언하여 async함수를 정의할 수 있으며 해당 함수는 결과값으로 항상 promise를 반환한다.
  • await 키워드는 async 함수 내에서만 사용이 가능하며 await 뒤에오는 promise가 처리될 때까지 현재 함수의 실행을 일시 중지하며 promise의 결과값을 기다린다.
  • promise가 처리되면 해당 promise의 결과값을 반환받을 수 있다.
  • promise의 then, catch 대신 try catch 구문으로 함수의 이행과 오류를 구분하여 처리할 수 있고 promise의 then 체인을 사용하지 않아도 보다 직관적인 비동기 코드 작성이 가능하다.
// async&await를 이용한 코드 리팩토링
(async () => {
    try {
        const data = await readFilePromise('hello.txt');
        const converted = await convert(data);
        await writeFilePromise('output.txt', converted);
        console.log('File successfully written');
    } catch (err) {
        console.log(err);
    }
})();

참고