비동기 처리 문제 해결 - callback, promise, await/async
비동기(asynchronous) 처리의 문제점
이전 게시글에서 동기와 비동기 처리에 대해 알아보았다.
이번 포스팅에서는 비동기가 갖는 문제점을 해결하기 위해, 동기적인 흐름으로 제어할 수 있는 방법 3가지(Callback, Promise, async/await)에 대해 알아볼 것이다.
먼저, 비동기 처리를 통해 발생하는 문제를 아래 코드를 통해 살펴보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class UserStorage {
loginUser(id, password) {
setTimeout(() => {
if (id === "hajeong" && password === "1234") {
return "Login success";
}
}, 1000);
}
}
const userStorage = new UserStorage();
const id = "hajeong";
const password = "1234";
console.log(userStorage.loginUser(id, password)); // undefined
setTimeout의 비동기 처리로 1초 뒤에 콜백함수가 실행되어 “Login success”를 return 해주는데 그 전에 console이 찍히기 때문에 undefined가 출력된다.
비동기 처리의 문제점을 가장 잘 느낄때는 데이터를 서버에서 받아올 때 일 것이다.
서버에서 데이터를 다 가져온 후에 데이터 필터 등 처리를 해야하는데 비동기 문제를 해결하지 않으면 없는 데이터에 대한 처리로 에러가 발생할 것이다.
비동기가 갖는 문제점을 알아보았으니 이제, 비동기가 갖는 문제점을 해결하기 위해 동기적인 흐름으로 제어할 수 있는 방법 3가지(Callback, Promise, async/await)에 대해 알아보자.
비동기 처리의 해결 1. Callback
Callback 함수란?
- 다른 함수의 파라미터로써 전달되는 함수
- 동기, 비동기적으로 사용 가능
1) 동기적(Synchronous) callback
1
2
3
4
5
// Synchronous callback
function printImmediately(print) {
print();
}
printImmediately(() => console.log("hello"));
2) 비동기적(Asynchronous) callback
1
2
3
4
5
// Asynchronous callback
function printWithDelay(print, delayTime) {
setTimeout(print, delayTime);
}
printWithDelay(() => console.log("hello"), 1000);
Callback로 비동기 처리 방식의 문제점 해결
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class UserStorage {
loginUser(id, password, onSuccess) {
setTimeout(() => {
if (id === "hajeong" && password === "1234") {
onSuccess();
}
}, 1000);
}
}
const userStorage = new UserStorage();
const id = "hajeong";
const password = "1234";
userStorage.loginUser(id, password, () => {
console.log("Login success"); // Login success
});
loginUser 메소드의 파라미터에 나중에 실행시키고자 하는 callback함수를 담아 호출한다.
setTimeout으로 1초 뒤에 callback함수로 전달받은 onSuccess가 실행되어 console에 “Login success”가 출력된다.
Callback 함수의 문제점: Callback Hell
위의 코드에서 console에 undefined가 아닌 “Login success”가 출력되는 것을 확인하였다.
하지만 callback함수의 가장 큰 문제인 callback hell이 있다.
* Callback Hell
callback hell이 발생할 수 있는 상황을 알아보기 위해 로그인 후 사용자의 정보를 가져온다고 가정해보자.
- 로그인 후 사용자의 정보를 가져오기
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
34
35
36
37
38
39
40
41
42
43
class UserStorage {
loginUser(id, password, onSuccess, onError) {
setTimeout(() => {
if (id === "hajeong" && password === "1234") {
onSuccess(id);
} else {
onError(new Error("not found"));
}
}, 1000);
}
getRoles(user, onSuccess, onError) {
setTimeout(() => {
if (id === "hajeong") {
onSuccess({ name: "hajeong", role: "admin" });
} else {
onError(new Error("not access"));
}
}, 1000);
}
}
const userStorage = new UserStorage();
const id = "hajeong";
const password = "1234";
userStorage.loginUser(
id,
password,
(user) => {
userStorage.getRoles(
user,
(userWithRole) =>
console.log(`${userWithRole.name} ${userWithRole.role}`), // hajeong admin
(error) => {
console.log(error);
}
);
},
(error) => {
console.log(error);
}
);
loginUser로 로그인 성공 여부를 확인한 다음 성공하면 로그인한 유저의 id를 getRoles로 전달하여 getRoles에서 id에 해당하는 이름과 역할을 출력하게 된다.
위의 예시에서는 로그인한 유저 id를 한번만 전달하면 되기 때문에 그렇게 문제가 되지 않을 것이라 생각할 수도 있다.
만약에 로그인한 유저의 id를 찾고 그 id로 역할을 찾고 그 역할에 대한 의무사항을 찾는 식으로 진행된다면 계속해서 타고 타고 결국엔 callback hell이 발생하고 그렇게 되면 코드를 알아보기 어려워 질 것이다.
비동기 처리의 해결 2. Promise
Promise란?
프로미스는 자바스크립트 비동기 처리에 사용되는 객체로 콜백함수를 사용하는 것보다 효율적으로 흐름 제어를 하기 위하여 고안된 방법이다.
Promise의 3가지 상태(State)
1. Pending(대기)
비동기 처리 로직이 아직 완료되지 않은 상태
- new Promise() 메서드를 호출하면 대기(Pending) 상태
- 콜백 함수의 인자는 resolve, reject
1
2
3
new Promise(function (resolve, reject) {
// ...
});
2. Fullfilled(이행)
비동기 처리가 완료되어 프로미스가 결과 값을 반환해준 상태
- resolve를 실행하면 이행(Fulfilled) 상태
- then()을 이용하여 처리 결과 값을 받을 수 있음.
1
2
3
4
5
6
7
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("hajeong");
}, 2000);
});
promise.then((value) => console.log(value)); // hajeong
3. Rejected(실패)
비동기 처리가 실패하거나 오류가 발생한 상태
- reject를 호출하면 실패(Rejected) 상태
- 실패 상태가 되면 실패한 이유(실패 처리의 결과 값)를 catch()로 받을 수 있음
1
2
3
4
5
6
7
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error("error"));
}, 2000);
});
promise.then().catch((error) => console.log(error)); // Error: error
Promise의 예외 처리
실제 서비스를 구현하다 보면 네트워크 연결, 서버 문제 등으로 인해 오류가 발생할 수도 있다.
에러 처리 방법은 2가지가 있는데, 모두 프로미스의 reject() 메서드가 호출되어 실패 상태가 된 경우에 실행된다.
1. then() 의 두 번째 인자로 에러를 처리하는 방법
1
getData().then(handleSuccess, handleError);
2. catch() 를 이용하는 방법
1
getData().then().catch();
⇒ 프로미스의 에러 처리는 가급적 catch()를 이용하는 것이 효율적이다.
Promise로 비동기 처리 방식의 문제점 해결
- 로그인 후 사용자의 정보를 가져오기
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
34
35
36
class UserStorage {
loginUser(id, password) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id === "hajeong" && password === "1234") {
resolve(id);
} else {
reject(new Error("not found"));
}
}, 1000);
});
}
getRoles(user) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id === "hajeong") {
resolve({ name: "hajeong", role: "admin" });
} else {
reject(new Error("not access"));
}
}, 1000);
});
}
}
const userStorage = new UserStorage();
const id = "hajeong";
const password = "1234";
userStorage
.loginUser(id, password)
.then(userStorage.getRoles)
.then((userWithRole) =>
console.log(`${userWithRole.name} ${userWithRole.role}`)
)
.catch(console.log);
callback함수로 작성한 코드는 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드를 해석하기 힘들었는데 promise로 작성하니 가독성이 좋아진 것을 볼 수 있다.
가독성이 좋아진다는 장점도 있지만 가장 중요한 장점은 실행은 바로하지만, 결괏값을 나중에 원할 때 쓸 수 있다는 것이다.
1
2
const user = userStorage.loginUser(id, password);
user.then((id) => console.log(id)); // hajeong
하지만 promise도 단점이 존재하는데 바로 Promise hell이다 .
Promise의 문제점: Promise hell
then 메소드는 Promise를 다시 반환하기 때문에 결과 값을 받기 위해서는 연속적으로 then 메서드를 사용해야 한다.
Promise 객체 반환 -> then, catch 메소드를 사용가능 -> 연속적인 then 메소드 사용 -> Promise hell
- 사과 + 바나나 출력 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function getApple() {
await delay(1000);
return "사과";
}
async function getBanana() {
await delay(2000);
return "바나나";
}
function pickFruits() {
return getApple().then((apple) => {
return getBanana().then((banana) => `${apple} + ${banana}`);
});
}
pickFruits().then(console.log); // 사과 + 바나나
비동기 처리의 해결 3. async/await
async와 await는 자바스크립트의 비동기 처리 패턴 중 가장 최근에 나온 문법이다.
기존의 비동기 처리 방식인 콜백 함수와 프로미스의 단점을 보완하고 개발자가 읽기 좋은 코드를 작성할 수 있게 도와준다.
위의 promise hell의 예시를 await으로 어떻게 해결할 수 있는지 먼저 코드를 통해 확인해보자.
1
2
3
4
5
6
7
8
9
async function pickFruits() {
try {
const apple = await getApple();
const banana = await getBanana();
return `${apple}+${banana}`;
} catch (error) {
console.log(error);
}
}
함수를 호출하는 부분이 linear 하게 변경되었고 훨씬 깔끔해진 것을 볼 수 있다. 이제 await/async에 대해 알아보자!
async/await란?
async
function 앞에 async 키워드가 붙으면 해당 함수는 항상 Promise를 반환한다.
Promise가 아닌 값을 반환하더라도 이행 상태의 Promise(resolved promise)로 감싸 이행된 Promise가 반환된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
function fetchUser1() {
return new Promise((resolve) => resolve("hajeong"));
}
async function fetchUser2() {
return "hajeong";
}
const user1 = fetchUser1();
console.log(user1); // Promise {<fulfilled>: 'hajeong'}
const user2 = fetchUser2();
console.log(user2); // Promise {<fulfilled>: 'hajeong'}
await
await 키워드는 async 함수 안에서만 동작한다.
말그대로 Promise가 처리될 때까지 함수 실행을 기다리게 만든다.
즉, 자바스크립트가 await 키워드를 만나면 프라미스가 처리(settled)될 때까지 기다린 후 그 결과를 반환한다.
1
2
3
async function 함수명() {
await 비동기 처리 메서드명();
}
async/await 예외 처리
async & await 에서 예외를 처리하는 방법은 try.. catch.. 구문을 사용하는 것이다.
catch {} 를 사용하면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async function loadData() {
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
return resolve("success");
}, 1000);
});
}
try {
const result = await fetchData();
console.log(result);
} catch (e) {
console.log(e);
}
}
async/await로 비동기 처리 방식의 문제점 해결
- 로그인 후 사용자의 정보를 가져오기
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
34
35
36
37
class UserStorage {
delay(time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
async loginUser(id, password) {
await delay(1000);
if (id === "hajeong" && password === "1234") {
return id;
} else {
throw "not found";
}
}
async getRoles(user) {
await delay(1000);
if (id === "hajeong") {
return { name: "hajeong", role: "admin" };
} else {
throw "not access";
}
}
}
const userStorage = new UserStorage();
const id = "hajeong";
const password = "1234";
const useAsyncAwait = async () => {
try {
const login = await userStorage.loginUser(id, password);
const roles = await userStorage.getRoles(login);
console.log(`${roles.name} ${roles.role}`);
} catch (error) {
console.log(error);
}
};
useAsyncAwait();
📑 참고자료