icon안동민 개발노트

비동기 프로그래밍 (콜백, Promise, async/await)


 자바스크립트에서 비동기 프로그래밍은 웹 애플리케이션의 성능과 사용자 경험을 향상시키는 핵심 개념입니다.

 이 절에서는 비동기 프로그래밍의 기본 개념부터 고급 패턴까지 상세히 알아보겠습니다.

비동기 프로그래밍의 필요성

 비동기 프로그래밍은 시간이 걸리는 작업(예 : 네트워크 요청, 파일 입출력)을 처리할 때 중요합니다.

 동기 코드는 이러한 작업을 기다리는 동안 전체 프로그램을 블록하지만, 비동기 코드는 다른 작업을 계속 처리할 수 있게 합니다.

 예시

// 동기 코드
console.log("시작");
const result = longRunningOperation(); // 이 작업이 완료될 때까지 대기
console.log(result);
console.log("끝");
 
// 비동기 코드
console.log("시작");
longRunningOperation((result) => {
    console.log(result);
});
console.log("끝"); // longRunningOperation이 완료되기 전에 실행됨

콜백 함수

 콜백은 비동기 작업이 완료된 후 실행될 함수입니다.

function fetchData(callback) {
    setTimeout(() => {
        callback("데이터");
    }, 1000);
}
 
fetchData((data) => {
    console.log(data);
});

 콜백 지옥

 복잡한 비동기 로직은 중첩된 콜백을 야기하여 "콜백 지옥"을 만들 수 있습니다.

fetchData1((data1) => {
    fetchData2(data1, (data2) => {
        fetchData3(data2, (data3) => {
            // 더 많은 중첩...
        });
    });
});

 이는 코드의 가독성과 유지보수성을 저하시킵니다.

Promise

 Promise는 비동기 작업의 최종 완료 또는 실패를 나타내는 객체입니다.

 Promise 상태

  • Pending : 초기 상태, 이행되거나 거부되지 않은 상태
  • Fulfilled : 작업이 성공적으로 완료된 상태
  • Rejected : 작업이 실패한 상태

 기본 사용법

const myPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("성공!");
        // 또는
        // reject("실패!");
    }, 1000);
});
 
myPromise
    .then((result) => {
        console.log(result); // "성공!"
    })
    .catch((error) => {
        console.error(error);
    })
    .finally(() => {
        console.log("Promise 완료");
    });

 Promise 체이닝

 Promise는 연속된 비동기 작업을 처리하기 위해 체이닝할 수 있습니다.

fetchData1()
    .then(data1 => fetchData2(data1))
    .then(data2 => fetchData3(data2))
    .then(data3 => {
        console.log(data3);
    })
    .catch(error => {
        console.error(error);
    });

 Promise 정적 메서드

  1. Promise.all() : 모든 Promise가 이행될 때까지 기다립니다.
Promise.all([fetchData1(), fetchData2(), fetchData3()])
    .then(([result1, result2, result3]) => {
        console.log(result1, result2, result3);
    });
  1. Promise.race() : 가장 먼저 이행되거나 거부되는 Promise의 결과를 반환합니다.
Promise.race([fetchData1(), fetchData2()])
    .then(result => {
        console.log("가장 빠른 결과:", result);
    });

async/await

 async/await는 Promise를 기반으로 한 더 직관적인 문법입니다.

async function fetchAllData() {
    try {
        const data1 = await fetchData1();
        const data2 = await fetchData2(data1);
        const data3 = await fetchData3(data2);
        console.log(data3);
    } catch (error) {
        console.error(error);
    }
}
 
fetchAllData();

 Promise와의 관계

  • async 함수는 항상 Promise를 반환합니다.
  • await는 Promise가 이행될 때까지 함수의 실행을 일시 중지합니다.

 장점

  • 더 동기적으로 보이는 코드
  • 에러 처리가 더 직관적 (try-catch 사용)

 단점

  • 오래된 브라우저에서는 지원되지 않을 수 있음
  • 병렬 실행에는 추가적인 처리 필요

에러 처리

 async/await에서의 에러 처리

async function fetchData() {
    try {
        const data = await fetch('https://api.example.com/data');
        const json = await data.json();
        console.log(json);
    } catch (error) {
        console.error('데이터 가져오기 실패:', error);
    }
}

실제 웹 개발 시나리오

  1. API 호출
async function getUserData(userId) {
    try {
        const response = await fetch(`https://api.example.com/users/${userId}`);
        if (!response.ok) {
            throw new Error('Network response was not ok');
        }
        const userData = await response.json();
        return userData;
    } catch (error) {
        console.error('Error fetching user data:', error);
        throw error;
    }
}
 
// 사용
getUserData(123)
    .then(user => {
        console.log('User:', user);
    })
    .catch(error => {
        console.error('Failed to get user:', error);
    });
  1. 파일 처리 (Node.js 환경)
const fs = require('fs').promises;
 
async function processFile(filePath) {
    try {
        const data = await fs.readFile(filePath, 'utf8');
        const processedData = data.toUpperCase();
        await fs.writeFile(filePath + '.processed', processedData);
        console.log('File processing complete');
    } catch (error) {
        console.error('Error processing file:', error);
    }
}
 
processFile('example.txt');

 비동기 프로그래밍은 자바스크립트의 핵심 특징 중 하나로, 특히 웹 개발에서 중요한 역할을 합니다. 비동기 코드를 통해 시간이 오래 걸리는 작업을 처리하면서도 사용자 인터페이스의 응답성을 유지할 수 있습니다.

 콜백 함수는 비동기 프로그래밍의 가장 기본적인 형태입니다. 그러나 복잡한 비동기 로직을 처리할 때 콜백 지옥이라는 문제에 직면할 수 있습니다. 이는 코드의 가독성을 떨어뜨리고 유지보수를 어렵게 만듭니다.

 Promise는 이러한 문제를 해결하기 위해 도입되었습니다. Promise는 비동기 작업의 최종 완료 또는 실패를 나타내는 객체로, 체이닝을 통해 연속된 비동기 작업을 더 깔끔하게 표현할 수 있게 해줍니다. 또한 Promise.all()과 Promise.race() 같은 정적 메서드를 통해 여러 비동기 작업을 효과적으로 관리할 수 있습니다.

 async/await는 Promise를 기반으로 한 더 현대적이고 직관적인 문법입니다. 이를 사용하면 비동기 코드를 마치 동기 코드처럼 작성할 수 있어, 가독성이 크게 향상됩니다. 또한 try-catch 구문을 사용하여 에러 처리를 더 자연스럽게 할 수 있습니다.

 실제 웹 개발에서 이러한 비동기 패턴들은 API 호출, 파일 처리, 데이터베이스 쿼리 등 다양한 시나리오에서 활용됩니다. 예를 들어, 사용자 데이터를 가져오거나 파일을 처리할 때 async/await를 사용하면 코드를 더 명확하고 관리하기 쉽게 작성할 수 있습니다.

 비동기 프로그래밍을 마스터하는 것은 효율적이고 반응성 높은 웹 애플리케이션을 개발하는 데 필수적입니다. 콜백, Promise, async/await 각각의 특징과 적절한 사용 상황을 이해하고, 이를 실제 개발에 적용할 수 있는 능력이 중요합니다. 또한 비동기 코드의 에러 처리에 특별히 주의를 기울여야 하며, 적절한 에러 핸들링 전략을 구현하는 것이 안정적인 애플리케이션 개발의 핵심입니다.