728x90
728x90
자바스크립트 비동기 프로그래밍(Asynchronous Programming)
들어가며
- 자바스크립트의 비동기 프로그래밍(Asynchronous Programming)에 대해 공부했던 내용을 정리해본다.
비동기 프로그래밍(Asynchronous Programming)
개념
- 자바스크립트에서 서버와 통신을 하다 보면 어떤 자료를 요청하고 받는지에 따라, 또는 네트워크 속도에 따라 조금씩 처리 시간이 달라진다.
- 그리고 시간 차이가 나는 처리 결과를 받아서 순서대로 처리해야 하는데, 이러한 처리 방식을 '비동기 처리 방식'이라고 한다.
- 자바스크립트 프로그램은 많은 함수들이 모여서 하나의 기능을 만든다.
- 그런데 이들 함수의 실행 시간이 서로 다르므로 특정 작업이 끝나면 다른 작업을 하고, 그 작업이 끝나면 이어서 또 다른 작업을 하도록 서로 연결해 주어야 한다.
- 이때 이전 작업이 끝날 때까지 기다렸다가 다음 작업을 하든지, 또는 이전 작업을 시작해 놓고 다음 작업도 동시에 하는지에 따라 동기(Synchronous)와 비동기(Asynchronous)로 나뉜다.
동기 처리 방식(Synchronous Processing)
개념
- 기본적으로 자바스크립트 프로그램은 코드가 작성된 순서대로 처리한다. (동기 처리 방식)
- 동기 처리 방식은 '단일 스레드 방식', '싱글 스레드 방식'이라고 불린다.
- 동기 처리 방식은 대기줄이 길어지면 처리 시간이 길어진다는 단점이 있다.
동기 처리 예
function displayA() {
console.log("A");
}
function displayB() {
console.log("B");
}
function displayC() {
console.log("C");
}
displayA();
displayB();
displayC();
// 콘솔창에 A -> B -> C 순으로 표시된다.
- 자바스크립트는 싱글 스레드를 사용하지만, 시간이 많이 걸리는 작업은 따로 처리해서 싱글 스레드의 단점을 보완한다.
function displayA() {
console.log("A");
}
function displayB() {
setTimeout(() => console.log("B"), 2000); // 2초 뒤에 실행
}
function displayC() {
console.log("C");
}
displayA();
displayB();
displayC();
// A -> C -> B 순으로 출력된다. (함수의 실행 시간에 따라 오래 걸리는 것은 별도로 처리하고, 실행이 끝났을 때 결과를 반환한다.)
// 자바스크립트는 싱글 스레드를 사용하지만, 시간이 많이 걸리는 작업은 따로 처리해서 싱글 스레드의 단점을 보완한다.
비동기 처리 방식(Asynchronous Processing)
개념
- 프로그램에서는 여러 개의 함수를 작성하는데, 실행 시간이 다른 함수들을 원하는 처리 순서에 맞게 프로그래밍하는 것을 '비동기 처리'라고 한다.
- 서버에서 자료를 가져와서 화면에 표시한다면 서버에서 자료를 가져올 때 아무리 많은 시간이 걸려도 자료를 가져오는 함수 다음에 화면에 표시하는 함수를 실행해야 한다.
비동기 처리 방식
- 자바스크립트에서 비동기 방식으로 처리하면 다음과 같이 크게 3가지 방법을 사용할 수 있다.
비동기 방식 | 버전 | 기능 |
콜백 함수 (Callback Function) |
기존부터 사용 | 함수 안에 또 다른 함수를 매개변수로 넘겨서 실행 순서 제어 (콜백 함수가 많으면 가독성이 떨어짐.) |
프로미스 (Promise) |
ECMAScript2015(ES6)부터 | @Promise@ 객체와 콜백 함수를 사용해서 실행 순서 제어 |
async, await | ECMAScript2017(ES8)부터 | @async@ 함수와 @await@ 예약어를 사용해서 실행 순서 제어 |
비동기 처리 예 ① : 콜백 함수(Callback Function)
- 콜백 함수(Callback Function)란 다른 함수의 매개변수로 사용하는 함수를 의미한다.
- 자바스크립트는 오래 전부터 콜백 함수를 사용해서 비동기 처리를 구현해왔다.
function displayA() {
console.log("A");
}
function displayB(callback) {
setTimeout(() => {
console.log("B");
callback();
}, 2000); // 2초 뒤 실행
}
function displayC() {
console.log("C");
}
displayA();
displayB(displayC);
// A -> B -> C (A를 표시하고 잠시 기다렸다가 B와 C를 표시한다.)
- 다음과 같이 @setTimeout()@을 이용하여 비동기 처리를 구현할 수 있다.
function order(coffee, callback) {
// 커피 주문 (3초 기다린 후 표시)
console.log(`${coffee} 주문 접수`);
setTimeout(() => {
callback(coffee);
}, 3000); // 3초 뒤 실행
}
function display(result) {
// 커피 완료 표시
console.log(`${result} 준비 완료`);
}
order("아메리카노", display);
// 아메리카노 주문 접수
// 아메리카노 준비 완료 (3초 후)
- 하지만 다음과 같이 콜백 지옥(Callback Hell)을 맞닥뜨릴 수도 있다.
// A를 표시한 후 1초마다 B -> C -> D -> STOP! 순서로 표시하기
function displayLetter() {
console.log("A");
setTimeout(() => {
console.log("B");
setTimeout(() => {
console.log("C");
setTimeout(() => {
console.log("D");
setTimeout(() => {
console.log("STOP!");
}, 1000);
}, 1000);
}, 1000);
}, 1000);
}
- 이러한 이유로 ECMAScript2015(ES6)에서 프로미스(Promise)가 등장하였다.
비동기 처리 예 ② : 프로미스(Promise)
- 프로미스(Promise)는 처리에 성공했을 때 실행할 콜백 함수와 성공하지 않았을 때 실행할 콜백 함수를 미리 '약속'하는 것이다.
- 프로미스를 사용하려면 먼저 @Promise@ 객체를 만들어야 한다.
- 성공했을 때 실행할 @resolve()@ 콜백 함수와, 실패했을 때 실행할 @reject()@ 콜백 함수를 매개변수로 사용한다.
Promise 객체 만들기
- 아래의 코드는 @Promise@ 객체를 만들기만 할 뿐 실제 이 프로미스를 사용하지는 않는데, 이렇게 @Promise@ 객체를 만드는 코드를 '제작 코드(Producing Code)'라고 한다.
let likePizza = true;
const pizza = new Promise((resolve, reject) => {
if (likePizza) {
resolve("피자를 주문합니다.");
} else {
reject("피자를 주문하지 않습니다.");
}
});
likePizza가 true이면 '피자를 주문합니다.'를 resolve 함수에, false이면 '피자를 주문하지 않습니다.'를 reject 함수에 넘긴다.
Promise 객체 사용하기
- Promise 객체를 사용하는 코드를 '소비 코드(Consuming Code)'라고 한다.
- 즉, 프로미스는 '객체를 생성하는 부분'과 '프로미스를 사용하는 부분'으로 나뉜다.
- 프로미스를 실행할 때는 @then()@ 메서드와 @catch()@ 메서드, @finally()@ 메서드를 사용한다.
- @then()@ 메서드는 프로미스에서 '성공'했다는 결과를 보냈을 때 실행할 함수나 명령을 연결한다.
- @catch()@ 메서드는 프로미스에서 '실패'했다는 결과를 보냈을 때 실행할 함수나 명령을 연결한다.
- @finally()@ 메서드는 프로미스에서 '성공'/'실패'했다는 결과와 상관 없이 실행할 함수나 명령을 연결한다.
let likePizza = true;
const pizza = new Promise((resolve, reject) => {
if (likePizza) {
resolve("피자를 주문합니다."); // v
} else {
reject("피자를 주문하지 않습니다.");
}
});
pizza
.then((result) => console.log(result)) // v, 프로미스에서 성공 결과를 보냈을 떄 처리 (세미콜론을 붙이지 않는다.)
.catch((err) => console.log(err)); // 프로미스에서 실패 결과를 보냈을 때 처리
let likePizza = false;
const pizza = new Promise((resolve, reject) => {
if (likePizza) {
resolve("피자를 주문합니다.");
} else {
reject("피자를 주문하지 않습니다."); // v
}
});
pizza
.then((result) => console.log(result)) // 프로미스에서 성공 결과를 보냈을 떄 처리
.catch((err) => console.log(err)); // v, 프로미스에서 실패 결과를 보냈을 때 처리
let likePizza = true;
const pizza = new Promise((resolve, reject) => {
if (likePizza) {
resolve("피자를 주문합니다."); // v
} else {
reject("피자를 주문하지 않습니다.");
}
});
pizza
.then((result) => console.log(result)) // v, 프로미스에서 성공 결과를 보냈을 떄 처리
.catch((err) => console.log(err)) // 프로미스에서 실패 결과를 보냈을 때 처리
.finally(() => console.log("완료")); // v, 프로미스에서 성공/실패 결과를 보냈을 때와 상관 없이 처리
result와 err의 변수 이름은 임의로 지정해도 된다.
Promise의 상태
- 프로미스는 @resolve()@ 함수나 @reject()@ 함수를 매개변수로 받아서 실행하는 객체이다.
- 프로미스 객체는 자신의 상태를 저장했다가 @resolve()@ 함수나 @reject()@ 함수를 실행하면 상태를 바꾼다.
- 그리고 다음의 3단계 상태로 진행된다.
상태 | 설명 |
pending | 처음 프로미스를 만들면 대기 상태(pending)가 된다. |
fulfilled | 처리에 성공하면 이행 상태(fulfilled)가 된다. |
rejected | 처리에 실패하면 거부 상태(rejected)가 된다. |
프로미스 체이닝(Promise Chaining)
- 콜백 함수로 여러 단계를 연결하면 다음과 같이 코드를 작성할 수 있다.
const step1 = (callback) => {
setTimeout(() => {
console.log("피자 도우 준비");
callback();
}, 2000);
};
const step2 = (callback) => {
setTimeout(() => {
console.log("토핑 완료");
callback();
}, 1000);
};
const step3 = (callback) => {
setTimeout(() => {
console.log("굽기 완료");
callback();
}, 2000);
};
console.log("피자를 주문합니다.");
step1(function () {
step2(function () {
step3(function () {
console.log("피자가 준비되었습니다.");
});
});
});
- 프로미스 체이닝(Promise Chaining)을 사용하여 다음과 같이 간단하게 표현할 수 있다.
- @then()@ 메서드를 사용해서 여러 개의 프로미스를 연결할 수 있다.
const pizza = () => {
return new Promise((resolve, reject) => {
resolve("피자를 주문합니다.");
});
};
const step1 = (message) => {
console.log(message);
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("피자 도우 준비");
}, 3000);
});
};
const step2 = (message) => {
console.log(message);
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("토핑 완료");
}, 1000);
});
};
const step3 = (message) => {
console.log(message);
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("굽기 완료");
}, 2000);
});
};
pizza()
.then((result) => step1(result)) // pizza()가 성공하면 step1() 실행
.then((result) => step2(result)) // step1()이 성공하면 step2() 실행
.then((result) => step3(result)) // step2()이 성공하면 step3() 실행
.then((result) => console.log(result)) // step3()이 성공하면 "굽기 완료" 표시
.then(() => {
console.log("피자가 준비되었습니다. 🍕");
});
- 위의 소비 코드를 아래와 같이 축약해서 표현할 수 있다.
pizza()
.then(step1) // pizza()가 성공하면 step1() 실행
.then(step2) // step1()이 성공하면 step2() 실행
.then(step3) // step2()이 성공하면 step3() 실행
.then(console.log) // step3()이 성공하면 "굽기 완료" 표시
.then(() => {
console.log("피자가 준비되었습니다. 🍕");
});
pizza().then((result) => step1(result)); → pizza().then(step1);
비동기 처리 예 ③ : async, await
- 프로미스 체이닝은 프로미스를 계속 연결해서 사용하므로 콜백 지옥처럼 코드가 복잡해질 수도 있다.
- 이 문제를 줄이기 위해 ECMAScript2017(ES8)부터 @async@ 함수와 @await@ 예약어가 등장했다.
async 함수
- @async@ 예약어를 붙인 함수는 프로미스(Promise) 객체를 반환한다.
async function displayHello() {
console.log("Hello");
}
displayHello();
// -> 콘솔창에 다음의 코드를 입력하면 displayHello() 함수가 프로미스를 반환하는 것을 확인할 수 있다.
displayHello(); // Promise {<fulfilled>: undefined}
- @async@ 함수를 사용한 않은 경우와 사용하는 경우를 비교해보면 다음과 같다.
function whatsYourFavorite() {
let fav = "Javascript";
return new Promise((resolve, reject) => resolve(fav));
}
function displaySubject(subject) {
return new Promise((resolve, reject) => resolve(`Hello, ${subject}`));
}
whatsYourFavorite()
.then(displaySubject) // .then(response => displaySubject(response))
.then(console.log); // .then (result => console.log(result));
async function whatsYourFavorite() {
let fav = "Javascript";
return fav;
}
async function displaySubject(subject) {
return `Hello ${subject}`;
}
whatsYourFavorite()
.then(displaySubject) //
.then(console.log); //
await 예약어
- @await@은 '이 함수가 끝날 때까지 기다려!' 라고 표시하는 것이다.
- 프로미스를 계속 연결하면서 실행할 경우 연달아 @then@을 사용한다. (프로미스 체이닝이 너무 길어지면 코드를 이해하기 어려워진다.)
- 이럴 때 @await@ 예약어를 사용하면 이전 프로미스 결과를 받아서 다음 프로미스로 연결해 주는 과정을 좀 더 쉽게 알아볼 수 있다.
- @await@ 예약어는 자바스크립트에서 비동기 코드를 실행할 때 유용한데, @await@은 @async@ 함수에서만 사용할 수 있다.
- @await@은 @async@ 함수에서만 사용할 수 있으므로, @async init()@ 함수를 따로 만든 후에는 그 안에서 @await@를 사용해 프로미스의 실행 순서를 지정하면 된다.
await은 async 함수에서만 사용할 수 있다.
async function whatsYourFavorite() {
let fav = "Javascript";
return fav;
}
async function displaySubject(subject) {
return `Hello ${subject}`;
}
async function init() {
const response = await whatsYourFavorite(); // whatsYourFavorite() 함수의 실행이 끝날 때까지 기다린 후, 결과값을 response에 저장
const result = await displaySubject(response); // response 값을 이용해 displaySubject() 함수를 실행하고, 끝나면 결과값을 result에 저장
console.log(result); // result를 표시
}
init();
fetch API
개념
- 서버에 있는 JSON 파일을 가져올 때 @XMLHttpRequest@ 객체를 사용한다.
- @XMLHttpRequest@를 통해 자료를 주고 받는 방법은 자바스크립트 초기 버전부터 지금까지 사용하고 있다.
- 모던 자바스크립트에서 @XMLHttpRequest@를 대신할 fetch API가 등장하였다.
특징
- @fetch@는 AJAX처럼 서버로 요청을 보내거나 자료를 받아오는 방법이지만, 프로미스(Promise)를 반환한다는 게 가장 중요한 차이점이다.
fetch("student1.json"); // Promise {<pending>} (프로미스 반환)
fetch("student1.json").then(console.log); // (Response 객체 반환)
// ▼ Response {type: 'basic', url: 'http://127.0.0.1:3000/Study/Day6/Practice/student1.json', redirected: false, json: ƒ, text: ƒ, …}
// json: ƒ ()
// text: ƒ ()
// body: (...)
// bodyUsed: false
// headers: Headers {}
// ok: true ✅
// redirected: false
// status: 200 ✅
// statusText: "OK"
// type: "basic"
// url: "http://127.0.0.1:3000/Study/Day6/Practice/student1.json"
// -> Response 객체에는 지정한 파일을 가져오는 데 성공했을 때 반환되는 값이 들어 있다.
// -> 자료를 성공적으로 가져왔는지 여부를 확인하려면 status 값이 200인지 또는 ok값이 true인지 체크한다.
예제 코드
@XMLHttpRequest@로 JSON 자료 가져오기
let xhr = new XMLHttpRequest();
xhr.open("GET", "student2.json");
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
let students = JSON.parse(xhr.responseText);
renderHTML(students);
}
};
function renderHTML(contents) {
let output = "";
for (let content of contents) {
output += `
<h2>${content.name}</h2>
<ul>
<li>전공 : ${content.major}</li>
<li>학년 : ${content.grade}</li>
</ul>
<hr>
`;
}
document.getElementById("result").innerHTML = output;
}
@fetch@로 JSON 자료 가져오기
- @fetch@를 사용할 경우 따로 if 문을 쓰지 않아도 된다.
- @then()@ 함수를 연결하면서 이미 자료를 성공적으로 가져왔다는 전제가 생겼기 떄문이다.
fetch("student2.json") // 1) json 파일을 읽어온다.
.then((response) => response.json()) // 2) json 파일을 객체로 변환한다.
.then((json) => {
// 3) 객체를 출력한다.
let output = "";
json.forEach((student) => {
output += `
<h2>${student.name}</h2>
<ul>
<li>전공 : ${student.major}</li>
<li>학년 : ${student.grade}</li>
</ul>
<hr>
`;
});
document.querySelector("#result").innerHTML = output;
})
.catch((error) => console.log(error)); // 4) 에러가 발생하면 에러를 출력한다.
Cheat Sheet
참고 사이트
728x90
728x90
'Programming > JavaScript' 카테고리의 다른 글
[JavaScript] for 문 정리 (for, for...in, for...of, forEach, for await...of) (0) | 2024.08.25 |
---|---|
[JavaScript] JSON(JavaScript Object Notation) 다루기 (0) | 2024.07.05 |
[JavaScript] Intl.NumberFormat 객체 (0) | 2024.06.28 |
[JavaScript] 옵셔널 체이닝 연산자(Optional Chaining Operator), null 병합 연산자(Nullish Coalescing Operator) (ES11(ECMAScript2020)) (0) | 2024.05.16 |
[JavaScript] 변수 재선언과 재할당 (var, let, const) (1) | 2024.01.18 |
[JavaScript] 스프레드 연산자(Spread Operator) (0) | 2023.12.14 |
[JavaScript] 디스트럭처링(Destructuring) (0) | 2023.12.14 |
[JavaScript] Map / Filter / Reduce / Find / FindIndex / IndexOf / Includes (0) | 2023.12.13 |