비동기 프로그래밍은 이미 한번 정리한적이 있지만, 이번에 플젝을 하면서 새로 알게 된 사실을 추가해서 한번 더 정리해보려 한다.
나에겐 참 어려웠던 개념이다. 비동기 자체를 이해하는 것도 어려웠고, 싱글스레드에서 비동기 프로그래밍이 돌아간다는 것도 이해하기 어려웠다. JavaScript에서 비동기 프로그래밍은 정말 중요한 개념이다. 싱글스레드로 동작하는 JavaScript의 여러 한계를 극복하게 해주는 방법이기 때문이다. database나 file에 접근하거나, 네트워크 통신이 필요한 경우 등의 작업이 필요하다면, 비동기 처리를 통해서 blocking하지 않고 효과적으로 처리할 수 있다. 이는 자바스크립트기반 애플리케이션의 한계를 넘어서게 하는 필수적인 기능이라고 볼 수 있다.
나는 학습 정리를 (아직은) 잘 못하기 때문에, 동기와 비동기에 대한 멋진 설명은 이미 잘 만들어진 포스팅에 맡기고, 내가 이해한 바를 날 것 그대로 작성해 보겠다.
내가 js의 비동기를 이해하는데 가장 중요했던 개념은 "시간이 오래 걸리거나 js에서 처리 할 수 없는 일은 다른 녀석에게 맡기고, 다른일을 한다" 라는 것이다. 이를 통해, js는 싱글스레드로 동작하면서도 비동기적으로 동작할 수 있다. 나는 싱글스레드와 역할을 오해하고 있었다. js에서 할 수 없는 일, 가령 네트워크 통신이나 파일 io는 각 os에서 처리할 수 있게 추상화한 api(libuv)를 가져다가 사용한다. 이녀석이 js의 메인스레드와는 다른 스레드에서 시간이 오래걸리거나, js에서 할 수 없는 일을 대신 해준다. 그리고 그 결과만을 다시 js의 메인스레드에게 전달해준다.
그 중에 네트워크 데이터 통신에 대해 학습해보자. 네트워크 통신은 시간이 오래 걸리는 작업이다. 다들 웹 페이지에 접속했을 때 로딩이 오래 걸린 경험이 있지 않은가. 많은 원인이 있겠지만 가장 주된 원인은, 네트워크 바운드가 걸려 필요한 정보가 (아직) 없어서 화면을 렌더링 하지 못하는 것일 거다. 예전에는 모든 데이터를 한번에 받아와서(ex: home.html) 화면을 렌더링 했지만, 요즘엔 미리 그릴걸 먼저 받아오고 그 안의 데이터들은 json형식으로 비동기적으로 받아와 사용성을 개선했다.
fetch api에 대해 공부하기 전에 먼저 Promise에 대해 공부해보자. Promise는 역시 js 대규모패치였던 ES6에 추가되었고 비동기 처리를 위해 사용된다. Promise는 비동기 작업을 처리하기 위한 일종의 컨테이너(객체)다. 주로 네트워크 요청, 파일 로딩, 타이머 등과 같은 비동기 작업을 다룰 때 사용되는데, 대기(pending), 이행(fulfilled), 거부(rejected)의 상태로 결과를 반환한다. 이행 또는 거부 상태가 되면, 해당 Promise의 then() 또는 catch() 메서드가 호출된다. finally()는 작업의 성공, 실패여부에 관계 없이 항사 호출된다.
Promise의 상태
추가적으로, Promise는 메서드 체이닝을 통해 연속적인 비동기 작업을 처리할 수 있다. 이를 통해 비동기 코드를 보다 구조화하고 가독성 있게 작성할 수 있다. (callback 지옥 탈출 가능!)
const fetchData = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const randomNum = Math.random();
if (randomNum < 0.5) {
resolve("Success: " + randomNum);
} else {
reject("Failure: " + randomNum);
}
}, 1000);
});
};
fetchData()
.then((result) => {
console.log(result); // 성공한 경우
})
.catch((error) => {
console.error(error); // 실패한 경우
});
위 예제에서 fetchData() 함수는 Promise를 반환하며, 비동기 작업이 완료되면 resolve 또는 reject 함수를 호출하여 결과 또는 오류를 반환한다.
Promise의 가장 큰 특징은 비동기로직에서 콜백함수를 분리했다는 점이다. then 메서드에 의해 실행되는 콜백함수는 resolve나 reject 메서드의 실행 이후에야 실행되도록 설계 되어 있다. 다시 말해 비동기콜백이 끝날때 resolve나 reject메서드를 실행해주면, then의 콜백함수를 시행시킬 수가 있다. 따라서, Promise 패턴의 적용으로 비동기를 포함하고 있는 코드의 가독성을 좀더 좋게 할 수 있다.
function delay(millisecond) {
return new Promise((resolve, reject) => {
//setTimeout을 이용한 지연로직
});
}
const delayMillisecond = 1000;
delay(delayMillisecond)
.then((data) => {
console.log("data : ", data);
return delayMillisecond / 1000;
})
.then((second) => console.log(`${second}초가 지났습니다`));
Fetch API는 비동기 네트워크 요청을 처리하기 위해 Promise 기반으로 설계되었다. 서버에서 데이터를 가져오거나 보낼 때 웹앱이 블로킹 되지 않고 계속 실행될 수 있게 해준다.
Fetch API는 fetch() 함수를 사용하여 네트워크 요청을 시작하고, 이 함수는 Promise객체를 반환한다. 이 Promise는 네트워크 요청이 완료되면 resolve되며, Response 객체를 반환한다. Response 객체는 네트워크 요청에 대한 응답을 나타내며, 이를 통해 응답의 상태 코드, 헤더, 본문 등의 정보를 액세스할 수 있다.
fetch("https://api.example.com/data")
.then((response) => {
if (!response.ok) {
throw new Error("Network response was not ok");
}
return response.json(); // JSON 응답 해석
})
.then((data) => {
// 데이터 처리
console.log(data);
})
.catch((error) => {
// 오류 처리
console.error("Fetch error:", error);
});