callback 함수를 통한 비동기 처리의 문제점
function makeCharcter(id,job,func){
let user
setTimeout(function(){
console.log('1초 뒤 실행')
user ={
id : id,
job : job,
info : id + job
};
func(user)
},1000)
}
makeCharcter('hoso','warrior',function(a){console.log('character:',a)})
한개의 callback함수만 있다면 전혀 어지러워 보이지 않지만 저 만들어진 callback 함수 속에 또 함수를 만들고 또 함수를 만들고 하면 코드가 너무나도 길어질 것입니다.
콜백 함수를 중첩해서 연쇄적으로 호출해야하는 복잡한 코드의 경우, 계속되는 들여쓰기 때문에 코드 가독성이 현저하게 떨어지게 됩니다. 자바스크립트 개발자들 사이에서 소위 콜백 지옥이라고 불리는 이 문제를 해결하기 위해 여러가지 방법들이 논의 되었고 그 중 하나가 Promise 입니다.
Promise의 개념
뭔가 Promise를 보면 결과의 알을 만들고 .then이나 .catch로 알을 깨주는 느낌이 듭니다. 알이 만들어지지 않으면 다음을 진행하지 않는다?는 느낌도 있고요.
Promise는 현재에는 당장 얻을 수는 없지만 가까운 미래에는 얻을 수 있는 어떤 데이터에 접근하기 위한 방법을 제공합니다.
당장 원하는 데이터를 얻을 수 없다는 것은 데이터를 얻는데까지 지연 시간(delay, latency)이 발생하는 경우를 말합니다. I/O나 Network를 통해서 데이터를 얻는 경우가 대표적인데, CPU에 의해서 실행되는 코드 입장에서는 엄청나게 긴 지연 시간으로 여겨지기 때문에 Non-blocking 코드를 지향하는 자바스크립트에서는 비동기 처리가 필수적입니다.
- 예시
makeCharcter('hoso','warrior').then(function(a){console.log('character:',a)})
function makeCharcter(id,job){
return new Promise(function(resolve,reject){
setTimeout(function(){
console.log('1초 뒤 실행')
let user ={
id : id,
job : job,
info : id + job
};
resolve(user)
},1000)
})
}
callback함수를 인자로 넘기는 대신에 Promise 객체를 생성하여 리턴하고 resolve를 하기 위해 then 메소드를 호출하여 결과값을 가지고 뒤에 있는 callback함수? 를 실행시키는 것입니다.
이렇게 되면 값을 먼저 무조건 가져오게 된 뒤에 함수가 실행이 됩니다. 만약에 resolve 되지 않는다면 아래와 같은 결과가 나타나게 됩니다.
Promise { <pending> }
pending의 사전적 의미 : (어떤 일이) 있을 때 까지, ...을 기다리는 동안
사전적 의미처럼 저 값을 풀어주는 메소드가 있을 때까지 알에 들어있겠다. then메소드라는 부화기를 가져올 때까지 기다리겠다.... 이런거 아닐까요.
Promise 생성법
먼저 Promise 객체를 리턴하는 함수를 작성하는 방법에 대해서 알아보겠습니다.
Promise는 객체는 new 키워드와 생성자를 통해서 생성할 수 있는데 이 생성자는 함수를 인자로 받습니다. 그리고 이 함수 인자는 reslove와 reject라는 2개의 함수형 파라미터를 가집니다.
따라서 아래와 같은 모습으로 Promise 객체를 생성해서 변수에 할당할 수 있습니다.
const promise = new Promise(function(resolve, reject) { ... } );
실제로는 변수에 할당하기 보다는 어떤 함수의 리턴값으로 바로 사용되는 경우가 많고, ES6의 화살표 함수 키워드를 더 많이 사용하는 것 같습니다.
function returnPromise() {
return new Promise((resolve, reject) => { ... } );
}
resolve와 reject 를 사용하는 예시를 볼까요
function devide(numA, numB) {
return new Promise((resolve, reject) => {
if (numB === 0) reject(new Error("Unable to devide by 0."));
else resolve(numA / numB);
});
}
그리고 먼저 정상적인 인자를 넘겨 devide() 함수를 호출해서 Promise 객체를 얻은 후 결과값을 출력해보겠습니다.
devide(8, 2)
.then((result) => console.log("성공:", result))
.catch((error) => console.log("실패:", error));
- 결과
성공: 4
이번에는 비정상적인 인자를 넘겨보겠습니다.
devide(8, 0)
.then((result) => console.log("성공:", result))
.catch((error) => console.log("실패:", error));
- 결과
실패: Error: Unable to devide by 0.
at Promise (<anonymous>:4:20)
at new Promise (<anonymous>)
at devide (<anonymous>:2:12)
at <anonymous>:1:1
출력 결과를 통해 정상적인 인자를 넘긴 경우 then() 메서드가 호출되고, 비정상적인 인자를 넘긴 경우 catch() 메서드가 호출되었다는 것을 알 수 있습니다.
실제 코딩 할 때는 Promise를 직접 생성하여 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() 함수의 인자로 아무 URL을 넘기지 않아보겠습니다.
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의 메서드 체이닝(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))
.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↵suscipit recusandae consequuntur …strum rerum est autem sunt rem eveniet architecto"}
또 다른 예로, 위의 포스팅를 작성한 userId 1을 가진 유저의 데이터가 필요한 경우, 다음과 같이 추가 메서드 체이닝을 할 수 있습니다. 3번째 줄의 콜백 함수는, post 객체에서 userId 필드만 추출하여 리턴하고 있으며, 4번째 줄의 콜백 함수는, 이 userId를 가자고 유저 상세 조회를 위한 API의 URL을 만들어서 리턴하고 있으며, 5번째 줄의 콜백 함수는, 이 url을 가지고 fetch() 함수를 호출하여 새로운 Promise를 리턴하고 있습니다.
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then((response) => response.json())
.then((post) => post.userId)
.then((userId) => "https://jsonplaceholder.typicode.com/users/" + userId)
.then((url) => fetch(url))
.then((response) => response.json())
.then((user) => 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번째 줄처럼 일반 객체를 리턴하든 5번째 줄처럼 Promise 객체를 리턴하든 크게 상관이 없다는 것입니다. 왜냐하면 일반 객체를 리턴할 경우, then()과 catch() 메소드는 항상 그 객체를 얻을 수 있는 Promise 객체를 리턴하도록 되어 있기 때문입니다.
위의 코드를..... callback 함수로 표현하려고 했다면 정말 무슨 일이 벌어졌을까요.
받은 값을 또 함수로 넣어서 함수를 만들고, 또 그것을 다른 함수로 만든다음 또 또또또또하면 벌써 코드가 난리부르스를 추고 있을 것입니다. 정말 아름다운 메소드를 만들어낸 것 같습니다.
여러분들도 비동기 함수를 사용하기 위해! 코드를 짧게 줄이기 위해 Promise를 연습해보는 것도 좋을 것 같습니다. 아니면 async await도 좋을 것 같습니다.
출처
'코딩 개발 > Javascript' 카테고리의 다른 글
Execution Context & Call Stack (0) | 2023.05.14 |
---|---|
JavaScript - async/await (0) | 2023.02.07 |
JavaScript - Callback (0) | 2023.02.06 |
JavaScript - 최대공약수, 최소공배수 (0) | 2023.01.15 |
JavaScript - Prototype (0) | 2023.01.10 |