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();