# 콜백함수를 통한 비동기처리의 문제점
ES6에 Promise가 도입되어 지금처럼 널리 사용되기 이전에는 주로 콜백함수를 다른 함수의 인자로 넘겨서 비동기처리를 했었다.
2023.12.23 - [JavaScript] - 비동기함수의 순서를 제어하는 방법 1. Callback함수
단순 코드를 작성할 때는 위와 같이 전통적인 방식으로 콜백함수를 통해 비동기처리를 해도 큰 문제가 발생하지 않았다.
하지만, 콜백함수를 중첩해서 연쇄적으로 호출해야하는 복잡한 코드의 경우, 계속되는 들여쓰기 때문에 코드 가독성이 현저하게 떨어진다. (콜백지옥..)
자바스크립트 개발자들 사이에서 소위 '콜백지옥'이라고 불리는 이 문제를 해결하기 위해 여러가지 방법들이 논의되었고, 그 중 하나가 "Promise".
# Promise의 개념
Promise는 현재에는 당장 얻을 수는 없지만(데이터를 얻는데까지 지연시간이 발생하는 경우), 가까운 미래에는 얻을 수 있는 데이터에 접근하기 위한 방법을 제공.
I/O나 network를 통해서 데이터를 얻는 경우가 대표적.
// callback함수 이용
findUserAndCallBack(1, function (user) {
console.log("user:", user); //비동기함수 실행 후, 실행될 코드
});
function findUserAndCallBack(id, cb) {
setTimeout(function () {
console.log("waited 0.1 sec.");
const user = {
id: id,
name: "User" + id,
email: id + "@test.com",
};
cb(user);
}, 100);
}
비동기함수 다음에 실행될 로직을 callback함수에 넣는 방법이다.
callback함수를 이용한 위 코드를 Promise를 이용하여 아래와 같이 재작성할 수 있다.
// promise 이용
findUserAndCallBack(1).then(function (user) {
console.log("user:", user);
});
function findUserAndCallBack(id) {
return new Promise(function (resolve) {
setTimeout(function () {
console.log("waited 0.1 sec.");
const user = {
id: id,
name: "User" + id,
email: id + "@test.com",
};
resolve(user);
}, 100);
})
}
waited 0.1 sec.
user: {id: 1, name: "User1", email: "1@test.com"}
콜백함수를 인자로 넘기는 대신에 Promise객체를 생성하여 리턴.
호출부에서는 리턴받은 Promise 객체(findUserAndCallBack(1))에 then()메서드를 호출하여 결과값을 가지고 실행할 로직을 넘겨주고 있다.
## 콜백함수와의 가장 큰 차이점
함수를 호출하면 'Promise 타입의 결과값'이 return되고, 이 결과값을 가지고 다음에 수행할 작업을 진행한다는 것.
따라서 기존 스타일보다 비동기처리임에도 불구하고, 마치 동기 처리코드처럼 코드가 읽히기 때문에 좀 더 '직관적'으로 느껴지게 된다.
(비동기방식으로 처리된 Promise 객체 → 비동기 처리로직이 종료된 후, then메서드의 콜백함수 실행)
# Promise 생성방법
Promise 객체는 new 키워드와 생성자를 통해서 생성할 수 있는데, 이 생성자는 함수를 인자로 받는다.
이 함수인자는 resolve와 reject라는 2개의 함수형 파라미터를 가진다.
아래와 같은 모습으로 Promise 객체를 생성하여 변수에 할당할 수 있다.
const promise = new Promise(function(resolve, reject) {...});
실제로는 위 코드처럼 Promise객체를 변수에 할당하기보다는, 어떤 함수의 return값으로 바로 사용되는 경우가 많고, ES6의 화살표함수 키워드를 많이 사용한다.
function returnPromise(){
return new Promise((resolve, reject) => { ... });
}
생성자의 인자로 넘어가는 함수 인자의 바디에는 resolve()나 reject()함수를 정상처리, 예외 발생 여부에 따라 적절히 호출해줘야한다.
일반적으로
- resolve()함수의 인자로는 미래 시점에 얻게될 결과를 넘겨주고,
- reject() 함수의 인자로는 미래 시점에 발생할 예외를 넘겨준다.
예를 들어, 나눗셈 함수를 Promise를 리턴하도록 구현해보자.
(나눗셈을 비동기처리할 이유는 없지만 간단한 예시를 위해..)
function divide(numA, numB) {
return new Promise((resolve, reject) => {
if(numB === 0) reject(new Error('Unable to divide by 0.'));
else resolve(numA / numB);
});
}
정상적인 인자를 넘겨 divide()함수를 호출하여 Promise객체를 얻은 후, 결과값을 출력해보자.
divide(8, 2)
.then((result) => console.log("성공:", result))
.catch((error) => console.log("실패:", error));
성공: 4
정상적인 인자를 넘긴 경우, then() 메서드가 호출되었다.
이번에는 비정상적인 인자를 넘겨보자.
divide(8, 0)
.then((result) => console.log("성공:", result))
.catch((error) => console.log("실패:", error));
실패: Error: Unable to divide by 0.
at <anonymous>:3:26
at new Promise (<anonymous>)
at divide (<anonymous>:2:10)
at <anonymous>:1:1
비정상적인 인자를 넘긴 경우, catch() 메서드가 호출되었다.
# Promise 사용 방법
실제 코딩을 할 때는 위와 같이 Promise를 직접 생성해서 return해주는 코드보다는, 어떤 라이브러리의 함수를 호출해서 return받은 Promise 객체를 사용하는 경우가 더 많다.
REST API를 호출할 때 사용되는 브라우저 내장함수인 fetch()가 대표적.
(NodeJS 런타임에서는 node-fetch 모듈을 설치해야 사용 가능하다.)
fetch() 함수는 API의 URL을 인자로 받고, 미래 시점에 얻게될 API 호출 결과를 "Promise 객체"로 리턴.
network latency (네트워크 시간지연)때문에 바로 결과값을 얻을 수 없는 상황이므로, 위에서 설명한 Promise 사용 목적에 정확히 부합한다.
- Promise 객체의 then() 메서드는 결과값을 가지고 수행할 로직을 담은 콜백함수를 인자로 받는다.
- catch() 메서드는 예외처리 로직을 담은 콜백함수를 인자로 받는다.
fetch() 함수를 이용하여 어떤 서비스의 API를 호출 후, 정상 응답 결과를 출력해보자.
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then((response) => console.log("response:", response))
.catch((error) => console.log("error:", error));
response: Response {type: "cors", url: "https://jsonplaceholder.typicode.com/posts/1", redirected: false, status: 200, ok: true, …}
인터넷 상에 유효한 URL을 fetch() 함수의 인자로 넘겼기 때문에, 예외가 발생하지 않고 then()인자로 넘긴 콜백함수가 호출되어 상태 코드 200의 응답이 출력되었다.
이번에는 fetch()함수의 인자를 넘기지 말아보자.
fetch()
.then((response) => console.log("response:", response))
.catch((error) => console.log("error:", error));
error: TypeError: Failed to execute 'fetch' on 'Window': 1 argument required, but only 0 present.
at main-sha512-G7qgGx8Wefk5JskAfRw2DfBPNPQTxDC23DcZ+KQTmNoSr2S6pZ3IJgYs1ThvLvvH7uI_KhycDx_FIDNlu5KhOw==.bundle.js:9070
at <anonymous>:1:1
이번에는 catch() 메서드의 인자로 넘긴 콜백함수가 호출되어 에러 정보가 출력되었다.
이와 같이 Promise는 then()과 catch() 메서드를 통해 동기처리 코드에서 사용하던 try-catch 블록과 유사한 방법으로 비동기 처리 코드를 작성할 수 있다.
# Promise의 메서드 체이닝(Method chaining)
then()과 catch()메서드는 또 다른 Promise 객체를 리턴한다.
그리고 이 Promise 객체는 인자로 넘긴 콜백 함수의 리턴값을 다시 then()과 catch() 메서드를 통해 접근할 수 있도록 해준다.
다시 말하면 then()과 catch() 메서드는 마치 사슬처럼 계속 연결하여 연쇄적으로 호출할 수 있다.
예를 들어, fetch() 메서드 사용 예제에서 단순히 응답 결과가 아닌, json형태로 출력하고 싶은 경우에는 then() 메서드를 추가로 연결해주면 된다.
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then((response) => response.json())
.then((post) => console.log("post:", post))//response.json()값이 post로 넘어옴
.catch((error) => console.log("error:", error));
post: {userId: 1, id: 1, title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit', body: 'quia et suscipit\nsuscipit recusandae consequuntur …strum rerum est autem sunt rem eveniet architecto'}
또 다른 예로 userId 1을 가진 유저의 데이터가 필요한 경우, 다음과 같이 추가 메서드 체이닝을 할 수 있다.
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then((response) => response.json())
.then((post) => post.userId) //post객체에서 userId만 추출하여 return
.then((userId) => "https://jsonplaceholder.typicode.com/users/" + userId) //위 post.userId가 post로 넘어옴
.then((url) => fetch(url))
.then((post) => console.log("user:", user))
.catch((error) => console.log("error:", error));
user: {id: 1, name: "Leanne Graham", username: "Bret", email: "Sincere@april.biz", address: {…}, …}
then()과 catch()의 인자로 넘긴 콜백함수는 3,4번째 줄처럼 Promise객체가 아닌 값이 return하든, 5번째 줄처럼 Promise객체를 리턴하든 크게 상관이 없다.
자바스크립트에서 then()함수는 인자로 넘어온 콜백함수가 무엇을 반환하는지에 따라 다르게 작동한다.
- then()함수의 인자로 넘어온 콜백함수가 어떤 값을 'return'할 경우 : 그 값이 그대로 그 다음 then()함수로 넘긴 콜백함수의 인자로 전달된다.
- then()함수의 인자로 넘어온 콜백함수가 어떤 값을 'resolve'할 경우 (즉, Promise객체를 반환하면) : Promise 객체가 비동기로 제공하는 값이 그 다음 then()함수로 넘긴 콜백함수의 인자로 전달된다.
function returnArg(arg) {
return `${arg} in returnArg 👉`;
}
function resolveArg(arg) {
return new Promise((resolve) => setTimeout(resolve(`${arg} in resolveArg 👉`), 100));
}
resolveArg("시작 👉")
.then((arg) => returnArg(arg)) // arg: "시작 👉 in resolveArg 👉"
.then((arg) => returnArg(arg)) // arg: "시작 👉 in resolveArg 👉 in returnArg 👉"
.then((arg) => console.log(arg)); // arg: "시작 👉 in resolveArg 👉 in returnArg 👉 in returnArg 👉"
resolveArg("시작 👉")
.then((arg) => resolveArg(arg)) // arg: "시작 👉 in resolveArg 👉"
.then((arg) => resolveArg(arg)) // arg: "시작 👉 in resolveArg 👉 in resolveArg 👉"
.then((arg) => console.log(arg)); // arg: "시작 👉 in resolveArg 👉 in resolveArg 👉 in resolveArg 👉"
# 마치면서
Promise를 사용하면서 자연스럽게 발생하는 이러한 코딩 스타일은 매우 흔하게 볼 수 있으며, 자바스크립트 개발자들 사이에서도 호불호가 갈리기도 한다.
최근 이러한 Promise를 이용해서 계속해서 메서드 체이닝하는 코딩 스타일은 자바스크립트의 async/await 키워드를 사용하는 방식으로 대체되고 있는 추세이다.
* 참고글 : https://www.daleseo.com/js-async-promise/
'JavaScript' 카테고리의 다른 글
자바스크립트 동작 원리(Call Stack, Task Queue, Event Loop etc) (0) | 2024.02.04 |
---|---|
비동기 처리 - async/await (2) | 2024.01.03 |
비동기함수의 순서를 제어하는 방법 1. Callback함수 (2) | 2023.12.23 |
callback함수란? (0) | 2023.12.23 |
원시 값과 객체의 비교 (1) | 2023.12.19 |