IT & 웹개발

JavaScript 비동기 처리 – 콜백, 프로미스, async/await

kkwako 2025. 4. 2. 14:25

1. 개요

JavaScript는 싱글 스레드 기반의 비동기 프로그래밍 언어로, 코드 실행 흐름을 블로킹하지 않고 작업을 처리할 수 있다. 이를 통해 네트워크 요청, 파일 읽기, 타이머 실행 등 시간이 걸리는 작업을 효율적으로 수행할 수 있다.

JavaScript에서 비동기 처리를 수행하는 주요 방법으로는 콜백(Callback), 프로미스(Promise), async/await이 있다. 각각의 방식은 코드의 가독성과 유지보수성에 영향을 미치므로, 적절한 방법을 선택하는 것이 중요하다.

이 글에서는 JavaScript의 비동기 처리 개념과 콜백, 프로미스, async/await의 동작 방식 및 차이점을 자세히 살펴본다.

 

JavaScript 비동기 처리 – 콜백, 프로미스, async/await

 


2. JavaScript의 비동기 처리 개념

JavaScript는 기본적으로 단일 스레드(Single Thread) 이벤트 루프 기반으로 동작한다. 즉, 한 번에 하나의 작업만 실행할 수 있지만, 비동기 처리를 통해 여러 작업을 병렬적으로 실행하는 것처럼 보이게 할 수 있다.

(1) 동기(Synchronous) vs 비동기(Asynchronous)

  • 동기 방식(Synchronous)
    • 하나의 작업이 끝나야 다음 작업을 실행하는 방식
    • 코드가 순차적으로 실행되므로 직관적이지만, 시간이 오래 걸리는 작업이 있을 경우 전체 프로세스가 멈출 수 있음
    js
    복사편집
    console.log("작업 시작"); for (let i = 0; i < 1000000000; i++) {} // 오래 걸리는 작업 console.log("작업 완료");
  • 비동기 방식(Asynchronous)
    • 특정 작업을 백그라운드에서 실행하고, 결과가 완료되면 콜백 함수를 호출하는 방식
    • 실행 중인 작업이 완료될 때까지 다른 작업을 수행할 수 있음
    js
    복사편집
    console.log("작업 시작"); setTimeout(() => { console.log("3초 후 실행"); }, 3000); console.log("작업 완료");

위 코드에서 "작업 완료"가 "3초 후 실행"보다 먼저 출력되는 것을 확인할 수 있다.


3. 콜백(Callback) 함수

콜백 함수는 다른 함수의 인자로 전달되어 특정 작업이 완료된 후 실행되는 함수를 의미한다.

(1) 콜백 함수의 기본 개념

js
복사편집
function fetchData(callback) { setTimeout(() => { console.log("데이터 로드 완료"); callback(); }, 2000); } function processData() { console.log("데이터 처리 중..."); } fetchData(processData);

위 코드에서 fetchData() 함수가 2초 후 실행된 후, processData() 함수가 호출된다.

(2) 콜백 지옥(Callback Hell) 문제

콜백 함수는 간단한 작업에는 유용하지만, 여러 개의 비동기 작업이 중첩될 경우 코드가 복잡해지는 콜백 지옥(Callback Hell) 문제가 발생할 수 있다.

js
복사편집
getUserData(1, (user) => { getPosts(user.id, (posts) => { getComments(posts[0].id, (comments) => { console.log(comments); }); }); });

이처럼 콜백이 중첩되면서 코드의 가독성이 떨어지고 유지보수가 어려워지는 문제가 발생한다. 이를 해결하기 위해 등장한 것이 **프로미스(Promise)**이다.


4. 프로미스(Promise)

프로미스는 비동기 작업의 결과를 나타내는 객체로, 성공 또는 실패 여부를 나타내는 **3가지 상태(Pending, Fulfilled, Rejected)**를 가진다.

(1) 프로미스의 3가지 상태

  1. Pending(대기 상태): 비동기 작업이 아직 완료되지 않은 상태
  2. Fulfilled(성공 상태): 작업이 성공적으로 완료된 상태
  3. Rejected(실패 상태): 작업이 실패한 상태

(2) 프로미스의 기본 사용법

js
복사편집
const fetchData = () => { return new Promise((resolve, reject) => { setTimeout(() => { let success = true; if (success) { resolve("데이터 로드 성공"); } else { reject("데이터 로드 실패"); } }, 2000); }); }; fetchData() .then((result) => { console.log(result); }) .catch((error) => { console.error(error); });
  • resolve(value): 작업이 성공하면 then()으로 결과를 전달
  • reject(error): 작업이 실패하면 catch()로 에러를 전달

(3) 프로미스를 활용한 비동기 처리

프로미스를 사용하면 콜백 지옥을 방지할 수 있다.

js
복사편집
getUserData(1) .then((user) => getPosts(user.id)) .then((posts) => getComments(posts[0].id)) .then((comments) => console.log(comments)) .catch((error) => console.error(error));

이처럼 then() 체이닝을 사용하여 가독성을 개선하고, 에러 처리를 중앙에서 관리할 수 있다. 하지만 여전히 코드가 길어지는 문제점이 있다. 이를 해결하기 위해 등장한 것이 async/await이다.


5. async/await

async/await는 프로미스를 보다 간결하게 사용할 수 있도록 도와주는 문법으로, 비동기 코드를 동기 코드처럼 작성할 수 있다.

(1) async/await의 기본 개념

  • async 키워드를 함수 앞에 붙이면 해당 함수는 항상 프로미스를 반환
  • await 키워드는 프로미스가 완료될 때까지 기다리고 결과를 반환
js
복사편집
async function fetchData() { return "데이터 로드 성공"; } fetchData().then(console.log); // 데이터 로드 성공

(2) async/await을 활용한 비동기 처리

js
복사편집
async function getData() { try { const user = await getUserData(1); const posts = await getPosts(user.id); const comments = await getComments(posts[0].id); console.log(comments); } catch (error) { console.error(error); } }

(3) async/await vs 프로미스

비교 항목프로미스(Promise)async/await
가독성 then() 체이닝으로 코드가 길어질 수 있음 동기 코드처럼 작성 가능
에러 처리 catch() 블록에서 에러 처리 try-catch 블록으로 간단하게 처리 가능
코드 구조 비동기 흐름을 추적하기 어려움 가독성이 높고 유지보수가 쉬움

6. 결론 – 어떤 방식을 사용할까?

JavaScript에서 콜백, 프로미스, async/await은 각각 장단점이 있으며, 프로젝트의 요구사항에 맞게 선택해야 한다.

  • 콜백(Callback): 간단한 비동기 작업에는 적합하지만, 콜백 지옥 문제 발생 가능
  • 프로미스(Promise): 비동기 작업을 체이닝할 수 있지만, 여전히 코드가 길어질 수 있음
  • async/await: 가장 직관적이고 유지보수하기 쉬운 방식

현대 JavaScript 개발에서는 async/await을 기본적으로 사용하며, 예외 처리는 try-catch 문으로 관리하는 것이 일반적이다.