앞서 자바스크립트의 비동기 구조와 Event Loop에 대해 살펴보았다.
비동기 작업은 작업이 완료될 때까지 기다리지 않고 다음 코드를 실행한다는 특징이 있다.

그렇다면 여기서 한 가지 문제가 생긴다.

비동기 작업이 언제 끝나는지 어떻게 알 수 있을까?

예를 들어 서버에서 데이터를 가져오는 작업이 있다고 가정해보자.

fetchUserData();
console.log("다음 작업 실행");

만약 데이터를 가져오는 작업이 3초가 걸린다면,
데이터가 도착하기 전에 다음 코드가 먼저 실행될 수 있다.

이때 작업이 끝난 뒤 실행할 코드를 전달하는 방법이 필요하다.

그래서 등장한 것이 바로 콜백 함수(Callback) 이다.


콜백 함수 (Callback)

콜백 함수는 다른 함수에 인자로 전달되는 함수이다.

하지만 단순히 인자로 전달된다는 의미보다 중요한 것은 다음과 같다.

콜백 함수는 실행 시점을 다른 함수에게 맡긴다.

말이 어려우니 예제로 살펴보자.

function greet(name, callback) {
  console.log("안녕하세요 " + name);
  callback();
}

function sayGoodbye() {
  console.log("안녕히 가세요");
}

greet("홍길동", sayGoodbye);

결과

안녕하세요 홍길동
안녕히 가세요

여기서 sayGoodbye는 직접 호출되지 않았다.
대신 greet 함수 내부에서 실행된다.

즉,

일반 함수 → 내가 원할 때 실행
콜백 함수 → 다른 함수가 필요할 때 실행

이처럼 실행 제어권이 다른 함수에게 넘어가는 것이 콜백 함수의 특징이다.


콜백 지옥 (Callback Hell)

콜백 함수는 특히 비동기 처리에 유용하여 비동기 작업에 많이 사용되지만, 작업이 많아지면 문제가 발생한다.

이러한 구조를 콜백 지옥이라고 한다.
사진을 보면 알겠지만 가독성이 매우 떨어지며, 에러 처리가 어렵고, 유지보수가 힘들다는 단점이 있다.

이것을 해결하기 위해 등장한 것이 Promise다.


Promise

Promise는

비동기 작업의 성공 또는 실패 결과를 나타내는 객체
이다.

그러니까, "작업이 끝나면 결과를 알려줄게~"라는 약속이라고 이해할 수 있다.

이 Promise는 3가지의 상태를 가지는데

// 1. Pending (대기)
// - 작업이 진행 중
const promise = new Promise((resolve, reject) => {
    // 작업 중...
});

// 2. Fulfilled (이행)
// - 작업 성공
const promise = new Promise((resolve, reject) => {
    resolve('성공!');
});

// 3. Rejected (거부)
// - 작업 실패
const promise = new Promise((resolve, reject) => {
    reject('실패!');
});

Pending - 대기, Fulfilled - 성공, Rejected - 실패 이 세 상태를 기억해두자.


Promise 만들기

❗Promise를 만들 때는, 항상 이 작업이 성공할 수도 있고 실패할 수도 있다는 것을 명시해줘야한다.

성공 = reseolve
실패 = reject

const promise = new Promise((resolve, reject) => {
    // 비동기 작업
    const success = true;

    if (success) {
        resolve('성공 데이터'); // 성공
    } else {
        reject('에러 메시지');  // 실패
    }
});
  • then() 과 catch() :
const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('완료!');
    }, 1000);
});

promise
    .then(result => {
        console.log('성공:', result);
    })
    .catch(error => {
        console.log('실패:', error);
    });

// 출력 (1초 후):
// 성공: 완료!
  • finally()
    성공/실패와 상관없이 실행:
fetchUser(123)
    .then(user => {
        console.log('성공:', user);
    })
    .catch(error => {
        console.error('실패:', error);
    })
    .finally(() => {
        console.log('작업 완료 (성공이든 실패든)');
        // 로딩 스피너 숨기기 등
    });

async/await

Promise를 더 쉽게 사용하는 문법으로 가장 중요한 포인트는 비동기 함수의 동기적 표현이다.

  • Promise를 동기 코드처럼 :
// ❌ Promise (then 체인)
fetchUser(123)
    .then(user => {
        console.log(user);
        return fetchOrders(user.id);
    })
    .then(orders => {
        console.log(orders);
    });

// ✅ async/await (읽기 쉬움)
async function getUser() {
    const user = await fetchUser(123);
    console.log(user);

    const orders = await fetchOrders(user.id);
    console.log(orders);
}

async 키워드는 함수를 비동기 함수로 만들어준다.
그리고 또 하나 중요한 특징이 있다.

async 함수는 항상 Promise를 반환한다.

async function hello() {
  return "안녕";
}

이 함수는 단순히 문자열을 반환하는 것처럼 보이지만 실제로는

function hello() {
  return Promise.resolve("안녕");
}

이것과 같은 의미를 가지고 있다.
그래서 async함수는 다음과 같이 사용 가능하다.

hello().then(msg => console.log(msg));

// 출력
안녕

  • await

await 키워드는 Promise가 완료될 때까지 기다리는 역할을 한다
그러기에 반드시 async 함수 내부에서만 사용이 가능하다.


에러처리 (try / catch)

async / await에서는 에러 처리를 try / catch로 할 수 있다.
Promise의 .catch()와 같은 역할이다.

async function getUser() {
  try {
    const user = await fetchUser(123);
    console.log("사용자:", user);
  } catch (error) {
    console.error("에러 발생:", error);
  }
}

간단한 개념들로 정리해봤는데, 실제 코드에서 사용해봐야 감을 찾을 것 같다. 다음은 실습을 통해 진행해보겠다.

profile
다른 건 노력의 시간

0개의 댓글