callback
을 활용한 비동기 함수를 제어하는 것은 윗 글과 같은 문제가 있었다.
이런 상황에서 callback hell
의 문제를 해결하기 위해 Promise
는 등장했다. 즉, Promise
문법은 callback
함수와 마찬가지로 비동기 함수의 실행 순서
를 제어하기 위해 나왔다고 볼 수 있는 것이다.
그렇다면 Promise는 callback과 어떻게 다른 방식으로 비동기의 순서를 제어하는가?
기존의 callback
함수를 연속적으로 활용해서 비동기 작업을 제어하는 것이 어려웠던 이유중 하나는 callback
함수가 여러 번 연결될수록 함수의 실행 순서가 직관적으로 눈에 들어오지 않게 되기 때문이었다.
아래와 같은 콜백 함수를 파악하려면 위와 아래를 번갈아가며 확인하면서 코드를 확인해야만 한다.
- 인터넷에 돌아다니는 흔한 callback hell 짤방
그런데 Promise 문법은 복잡하게 보이는 코드들을 위에서 아래로 한 방향으로 정리하여 우리가 읽기 편한 상태로 정리해주었다.
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000); // (*)
}).then(function(result) { // (**)
alert(result); // 1
return result * 2;
}).then(function(result) { // (***)
alert(result); // 2
return result * 2;
}).then(function(result) {
alert(result); // 4
return result * 2;
});
출처: https://ko.javascript.info/promise-chaining
위의 코드가 프로미스 체이닝
을 활용한 코드인데, 이전의 callback
을 활용한 방식과 달리 코드의 실행 순서가 .then()
이라는 단위를 바탕으로 해서 사람이 읽기 편한 방식과 순서대로 코드가 정리되는 것을 볼 수 있다.
아마도 저 맨 처음의 new Promise
라는 인스턴스 생성자가 익숙하지 않을 수도 있을 것이다. 충분히 그럴 수 있는 것이, 우리가 실제로 Promise 객체
를 사용할 때는 우리가 직접 new Promise
라는 생성자를 생성해서 사용하기보다는 fetch()
와 같은 API의 결괏값으로 Promise 객체
를 받아서 사용하는 경우가 훨씬 많기 때문일거다.
어쨋든 Promise
가 callback hell
의 문제를 해결하기 위해 도입되었다는 것을 기억하면 충분하다.
주소창에 https://jsonplaceholder.typicode.com/todos/1
를 쳐보면 JSON
형식으로 데이터를 반환해주는 것을 볼 수 있다.
이제 이걸 코드 상에서 데이터로 받고 싶으면 fetch()
같은 방법을 통해서 데이터를 요청할 수 있는데, 보통은
fetch("요청하고 싶은 주소")
의 형태로 구성된다.
그런데 fetch()
함수는 함수의 결괏값을 우리가 원하는 데이터의 형태가 아니라 Promise 객체
의 형태로 반환한다.
나는 처음에 Promise 객체
를 만났을 때 너무 당황했었다. 물론 내가 처음에 다른 사전지식 없이 Promise
를 만났기 때문이기도 하지만 아마 대부분의 우리가 처음에 그런 순간을 겪지 않을까 싶다. 앞서 언급했던 것처럼 우리가 Promise 객체
를 만나게 되는 첫 순간은 대부분 API 요청의 결과로 Promise 객체
를 받는 경우였을 거고, 앞으로도 그럴 것 같으니까 말이다.
갑자기 수업에서 fetch()
함수를 써보라고 해서 썼는데, 함수의 반환값으로 내가 원하는 데이터가 들어온 게 아니라 웬 못생긴 Promise {<pending>}
이라는 결괏값을 받았던 순간 말이다.
- 나는 data를 출력했는데, 내 데이터는 어디가고 이런 못생긴 게 들어있는 거지??
웹 상에서 Promise 객체
를 보면 이렇게 생겼는데,
- 이건 뭘까...?
저 안에 있는 것들이 뭔지 궁금하다면 검색하면서 찾아보시길!
요약하자면 fetch()
같은 어떤 비동기 함수들은 비동기 함수를 처리하는 방법으로 callback
과 Promise
중에서 Promise
를 활용한다.
그래서 우리에게 함수 실행에 대한 결괏값으로 Promise 객체
라는 것을 반환하는데, 이 Promise 객체를 우리가 원하는 방법으로 사용하기 위해서는 특별한 방법이 필요하다.
then()
const fetchData = () => {
fetch("https://jsonplaceholder.typicode.com/todos/1")
.then((response) => response.json())
.then((data) => {
console.log(data);
});
};
fetchData();
일단 결론부터 말하자면, 우리가 원하는 결과를 얻기 위해서는 then()
이라는 메서드를 사용해서 프로미스 체이닝
이라는 것을 하면 된다. 아마도 이미 대부분이 써봤을지도 모른다.
근데 저 코드에서는 무슨 일이 일어나고 있는가?
왜 then()
을 쓰면 promise 객체
가 data
로 변하는가?
그리고 then()
이 계속 이어지는 것은 무엇을 의미하는가?
여기서 하나의 예시를 들어볼까 한다.
혹시 중고 거래를 해 본 적이 있는가? 나는 애플 전자기기나 소니의 카메라를 좋아해서 전자기기를 자주 사고 파는 편인데, 중고 거래로 물건을 팔기 전날이면 택배를 보내기 전에 소중하게 물건을 닦고 포장해서 뽁뽁이로 감싸던 기억이 난다.
|
여기서 내가 팔고자 하는 카메라나 맥북은data
가 되고, 카메라나 맥북을 감싼 뽁뽁이나 택배 박스는Promise 객체
가 된다. 그리고 물건을 사고 파는 사람이then()
메서드가 된다.
|
물건을 사고 파는 사람은 물건을 산 동안에는 자유롭게 사용할 수 있지만, 그걸 다시 팔고자 해서 포장한 다음 다른 사람에게 배송중인 중간에는 물건을 사용할 수가 없다.
|
그게 바로then()
이 다음then()
에게Promise 객체
를 전달하는 과정이다.
위의 코드를 조금 뜯어보자.
const fetchData = () => {
fetch("https://jsonplaceholder.typicode.com/todos/1")
.then((response) => response.json())
.then((data) => {
console.log(data);
});
};
fetchData();
위의 함수는 fetch()
함수의 결괏값을 then()
의 response
로 넘겨준다는 뜻이므로 이렇게 쓸 수도 있다.
const fetchData = () => {
const result = fetch("https://jsonplaceholder.typicode.com/todos/1");
result.then((response) => response.json())
.then((data) => {
console.log(data);
});
};
fetchData();
하나씩 뜯어보자. 어렵게 느껴진다면 당연하다. 천천히 하나씩 보자.
위에서 말했던 카메라를 사고 파는 과정을 예시로 들어보자.
const buyDSLRCamera = () => {
const package = fetch("https://jsonplaceholder.typicode.com/todos/1");
console.log(package);
};
buyDSLRCamera();
먼저 fetch()
의 결괏값은 result
라는 변수에 담겨있다. 무엇이 담겨있을까? 요청에 대한 데이터가 담겨있을까?
궁금하다면 직접 코드를 적고 실행해봐도 되는데, 아마도 Promise {<pending>}
이라는 Promise 객체가 담겨있을 것이다.
저 Promise
객체는 우리가 그냥 쓸 수는 없는, 쉽게 말하면 포장이 뜯기지 않은
상태이다. 즉, 아까의 예시로 이어서 설명하자면 내가 새로 소니 카메라를 사서 물건을 받기는 했는데 아직 포장이 뜯기지 않은, 포장 박스에 담긴 상태인 것이다.
그리고 그 포장을 열어주는게 then()
이다.
const buyDSLRCamera = () => {
const package = fetch("https://jsonplaceholder.typicode.com/todos/1");
package.then((myCamera) => {
console.log(myCamera);
});
};
buyDSLRCamera();
package
에 then()
메서드를 써서 프로미스 체이닝을 해주면 Promise 객체
안에 담긴 내용물을 인자로 전달해주고 우리는 그것을 사용할 수 있다.
console.log()
의 결과로 뭔가 알 수없는 것들이 쏟아져 나왔지만 당황할 필요 없다. 중요한 것은 우리가 포장을 뜯었다는 것! 우리는 지금 택배 박스를 열고나서 카메라가 담긴 상품 박스를 꺼낸 것이다!
이제 우리가 카메라를 잘 사용한 다음에 다른 사람에게 다시 팔기 위해서는, then()
을 사용해서 넘겨줘야 한다.
const buyDSLRCamera = () => {
const package = fetch("https://jsonplaceholder.typicode.com/todos/1");
const anotherPackage = package.then((myCamera) => {
return myCamera.json();
});
};
buyDSLRCamera();
package.then((myCamera) => {...})
메서드는 myCamera.json()
의 값을 반환하고 있는데, 한 가지 기억해야 할 것은 then()
메서드는 항상 Promise 객체를 return 한다는 것이다.
즉 지금 anotherPackage
라는 변수에 담긴 값은 우리가 방금 위에서 본 긴 데이터를 다시 박스로 포장한 Promise 객체
가 된다.
const buyDSLRCamera = () => {
const package = fetch("https://jsonplaceholder.typicode.com/todos/1");
const anotherPackage = package.then((myCamera) => {
return myCamera.json();
});
anotherPackage.then((camera) => {
console.log(camera);
});
};
buyDSLRCamera();
이걸 이제 다시 then()
메서드를 통해 꺼내서 인자로 데이터를 받아오면 우리가 원하는대로 사용할 수 있게 되는 것이다.
즉, Promise 객체
는 택배 박스이고 우리는 then()
메서드를 통해서 택배 박스를 뜯은 동안에만, 다시 말하면 then()
메서드 안에서 작동하는 콜백 함수
를 통해서만 Promise 객체
를 다룰 수 있다는 것이다.
이를 변수에 저장하는 중간 과정을 거치지 않고, 함수의 반환값을 바로 다음 then()
으로 넘겨주도록 작성하면 아래의 코드와 같이 된다.
const buyDSLRCamera = () => {
const package = fetch("https://jsonplaceholder.typicode.com/todos/1")
.then((myCamera) => myCamera.json())
.then((camera) => {
console.log(camera);
});
};
buyDSLRCamera();
결국 이런 형태들이 우리가 주로 사용하게 되는
const fetchData = () => {
fetch("https://jsonplaceholder.typicode.com/todos/1")
.then((response) => response.json())
.then((data) => {
console.log(data);
});
};
fetchData();
의 형태로 코드가 구성되게 되는 것이다.
그림으로 정리해보자면 이런 느낌이 될 것 같다.
결국 Promise객체
는 then()
안에서만 활용할 수 있다는 것을 기억하면 될 것 같다.
원한다면 어떤 변수 안에 then()
메서드의 return값인 Promise 객체
를 저장할 순 있지만, 그것을 다시 사용하려면 다시 변수에 then()
메서드를 사용해야 하는 것이다.
Promise 객체
내부의 데이터 그 자체를 변수에 Promise 객체
로 감싸지 않고 할당하는 것이 아예 불가능한 것은 아니지만, Promise 객체
와 then()
의 이런 제한은 변수에 데이터 그 자체를 할당하는 것을 어렵게 만든다는 단점이 있었다.
변수에 데이터를 할당하려고 하면 언제나 그렇듯이 Promise 객체
가 들어가고, 이는 변수를 사용하려고 할 때 또 다른 then()
을 붙여야 함을 의미하기 때문이다.
즉, 오직 then()
의 내부에서만 데이터를 다룰 수 있었기 때문이다.
그리고 이런 불편한 상황을 타개하기 위해서
async, await 문법
이 등장하였다.