P4Ch8. 비동기
동기와 비동기
동기(synchronous): 순차적으로 코드 실행
비동기(Asynchronous): 순차적인 코드 실행X (ex. 버튼 클릭, 요청)
아래는 setTimeout을 사용한 비동기 코드의 예시이다.
1
2
3
4
5
console.log(1);
setTmeout(() => {
console.log(2);
}, 1000);
console.log(3);
결과: 1 3 2
=> 코드 순서대로 실행되지 않았다!
콜백과 콜백 지옥
1
2
3
4
5
6
7
8
9
const a = () => {
setTimeout(() => {
console.log(1);
}, 1000);
};
const b = () => console.log(2);
a();
b();
위 코드는 1초 후에 console.log(1)을 실행하는 코드이다.
코드는 a(),b()의 순으로 순차적으로 실행되나 함수 a() 내부의 코드 console.log(1)은 setTimeout()으로 인해 비동기적으로 실행되기에 console.log(2) 이후에 실행된다.
콜백(Callback) 패턴
callback을 사용해 코드의 실행 순서를 보장할 수 있다.
1초 뒤 console.log(1)가 실행된 다음에 console.log(2) 가 실행되길 원한다.
const a = (callback) => {
setTimeout(() => {
console.log(1);
callback();
}, 1000);
};
const b = () => console.log(2);
a(() => {
b();
});
a 함수의 callback 매개변수로 b함수 데이터를 넣음으로 a함수 실행 뒤 b함수의 실행을 보장할 수 있다.
콜백 지옥
비동기 패턴에서 실행순서를 보장하기 위해 콜백 함수를 중첩해 사용하고, 그로인해 연속적인 들여쓰기가 발생하는 것
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const a = (callback) => {
setTimeout(() => {
console.log(1);
callback();
}, 1000);
};
const b = (callback) => {
setTimeout(() => {
console.log(2);
}, 1000);
};
const c = () => console.log(3);
a(() => {
b(() => {
c();
});
});
여기서 const d = () => console.log(4)를 추가해 1,2,3,4 순서로 값이 출력되길 원한다면…?😵💫
콜백 지옥은 코드의 가독성을 떨어뜨리고, 프로그래머가 코드를 유지보수하기 어렵게 만든다.
콜백 지옥 탈출법
Promise
- Promise를 사용하면 콜백 지옥을 피할 수 있다.
- callback 대신
resolve매개변수를 사용해 실행순서를 보장할 수 있다.
- callback 대신
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const a = () => {
return new Promise((resolve) => {
setTimeout(() => {
console.log(1)
resolve()
},1000)
})
}
const b = () => {
return new Promise((resolve) => {
setTimeout(() => {
console.log(2)
resolve()
},1000)
})
}
const c = () => {
return new Promise((resolve) => {
setTimeout(() => {
console.log(3)
resolve()
},1000)
})
}
const d = () => console.log(4)
a().then(() => {
return b()
}).then(() => {}
return c()
).then(() => {
d()
})
- 함수 a는
Promise를 통해 생성한 인스턴스를 반환한다.- Promise로 만들어진 인스턴스에서는
then메소드를 이어서 사용 가능하다. - 이때 then의 콜백 함수는 Promise 생성자 함수의
resolve매개변수로 사용된다. then의 콜백이 Promise 인스턴스를 반환하는 경우, 메소드 체이닝으로 then 메소드를 한 번 더 사용 가능하다.
- Promise로 만들어진 인스턴스에서는
1
2
3
4
5
6
a()
.then(() => b())
.then(() => c())
.then(() => {
d();
});
위의 코드에서 then이 인수로 가진 화살표 함수 내부에 return 외 다른 코드가 존재하지 않으므로, 위 코드는 다음과 같이 쓸 수 있다.
1
a().then(b).then(c).then(d);
또한 resolve 매개변수로 넘어가는 데이터는 함수 데이터이기 때문에, 위와 같은 코드 대신 화살표 함수 대신 실행하려는 a,b,c,d 함수 데이터 자체를 넣어줄수도 있다.
Async Await 패턴
우리는 a 함수 실행 후 b함수를 실행하는 순서를 보장하기 위해 Promise를 사용할 수 있다는 것을 앞서 학습했다.
이와 같이 실행 순서를 보장하는 또다른 방법으로 Async Await 패턴을 사용하는 방법이 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const a = () => {
return new Promise((resolve) => {
setTimeout(() => {
console.log(1);
resolve();
}, 1000);
});
};
const b = () => console.log(2);
const wrap = async () => {
await a();
b();
};
wrap();
// a().then(() => b())
- a는 Promise 인스턴스를 반환하는, 비동기 코드로 만들어진 함수이다.
- await 키워드는 Promise 인스턴스를 반환하는 비동기 함수 바로 앞에 붙여 사용한다.
- await는 해당 비동기 함수의 Promise 인스턴스가 반환될때까지 기다린다.
- await를 사용하는 코드는 async를 사용하는 함수 내부에 존재해야한다.
Resolve, Reject 그리고 에러 핸들링
아래는 콜백을 사용해 에러를 핸들링하는 예제코드다.
const delayAdd = (index, cb, errorCb) => {
setTimeout(() => {
if (index > 10) {
errorCb(`${index}는 10보다 클 수 없습니다.`);
return;
}
console.log(index);
cb(index + 1);
}, 1000);
};
delayAdd(
4,
(res) => console.log(res),
(err) => console.error(err)
);
위 코드는 비동기적으로 동작하며, 로직이 정상 동작할 시 delayAdd의 두 번째 인수 부분의 콜백(cb)이 실행되고, 정상적으로 처리 되지 않았다면 세 번째 인수의 콜백(errorCb)이 실행되는 방식으로 에러를 핸들링하는 코드이다.
따라서 비동기 작업에서의 에러 핸들링은
Promise를 사용하는 것이 선호된다.
Promise를 사용한 에러 핸들링
함수 delayAdd의 정의는 아래와 같다.
const delayAdd = (index) => {
return new Promise(( resolve, reject) => {
setTimeout(() => {
if (index > 10) {
reject(`${index}는 10보다 클 수 없습니다.`);
return;
}
console.log(index);
resolve(index + 1);
}, 1000);
});
};
이때, Promise를 사용해 에러 핸들링을 하는 3가지 방법이 있다.
then, catch 메소드 사용
1
2
3
4
delayAdd(4)
.then((res) => console.log(res))
.catch((err) => console.error(err))
.finally(() => console.log("Done!"));
- then과 catch 메소드를 사용해 에러 핸들링이 가능하다.
- then 메소드의 콜백은
resolve매개변수로 들어간다. - catch 메소드의 콜백은
reject매개변수로 들어간다.
- then 메소드의 콜백은
- reject와 resolve 둘 중 하나가 실행되면 다른 하나는 실행되지 않는다.
- finally 메소드는 reject 혹은 resolve의 동작 여부와 관계없이 항상 동작한다.
Async Await 패턴
1
2
3
4
5
const wrap = async () => {
const res = await delayAdd(2);
console.log(res);
};
wrap();
- 데이터를 받아와(then과 catch 메소드의 res, err와 같이) 변수에 할당 할 수 있다.
Async Await 패턴에서의 catch 사용
1
2
3
4
5
6
7
8
9
10
11
const wrap = async () => {
try {
const res = await delayAdd(12);
console.log(res);
} catch (err) {
console.error(err);
} finally {
console.log("Done!");
}
};
wrap();
- error가 발생하면 catch 구문이 실행된다.
- reject의 인수는 catch 구문의 err 변수로 들어간다.
- finally 구문은 reject 혹은 resolve의 동작 여부와 관계없이 항상 동작한다.
반복문에서의 비동기 처리
forEach에서의 await는 실행 순서를 보장하지 못한다!
1
2
3
4
5
6
7
8
9
10
11
12
13
const getMovies = (movieName) => {
return new Promise((resolve) => {
fetch(`https://www.omdbapi.com/?apikey=7035c60c&s=${movieNAme}`)
.then((res) => res.json())
.then((res) => resolve(res));
});
};
const titles = ["frozen", "avengers", "avatar"];
titles.forEach(async (title) => {
const movies = await getMovies(title);
console.log(title, movies);
});
forEach 메소드는 비동기 작업의 완료를 기다리지 않고 다음 반복을 시작한다. 따라서 getMovies 함수 호 frozen, avengers, avatar의 순차적인 실행을 보장할 수 없다.
반복문에서의 비동기 처리를 위해 forEach 메소드 대신 for 반복을 사용해야 한다.
1
2
3
4
5
6
7
const wrap = async () => {
for (const title of titles) {
const movies = await getMovies(title);
console.log(title, movies);
}
};
wrap();