개발

비동기와 API

sjindev 2025. 7. 9. 19:49

 

오늘은 비동기와 API 에 대해서 알아볼 예정입니다.

위 그림은 동기 / 비동기 를 이해하기 가장 유용한 것 같아 첨부하였습니다.

동기 예시는 아래와 같습니다.

const workA = () => {
    console.log('workA');
};
const workB = () => {
    console.log('workB');
};
const workC = () => {
    console.log('workC');
};

workA();
workB();
workC();

위와 같이 작성한 코드는 호출 순서인 workA(), workB(), workC() 순으로 수행됩니다. 

 

이번에는 자바 스크립트에서 사용하는 비동기 내장함수 setTimeout() 을 수행하여 비동기의 동작 흐름을 알아보겠습니다.

const workA = () => {
    setTimeout(() => {
        console.log('workA');
    }, 5000);
};
const workB = () => {
    setTimeout(() => {
        console.log('workB');
    }, 3000);
};
const workC = () => {
    setTimeout(() => {
        console.log('workC');
    }, 10000);
};

workA();
workB();
workC();

이번에는 setTimeout() 에 의해 콘솔창에 workB -> workA -> workC 순으로 출력되는 것을 확인할 수 있습니다.

 

그렇다면 동기와 비동기 작업이 섞여 있는 경우에는 어떻게 작동할까요?

const workA = () => {
    setTimeout(() => {
        console.log('workA');
    }, 5000);
};
const workB = () => {
    setTimeout(() => {
        console.log('workB');
    }, 3000);
};
const workC = () => {
    setTimeout(() => {
        console.log('workC');
    }, 10000);
};
const workD = () => {
    console.log('workD');
};

workA();
workB();
workC();
workD();

코드를 실행하면 가장 마지막에 호출한 workD가 제일 먼저 출력이 되고, 그 다음은 동일하게 workB, workA, workC의 순서로 출력이 되는 것을 볼 수 있습니다. workD는 동기로 처리한 함수이기 때문에 다른 함수들과 상관 없이 가장 먼저 출력되고, 이후에 비동기 함수들이 순서대로 출력됩니다.

[promis 객체]

자바스크립트에는 작업을 비동기로 처리할 때 사용하는, 프로미스(Promise)라는 객체가 있습니다.

const executor = (resolve, reject) => {
    //함수 내용
};

const promise = new Promise(executor);
console.log(promise); // Promise{<pending>}

프로미스 객체는, 객체 생성 시 인수로 executor라는 실행 함수를 전달하고, 실행 함수에는 매개변수로 resolvereject라는 콜백 함수를 전달합니다. 프로미스 객체가 생성됨과 동시에 executor 가 실행됩니다.

비동기는 이전 작업의 처리가 완료될 때까지 기다리지 않고, 다음 작업을 병렬적으로 처리하기 때문에, 항상 작업 처리의 성공 여부에 따라 함수를 다르게 호출해야 합니다.

 

executor는 작업 처리 성공시 resolve, 실패시 reject 를 호출합니다.  

 

아래는 promis 객체를 사용하는 예시 코드입니다.

const executor = (resolve, reject) => {
    setTimeout(() => {
        resolve('성공');
        reject('실패');
    }, 3000);
};

const promise = new Promise(executor);
promise.then(
    (result) => {
        console.log(result);
    },
    (error) => {
        console.log(error);
    }
);

resolve(), reject() 함수는 각각 promis.then() 안에서 함수의 내용이 정해진다. 일단, resolve() 함수는 위에서 '성공' 이라는 인수를 담고 있다. 이는 (result) => { console.log(result); } 로 구체화되었다고 생각할 수 있다.

위 코드는 then/catch 메서드를 사용해서 코드를 조금 더 직관적으로 작성할 수 있습니다.

const executor = (resolve, reject) => {
    setTimeout(() => {
        resolve('성공');
        reject('실패');
    }, 3000);
};

const promise = new Promise(executor);
promise
    .then((result) => {
        console.log(result);
    })
    .catch((error) => {
        console.log(error);
    });

작업이 실패했을 때, .catch() 문으로 들어가 함수를 수행합니다.

[콜백지옥]

 

아래 코드의 실행결과는 15, 12, 22 가 순서대로 출력되는 것이다.

const workA = (val) => {
    const promise = new Promise((resolve) => {
        setTimeout(() => {
            resolve(val + 5);
        }, 5000);
    });
    return promise; 
}

const workB = (val) => {
    const promise = new Promise((resolve) => {
        setTimeout(() => {
            resolve(val - 3);
        }, 3000);
    });
    return promise; 
}

const workC = (val) => {
    const promise = new Promise((resolve) => {
        setTimeout(() => {
            resolve(val + 10);
        }, 10000);
    });
    return promise; 
}


workA(10).then((resA) => {
    console.log(`workA : ${resA}`);
    workB(resA).then((resB)=> {
        console.log(`workB : ${resB}`);
        workC(resB).then((resC) => {
            console.log(`workC : ${resC}`);
        })
    })
})

하지만 workA(10) 부터 시작하여 콜백함수를 타고 타고 수행하는 절차가 다소 복잡하고 가독성이 안좋게 느껴진다.

이를 promis-then 을 이용하여 좀 더 간결하게 리팩토링하면 아래와 같다.

const workA = (val) => {
    const promise = new Promise((resolve) => {
        setTimeout(() => {
            resolve(val + 5);
        }, 5000);
    });
    return promise; 
}

const workB = (val) => {
    const promise = new Promise((resolve) => {
        setTimeout(() => {
            resolve(val - 3);
        }, 3000);
    });
    return promise; 
}

const workC = (val) => {
    const promise = new Promise((resolve) => {
        setTimeout(() => {
            resolve(val + 10);
        }, 10000);
    });
    return promise; 
}

workA(10)
        .then((resA) => {
            console.log(`1. ${resA}`);
            return workB(resA);
        })
        .then((resB) => {
            console.log(`2. ${resB}`);
            return workB(resB);
        })
        .then((resC) => {
            console.log(`3. ${resC}`);
            return workB(resC);
        })

훨씬 더 가독성 좋은 코드를 얻을 수 있다.

 

또하나의 예시를 살펴보겠습니다.

const delay = (ms) => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('3초가 지났습니다.');
        }, ms);
    });
};

const start = () => {
    delay(3000).then((res) => {
        console.log(res);
    });
};
start();

위 코드는 delay()함수의 인자로 3000ms 를 주고, Promise 함수는 정상적으로 수행됐을 때  resolve 라는 함수를 수행합니다. 이때 resolve() 함수의 인자인 '3초가 지났습니다.' 는  res 라는 인자로 치환되고, 이는 console.log() 함수의 인자로 들어가기 때문에  결론적으로 start() 함수 수행 시 3000ms 뒤,   '3초가 지났습니다.'가 콘솔에 출력됩니다.

위 코드를 async와 await 를 활용하여 더 직관적으로 바꿔보겠습니다.

여기서 중요!! 어떤함수에 async 키워드를 붙이면 이는 자동으로 함수가 Promise 객체를 return 하게 됩니다!

awaitasync 키워드가 작성된 함수의 내부에서 사용하는 키워드이며, await 키워드가 포함된 코드가 실행되면, 해당 작업이 종료될 때까지 프로그램의 실행이 중단됩니다.(= Promise 가 처리될때까지 기다린다.)

 

 async만 사용한 코드. 아직 then을 사용해야 함을 확인할 수 있다.

// async민 사용한 코드. 아직 then을 사용해야 함을 확인할 수 있다.
const delay = (ms) => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('3초가 지났습니다.');
        }, ms);
    });
};

const start = async () => {
    delay(3000).then((res) => {
        console.log(res);
    });
};

start();

 

async와 그 안에서 await 키워드를 사용. then을 사용하지 않아도 되며 가독성이 좋다.

// async와 그 안에서 await 키워드를 사용. then을 사용하지 않아도 되며 가독성이 좋다.
const delay = (ms) => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('3초가 지났습니다.');
        }, ms);
    });
};

const start = async () => {
    let result = await delay(3000);
    console.log(result);
};
start();

 

Promise 객체를 사용하고, async  / await 을 사용하면, 호출된 순서대로 작업을 수행할 수 있게된다!(예측한대로 동작 가능)

단 주의할 점은, async 키워드가 없는 함수에서 await 키워드 사용시 Syntax error 가 발생한다!

 

async 함수에서는 에러처리를 try-catch로 핸들링할 수 있다!

 

여기서 중요한 사실

const workA = () => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('workA');
        }, 5000);
    });
};
const workB = () => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('workB');
        }, 3000);
    });
};
const workC = () => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('workC');
        }, 10000);
    });
};

const start = async () => {
    try {
        let resultA = await workA();
        let resultB = await workB();
        let resultC = await workC();
        console.log(resultA);
        console.log(resultB);
        console.log(resultC);
    } catch (err) {
        console.log(err);
    }
};

start();

위 코드는 비동기 작업임에도 await 키워드에 의하여 호출 순서대로 동작한다. 이때, await 는 동작이 완료될때 까지 다른 동작의 수행을 멈추게 된다. 따라서 비동기 작업임에도 시간이 오래걸리게(동기작업 처럼 수) 된다. (이 코드에서는 총 18초가 소요된다.)

[API 란?]

클라이언트 - 서버 - DB 는 위와 같은 관계를 갖으며 각 화살표방향대로 어떤 작업을 하는지 잘 알아두자. 

 

클라이언트와 서버의 통신은 클라이언트가 서버에게 데이터를 요청하면 서버가 데이터베이스에서 요청받은 데이터를 찾고 꺼내 다시 클라이언트에게 알맞은 데이터를 전달하는 과정  위 과정이 우리가 주목해야할 과정이며, 프론트엔드 개발자의 필수 역량이 되는 부분이라고 할 수 있다.

 

API 호출은 JSON 형식을 이용하여 서버로부터 데이터를 받아온다. 그전에, API를 호출하기 위해서는 가장 먼저 API 호출에 응답할 수 있는 서버가 필요하다. 하지만, 편하게 연습할 때는 아래 사이트를 이용해보자.

JSONPlaceholder - Free Fake REST API

 

JSONPlaceholder - Free Fake REST API

{JSON} Placeholder Free fake and reliable API for testing and prototyping. Powered by JSON Server + LowDB. Serving ~3 billion requests each month.

jsonplaceholder.typicode.com

 

아래와 같이 시작할땐 fetch() 함수 안에 우리가 접근하려는 API주소를 적어준다.

let response = fetch('https://jsonplaceholder.typicode.com/users');
console.log(response);

또한 불러온 값을 response 라는 이름의 변수에 할당해준다.

이를 콘솔에 확인해보면, 아래와 같다.

state 프로퍼티가 fulfilled 인 Promise 객체가 반환됨을 알 수 있다.

Promise 객체가 반환된다고?? -> 비동기 처리 함수. 이는 then() 메서드를 이용하여 결과값을 출력할 수 있다는 뜻.

let response = fetch('https://jsonplaceholder.typicode.com/users')
                .then((res) => console.log(res))
                .catch((err) => console.log(err));
console.log(response);

위 코드와 같이 작성 가능하다.

 

지금까지의 과정을 가독성 좋게 리팩토링(+ json 형식으로 parsing)하면 아래와 같다.

const getData = async () => {
    let response = await fetch('https://jsonplaceholder.typicode.com/users');
    let data = response.json();
    console.log(data);
};

getData();

 

마지막으로 생각해봐야 할 것은 API 호출은 비동기 작업이기 때문에 여러가지 이유로 작업이 실패할 수 있다.(ex: API주소 오류 등)

이를 위해 코드를 작성할 때, 호출부를 try-catch 문으로 작성하여 실패시 에러를 확인할 수 있다. 예시코드는 아래와 같다.

const getData = async () => {
    try {
        let response = await fetch('https://jsonplaceholder.typicode.com/users');
        let data = response.json();
        console.log(data);
    }
    catch(err) {
         console.log(err);
    }
};

getData();