01. 비동기 처리란?
특정 코드의 연산이 끝날 때까지 코드의 실행을 멈추지 않고 다음 코드를 먼저 실행하는 자바스크립트의 특성입니다. 비동기처리의 이유는 클라이언트가 서버로 데이터를 요청 시 , 서버가 언제 그 요청에 대한 응답을 줄지 모르는데 마냥 다른 코드를 실행하지 않고 기다릴 순 없기 때문입니다. 만약 동기 처리로 실행하게 된다면 웹을 실행하는데 수십분이 걸리는 대참사가 일어나게 됩니다.
//편의점에 들어가서 음료수를 사고 나오는 상황
function goMart() {
console.log('마트에 가서 어떤 음료를 살지 고민한다.');
}
function pickDrink() {
setTimeout(function () {
console.log('고민 끝!');
product = '제로콜라';
price = 2000;
}),
3000;
}
function pay(product, price) {
console.log(`상품명 :${product}, 가격 : ${price}`);
}
let product;
let price;
goMart();
pickDrink();
pay(product, price);
//마트에 가서 어떤 음료를 살지 고민한다.
//상품명 :undefined, 가격 : undefined
//고민 끝!
다음 js 파일을 실행하면 product와 price가 undefined가 뜹니다. 이는 pickDrink함수안에서 setTimeout함수가 3초뒤에 실행되기 때문에 그 시간동안 pay함수를 먼저 처리하기 때문에 product와 price의 변수에 값이 할당되지 않은채로 출력되게 됩니다. 이러한 비동기 코드를 처리하기 위해서는 callback 함수를 활용하거나 promise나 async await를 이용하면됩니다.
01-1. callback 함수
JavaScript는 함수를 매개변수로 받고 다른 함수를 통해 반환될 수 있는데, 매개변수(인자)로 대입되는 함수를 콜백함수라고 합니다. 즉, 다른 함수가 실행을 끝낸 뒤 실행되는 함수입니다. 콜백함수를 이용하면 비동기 방식으로 작성된 함수를 동기 처리할 수 있습니다. 위에 비동기 처리된 코드를 동기 처리할 수 있도록 callback 함수를 이용해서 코드를 다시 작성하면 아래와 같이 작성할 수 있습니다.
//비동기처리
//1.callback
function goMart() {
console.log('마트에 가서 어떤 음료를 살지 고민한다...');
}
function pickDrink(call) {
setTimeout(function () {
console.log('고민 끝!');
product = '제로 콜라';
price = 2000;
call(product, price); //*매개변수로 넘긴 콜백함수 실행
}, 3000);
}
let product;
let price;
goMart();
pickDrink(function pay(product, price) {
console.log(`상품명 :${product}, 가격 : ${price}`);
});
//마트에 가서 어떤 음료를 살지 고민한다...
//고민 끝!
//상품명 :제로 콜라, 가격 : 2000
동기처리가 잘 되어 출력되는 모습입니다. 하지만 함수이 매개변수로 넘겨지는 콜백함수가 반복되어 코드가 깊어지면 가독성이 떨어지고 코드 수정 난이도가 높아져 유지보수가 어렵습니다. 이를 콜백지옥이라고 합니다..
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h1>콜백 지옥 예시</h1>
<script>
//1. 1초 뒤에 body 태그의 색상을 빨간색으로 변경
function a() {
const body = document.querySelector('body');
setTimeout(function () {
body.style.backgroundColor = 'red';
}, 1000);
}
a();
//2. 1초 뒤에 body 태그의 색상을 빨 -> 1초뒤 2주
function b() {
const body = document.querySelector('body');
body.style.backgroundColor = 'red';
setTimeout(function () {
const body = document.querySelector('body');
body.style.backgroundColor = 'orange';
setTimeout(function () {
const body = document.querySelector('body');
body.style.backgroundColor = 'yellow';
}, 1000);
setTimeout(function () {
const body = document.querySelector('body');
body.style.backgroundColor = 'green;';
setTimeout(function () {
const body = document.querySelector('body');
body.style.backgroundColor = 'blue';
}, 1000);
}, 1000);
}, 1000);
}
a();
b();
</script>
</body>
</html>
callback 지옥의 예시로 만든 코드입니당
01-2. Promise
promise 객체는 ES6에서 추가된 JS 문법으로 비동기 함수를 동기 처리하기 위해 만들어진 객체로 성공과 실패를 분리하여 반환합니다. 비동기 작업이 완료된 이후에 다음 작업을 연결시켜 진행할 수 있는 기능을 가집니다.
사용은 new Promise로 만들어서 사용하고 new Promise가 만들어질 때, executor(실행함수)가 자동으로 실행됩니다. executor의 인수로는 reject와 resolve가 있습니다. Promise가 생성되면 작업을 실행하고, 작업의 완료 여부를 executor의 매개변수를 통해서 전달합니다. executor의 매개변수인 resolve(value)는 비동기 작업이 성공했을때 그 결과를 value와 함께 호출하고 reject(error)는 비동기 작업이 실패했을때 에러 객체를 나타내는 error와 함께 호출되는 callback 함수입니다.
promise의 상태는
- pending(대기) -> Promise를 수행중인 상태
- Fulfilled(이행) -> Promise가 Resolve 된 상태 (성공)
- Rejected(거부) -> Promise가 지켜지지 못한 상태. 즉, Reject 된 상태(실패)
- Settled -> fulfilled 혹은 rejected로 결론이 난상태
가 있습니다.
성공, 실패에 대한 결과는 then 메서드 , catch 메서드로 이어 받아서 처리가 가능합니다.
//1. promise 를 생성하는 코드
function promise1(flag) {
return new Promise(function (resolve, reject) {
if (flag) {
resolve(
`현재 프로미스의 상태는 fulfilled(이행)! then 메서드로 연결~이때의 flag의 값은 ${flag}입니다.`
);
} else {
reject(
` 현재 프로미스의 상태는 rejected(거절)! catch 메서드로 연결~ 이때의 flag의 값은 ${flag}입니다.`
);
}
});
}
//1. promise를 생성하는 코드 입니다.
//2. promise를 사용 (소비)하는 코드
promise1(5 > 3)
.then(function (result) {
console.log(result);
})
.catch(function (err) {
console.log(err);
});
//현재 프로미스의 상태는 fulfilled(이행)! then 메서드로 연결~이때의 flag의 값은 true입니다.
//화살표 함수로도 가능!
promise1(5 > 3)
.then((result) => {
console.log(result);
})
.catch((err) => {
console.log(err);
});
//현재 프로미스의 상태는 fulfilled(이행)! then 메서드로 연결~이때의 flag의 값은 true입니다.
//2. promise를 사용하는 코드 입니다. promise1의 매개변수가 성공이라면 then 메서드로 연결되어 resolve함수의 value인 [현재 프로미스의 상태는 fulfilled(이행)! then 메서드로 연결~이때의 flag의 값은 true입니다.]가 result 매개변수로 받아와 출력됩니다.
promise 또 하나의 예제로
//프로미스 예제 2
function goMart() {
console.log('마트에 가서 어떤 음료를 살지 고민한다..');
}
function pickDrink(callback) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('고민 끝!');
product = '제로 콜라';
price = 5000;
if (price <= 2000) {
resolve(1);
} else {
reject(2);
}
}, 3000);
});
}
function pay() {
console.log(`상품명 : ${product}, 가격 : ${price} `);
}
function nopay() {
console.log('금액 부족 이잉');
}
let product;
let price;
goMart();
//resolve 값
pickDrink()
.then((result) => {
console.log(result);
})
.catch((result) => {
console.log(result);
});
resolve 상태라면 resolve의 value인 1이 result 매개변수에 할당되어 1을 출력하고 reject 상태라면 reject의 value인 2가 result 매개변수에 할당되어 2 를 출력합니다. 위와 같은 코드는 reject의 값인 2가 출력됩니다. 또 다른 방식으로 작성한다면
pickDrink().then(pay).catch(nopay);
//고민 끝!
//금액 부족 이잉
//==
pickDrink()
.then(function () {
pay();
})
.catch(function () {
nopay();
});
//고민 끝!
//금액 부족 이잉
다음과 같이 실행코드 작성시 resolve상태라면 pay함수가 실행되고, reject 상태라면 nopay함수가 실행됩니다.
01-3. 프로미스 체이닝
우선, 함수를 이용해서 (4+3) * 2 - 1 = 13을 연산해보겠습니다. sub(mul(add(4,3),2),1) 구조로 add 함수가 실행되고 mul함수가 실행되고 sub함수가 실행되면 되겠죠죠죠. 우선, 이 연산을 callback함수로 작성해보겠습니다.
//case1. 콜백함수로 처리한다면?
function add(n1, n2, callback) {
setTimeout(function () {
const result = n1 + n2;
callback(result); //callback(7)
}, 1000);
}
function mul(n, callback) {
setTimeout(function () {
const result = n * 2;
callback(result); //callback(14)
}, 700);
}
function sub(n, callback) {
setTimeout(function () {
const result = n - 1;
callback(result); //callback(13)
}, 500);
}
add(4, 3, function (x) {
console.log('1 : ', x);
mul(x, function (y) {
console.log('2 : ', y);
sub(y, function (z) {
console.log('3 : ', z);
});
});
});
//1 : 7
//2 : 14
//3 : 13
add함수가 먼저 실행되어 1초뒤에 3과4를 더한 값을 변수 result에 할당하고 이 result값(=7)을 출력후 add의 콜백함수인 mul에게 이 result 값을 인자로 넘겨줍니다. mul함수는 7에 2 를곱한 result 값(=14)를 출력 후 다음 콜백함수인 sub의 인자로 넘겨줍니다. sub함수는 14에서 1을 뺀 result 값을 출력하고 끝이 나게 됩니다.
프로미스 체이닝을 사용한 경우를 보겠습니다.
//case2. promise 로 처리
function add(n1, n2) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
const result = n1 + n2;
resolve(result);
}, 1000);
});
}
function mul(n) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
const result = n * 2;
resolve(result);
}, 700);
});
}
function sub(n) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
const result = n - 1;
// resolve(result);
reject(new Error('의도적으로 에러 일으켜봄 ! '));
}, 500);
});
}
//실행결과
add(4, 3)
.then(function (result) {
//성공값을 then으로 받아옴
console.log('1 : ', result); //7
//메서드 체이닝에선 return을 해야함! -> return mul(7)
return mul(result);
})
.then(function (result) {
console.log('2 : ', result); //14
return sub(result); //return sub(14)
})
.then(function (result) {
console.log('3 : ', result);
})
.catch(function (err) {
console.log('실패!');
console.log(err);
});
//1 : 7
//2 : 14
//실패!
//Error: 의도적으로 에러 일으켜봄 !
promise를 사용하기 위해서는 함수 내 먼저 new로 Promise 객체를 생성합니다. add 함수부터 살펴보면 1초 뒤에 add인자들을 더한 후 result 변수에 할당합니다. 이 promise 객체가 성공하면 resolve 함수는 나중에 then메서드를 실행합니다. 다음 mul함수나 sub 함수도 마찬가지로 작성 후 먼저 실행될 함수를 매개변수와 함께 써주고 성공시 처리할 함수들을 .(연산자)를 이용하여 then 메서드와 체이닝 해줍니다. 코드로 보자면 add함수가 성공시, then 메서드로 연결되어 add함수에서 더한 result값(=7)을 출력하고 이 result값을 인자로 받는 mul함수를 리턴합니다. 이 mul함수는 자신의 매개변수에 2를 곱하고 result 변수에 값을 할당합니다. 이와 같은 작업이 오류 없이 이루어졌다면 mul 함수는 resolve함수 인자에 result를 담아 then 메서드로 연결되어 result값(=14)를 출력 후 다시 sub함수에 result값을 인자로 넣어주고 리턴합니다. 같은 과정으로 then 메서드들은 순차적으로 진행하게 되다가 sub함수에 일부러 에러를 발생시키도록 catch메서드로 이어지는 reject함수에 에러 객체를 생성후 sub함수를 실행하게 된다면, 이 함수는 then 메서드에서 실행되지않고 catch메서드로 넘어가서 실패를 출력하고 reject함수의 인자를 출력합니다.
+)실습1. callback 함수 promise로 바꾸기
function call(name, cb) {
setTimeout(function () {
console.log(name);
cb(name);
}, 1000);
}
function back(cb) {
setTimeout(function () {
console.log('back');
cb('back');
}, 1000);
}
function hell(cb) {
setTimeout(function () {
cb('callbackk hell');
}, 1000);
}
call('kim', function (name) {
console.log(name + '반가워');
back(function (txt) {
console.log(txt + '을 실행했구나');
hell(function (message) {
console.log('여기는 ' + message);
});
});
});
//kim
//kim반가워
//back
//back을 실행했구나
//여기는 callbackk hell
이 callback 함수를 promise로 바꾼다면
function call(name) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log(name);
resolve(name);
}, 1000);
});
}
function back(n) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('back');
resolve('back');
}, 1000);
});
}
function hell(n) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve('callback hell');
}, 1000);
});
}
call('kim')
.then(function (name) {
console.log(name + '반가워');
return back(name);
})
.then(function (n) {
console.log(n + '을 실행했구나');
return hell(n);
})
.then(function (n) {
console.log('여기는 ' + n);
});
01-4.async-await
promise 도 chaning 하다보면 then().then()처럼 꼬리를 물게 되어 코드의 가독성이 떨어질 수 있습니다. async-await는 Promise 보다 직관적인 코드를 위해 등장한 것으로 따로 promise에서 기능이 추가된 것이아니라 promise를 다르게 사용하는 것입니다. 즉, 프로미스 기반 코드를 좀 더 쓰기 쉽고 읽기 쉽게 하기 위해 등장했으며 비동기 처리 패턴 중 가장 최근에 나온 문법입니다.
- async
- 함수 앞에 붙여 Promise를 반환
- 프로미스가 아닌 값을 반환해도 프로미스로 감싸서 반환한다.
- await
- 프로미스 앞에 붙여 프로미스가 다 처리될 때까지 기다리는 역할을 하며 결과는 그 후에 반환.
async function 함수명(){
await 비동기_처리_메서드_명();
}
기본 문법은 위와 같습니다.
async function f1() {
return 1;
}
async function f2() {
return Promise.resolve(1);
}
console.log('1 >> ', f1()); // f1 함수는 async 키워드가 붙어 있으므로 프로미스를 반환! => Promise { 1 }
f1().then(function (result) {
console.log('2 >> ', result); // 1
});
console.log('3 >> ', f2()); // Promise { <pending> }
f2().then(function (result) {
console.log('4 >> ', result); // 1
});
//1 >> Promise { 1 }
//3 >> Promise { <pending> }
//2 >> 1
//4 >> 1
async가 붙은 함수는 항상 Promise를 반환하고 async가 있는 함수에서만 await 키워드가 사용 가능합니다.
// 1초 뒤에 과일 배열을 출력하는 코드
function fetchFruits() {
return new Promise(function (resolve, reject) {
setTimeout(function () {
const fruits = ['사과', '레몬', '수박'];
resolve(fruits);
// reject(new Error('알 수 없는 에러 발생!! 아이템을 가져올 수 없음!!'));
}, 1000);
});
}
//1) promise then()메서드 사용
fetchFruits()
.then(function (f) {
console.log(f); //[ '사과', '레몬', '수박' ]
})
.catch(function (error) {
console.log(error); //Error: 알 수 없는 에러 발생!! 아이템을 가져올 수 없음!!
});
//[ '사과', '레몬', '수박' ]
위에 함수 fetchFruits 는 promise 객체 기반 함수로 1초뒤에 과일 배열을 출력하는 코드입니다. 이 코드를 async-await로 바꾸면
function fetchFruits() {
return new Promise(function (resolve, reject) {
setTimeout(function () {
const fruits = ['사과', '레몬', '수박'];
resolve(fruits);
// reject(new Error('알 수 없는 에러 발생!! 아이템을 가져올 수 없음!!'));
}, 1000);
});
}
async function printItems() {
try {
const fruits = await fetchFruits();
console.log(fruits);
} catch (error) {
console.log(error);
}
}
printItems();
//[ '사과', '레몬', '수박' ]
전에 했던 예제로 async-await로 바꾸면
function goMart() {
console.log('마트에 가서 어떤 음료를 살지 고민한다..');
}
function pickDrink() {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('고민 끝!!');
product = '제로 콜라';
price = 2000;
resolve();
}, 3000);
});
}
function pay() {
console.log(`상품명 : ${product}, 기갹 : ${price}`);
}
async function exec() {
goMart();
await pickDrink();
pay();
}
let product;
let price;
exec();
로 작성해주시면됩니다.
+)실습2. Promise-async/await
실습1에서 promise로 바꾼 코드를 exec 함수를 만들어 실행하게 하기 (이때, exec는 async함수)
function call(name) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log(name);
resolve(name);
}, 1000);
});
}
function back(n) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log(n);
resolve(n);
}, 1000);
});
}
function hell(n) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(n);
}, 1000);
});
}
async function exec() {
let user = await call('kim');
console.log(user + '반가워');
let space = await back('back');
console.log(space + '을 실행했구나');
let space2 = await hell('callbackhell');
console.log('여기는 ' + space2);
}
exec();
+)실습3. Promise로 배경색 변경하기
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
function change1(color) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
document.body.style.backgroundColor = color;
resolve();
}, 1000);
});
}
change1('red')
.then(function () {
return change1('orange');
})
.then(function () {
return change1('yellow');
});
</script>
</body>
</html>
function change1() {
document.body.style.backgroundColor = 'red';
return new Promise(function (resolve, reject) {
setTimeout(function () {
document.body.style.backgroundColor = 'orange';
resolve();
});
});
}
function change2() {
return new Promise(function (resolve, reject) {
setTimeout(function () {
document.body.style.backgroundColor = 'yellow';
resolve();
});
});
}
function change3() {
return new Promise(function (resolve, reject) {
setTimeout(function () {
document.body.style.backgroundColor = 'green';
resolve();
});
});
}
change1()
.then(function () {
return change2();
})
.then(function () {
return change3();
});
'SeSAC > javascript' 카테고리의 다른 글
[SeSACXCodingOn] 웹풀스택과정 07W_19_01 : MYSQL 연동하기 (0) | 2023.08.29 |
---|---|
[SeSACXCodingOn] 웹풀스택과정 06W_3_18 : MVC (+라우터쪼개기) (0) | 2023.08.25 |
[SeSACXCodingOn] 웹풀스택과정 4W_11 _01: Node.js + Module + Express (0) | 2023.08.09 |
[SeSACXCodingOn] 웹풀스택과정 2W_06 _04: javascript Event (0) | 2023.08.04 |
[SeSACXCodingOn] 웹풀스택과정 2W_06 _03: javascript DOM (0) | 2023.08.02 |