공부/JavaScript

[JS] 동기와 비동기란? (Feat. 콜백, Promise, async/await)

y_flm 2025. 4. 13. 01:00
반응형

 

 

동기(Synchronous)란? : 일이 순서대로 끝날 때까지 기다리는 방식

 

라면으로 예시를 들어보자면, 라면을 먹기 위해 물을 끓이기 시작하고, 물이 다 끓으면 면을 넣고 끓인 뒤 라면을 먹는다.

코드로 표현하자면 다음과 같다.

function boilWater() {
  console.log('1. 물 끓이기 시작');
  for (let i = 0; i < 3; i++) {
    console.log('2. 물 끓이는 중 ...');
  }
  console.log('3. 물을 다 끓였어요!');
}

function makeRamen() {
  boilWater();
  console.log('4. 면 넣고 끓이기');
  console.log('5. 라면 완성!');
}

makeRamen();

코드의 흐름을 보면, makeRamen이 호출되면서 먼저 boilWater 함수가 실행된다.

boilWater 함수에서 1번이 출력된 후, 2번이 3번 출력되고 나서 3번이 출력된다.

이 과정이 끝날 때까지 다음 코드는 실행되지 않으며, 작업이 끝나면 함수를 빠져나와 그 이후의 작업인 4번과 5번이 순서대로 출력된다.

// 출력
1. 물 끓이기 시작
2. 물 끓이는 중 ...
2. 물 끓이는 중 ...
2. 물 끓이는 중 ...
3. 물을 다 끓였어요!
4. 면 넣고 끓이기
5. 라면 완성!

 

그렇다면 비동기에서의 라면은 어떻게 끓일까?

 

비동기란(Asynchronous)? : 일이 끝나길 기다리지 않고 다른 일을 먼저 처리하는 방식

 

일단 라면을 먹기 위해 물을 끓일 것이다.

하지만 이전과 다른 점은, 라면을 끓일동안 게임을 하다 올 것이다.

게임을 하다가 물이 끓으면 타이머가 울리고, 그때 면을 넣고 끓인 후 라면을 먹으면 된다.

코드로 표현하자면 다음과 같다.

function boilWaterAsync() {
    console.log("1. 물 끓이기 시작");
    setTimeout(() => {
        console.log("3. 물 끓었음");
        console.log("4. 면 넣고 끓이기");
        console.log("5. 라면 완성!");
    }, 3000);
}

function doSomethingElse() {
    console.log("2. 게임하기 🎮");
}

boilWaterAsync();
doSomethingElse();

먼저 boilWaterAsync가 호출되고 1번이 출력된다.

그 후 setTimeout으로 3초 뒤 실행하게끔 3, 4, 5번이 출력되게끔하고,

그 사이에 doSomethingElse가 호출되면서 2번이 출력된다.

그리고 3초 후 3, 4, 5번이 순서대로 출력된다.

1. 물 끓이기 시작
2. 게임하기
(3초 후 ...)
3. 물 끓었음
4. 면 넣고 끓이기
5. 라면 완성!

 

그럼 이제 비동기에서 중요한 개념들인 콜백, Promise, async/await은 뭘까?

 

콜백 함수란? : 콜백은 “나중에 실행할 함수”를 인자로 전달해서, 어떤 일이 끝난 뒤 실행되게 하는 방식
function cook(callback) {
    console.log("요리 시작");
    setTimeout(() => {
        console.log("요리 끝!");
        callback();
    }, 2000);
}

cook(() => {
    console.log("먹자! 🍽️");
});

위의 코드를 보면, cook이 먼저 호출되고 '요리 시작'이 출력된다.

그 후 2초 뒤에 '요리 끝'이 출력된 다음 아규먼트로 넘겨준 callback 함수가 실행되면서 '먹자!'가 출력된다.

 

콜백 함수에서의 주의점은 콜백 지옥(Callback Hell)을 조심해야하는데,

이는 함수 안에 함수, 또 함수 안에 함수와 같은 형태로 가독성이 떨어지고 디버깅이 어려워진다.

function login(id, callback) {
    setTimeout(() => {
        console.log("로그인 성공");
        callback(id);
    }, 1000);
}

function getUserInfo(id, callback) {
    setTimeout(() => {
        console.log(`사용자 정보 불러오기: ${id}`);
        callback({ id, name: "Alice" });
    }, 1000);
}

function getFriends(user, callback) {
    setTimeout(() => {
        console.log(`${user.name}의 친구 목록`);
        callback(["Bob", "Charlie"]);
    }, 1000);
}

// 콜백 지옥 시작
login("user123", (id) => {
    getUserInfo(id, (user) => {
        getFriends(user, (friends) => {
            console.log("친구 목록:", friends);
        });
    });
});

 

 

Promise란? : 작업이 끝났을 때 성공/실패 결과를 주겠다고 약속하는 객체

 

Promise는 .then()과 .catch()를 이용해서 결과를 처리할 수 있다.

function cook() {
    return new Promise((resolve) => {
        console.log("요리 시작");
        setTimeout(() => {
            console.log("요리 끝!");
            resolve("라면 완성 🍜");
        }, 2000);
    });
}

cook().then((result) => {
    console.log(result); // 라면 완성 🍜
});

cook이 호출되면서 '요리 시작'이 출력되고, 2초 후 '요리 끝'이 출력된다.

그 순간 resolve("라면 완성 🍜")가 호출되어 Promise가 성공(이행)되고, .then()이 그 값을 받아 '라면 완성 🍜'을 출력한다.

 

하지만 Promise를 사용하면서도 주의할 점은 Promise 체인이 길어지면 가독성이 떨어진다.

function login(id) {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log("로그인 성공");
            resolve(id);
        }, 1000);
    });
}

function getUserInfo(id) {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log(`사용자 정보 불러오기: ${id}`);
            resolve({ id, name: "Alice" });
        }, 1000);
    });
}

function getFriends(user) {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log(`${user.name}의 친구 목록`);
            resolve(["Bob", "Charlie"]);
        }, 1000);
    });
}

login("user123")
    .then(getUserInfo)
    .then(getFriends)
    .then((friends) => {
        console.log("친구 목록:", friends);
    });

 

async/await 이란? : Promise를 더 간단하고 동기 코드처럼 보이게 만드는 문법

 

택배로 예시를 들어보면, Promise 같은 경우는 택배가 오면 그때 열어보는 것이고 async/await은 택배가 올 때까지 기다렸다가 열어보는 것이라고 할 수 있다.

function cook() {
    return new Promise((resolve) => {
        console.log("요리 시작");
        setTimeout(() => {
            console.log("요리 끝!");
            resolve("라면 완성 🍜");
        }, 2000);
    });
}

async function eat() {
    const food = await cook(); // cook() 끝날 때까지 기다림
    console.log(food);
    console.log("먹자! 😋");
}

eat();

코드를 보면, 먼저 eat이 호출되고 cook이 실행된다.

cook이 전부 실행될 때 까지 기다린 후에 food가 출력되고 마지막으로 '먹자'가 출력된다.

food에는 cook에서 resolve가 호출되면서 Promise가 성공했기 때문에 '라면 완성'이 저장된다.

가독성도 좋기 때문에 많이 쓰이는 문법이다.


요약
콜백 함수 함수 안에 함수
Promise 성공/실패를 나중에 알려주는 약속
async / await Promise를 더 깔끔하게 쓰는 방식

 

반응형