프로미스를 알아야 하는 이유
웹서버와 통신하는 로직을 만들고 싶다면
일정 시간 후에 동작하는 기능을 만들고 싶다면
서버에 요청한 데이터를 이용해서 사용자에게 더 나은 경험을 만들고 싶다면
웹페이지를 웹어플리케이션으로 만들고 싶다면
프로미스
앞선 이야기인 서버의 통신을 위한 관계와 약속을 알아봤습니다. 그렇다면 프론트엔드에서 이 통신을 어떻게 다루는지를 알아야 할 텐데요. 통신이란 것은 실행 시간이 정확히 얼마나 걸릴지 알 수 없는 작업이기 때문에, 프로그래머들은 프로미스를 만들었습니다.
#프로미스 너란 녀석
promise
Promise는 프로미스가 생성될 때 꼭 알 수 있지는 않은 값을 위한 대리자로, 비동기 연산이 종료된 이후의 결과값이나 실패 이유를 처리하기 위한 처리기를 연결할 수 있도록 합니다. 프로미스를 사용하면 비동기 메서드에서 마치 동기 메서드처럼 값을 반환할 수 있습니다. 다만 최종 결과를 반환하지는 않고, 대신 프로미스를 반환해서 미래의 어떤 시점에 결과를 제공합니다.
여기서 가장 중요한 것은 미래의 어떤 시점에 결과를 제공한다는 것입니다. 이를 한마디로 줄여서 이야기한다면 프로미스는'미랫값이라고 볼 수 있습니다.
예를 들어 봅시다. 커피집에 가서 딸기라떼를 주문을 하면, 직원은 진동벨을 넘겨줍니다. 이 진동벨이 바로 프로미스입니다.
이 진동벨은 미래에 내가 아메리카노를 받을 수 있을 것이라는 '약속' 입니다. 이 약속을 기다리면서 여러분은 다른 걸 할 수 있습니다. 친구와 카톡을 할 수도 있고, 게임을 할 수도 있고 또는 유튜브를 보고 있을 수도 있겠죠. 그리고 진동벨이 울리면 카운터로 가서 주문한 딸기라떼를 받아와 맛있게 딸기라뗴를 즐길 수 있습니다.
그런데 이런 상황이 발생할 수도 있습니다. 진동벨이 울려서 카운터에 갔더니 "죄송합니다, 손님. 오늘 딸기 재고가 다 떨어져서 딸기라떼 주문이 불가능합니다 " 라고 하는겁니다. 그러면 다른 메뉴를 고민하고 요청하던가 포기해야만 하죠.
이처럼 미랫값인 프로미스는 성공할 수도, 실패할 수도 있습니다.
이런 미랫값을 다루기 위한 프로미스가 등장하기 전에 사용하던 방법은 콜백이었습니다.
callback1(function(value1) {
callback2(value1, function(value2) {
callback3(value2, function(value3) {
callback4(value3, function(value4) {
callback5(value4, function(value5) {
// value5를 사용하는 처리
});
});
});
});
});
JavaScript
복사
콜백 함수는 비동기 데이터를 다루기에 간편하고, 오랜 시간 동안 표준적인 방법이었습니다. 하지만 서비스의 로직이 복잡해지고 여러 비동기 동작들이 필요한 시점부터 비동기 함수에서 다른 비동기 함수를 호출하고, 거기서 또 다른 비동기 함수를 호출하는 등의 중첩이 발생합니다. 이런 경우를 흔히 '콜백 지옥' 에 빠졌다고 합니다. 게다가 콜백의 연결은 에러 처리를 하는 경우에서도 상당히 곤란한 상황을 만들어냅니다.
try {
setTimeout(() => { throw new Error('Error!'); }, 1000);
} catch (e) {
console.log('에러를 캐치하지 못한다..');
console.log(e);
}
JavaScript
복사
try 블록 내에서 setTimeout 함수가 실행되면 1초 후에 콜백 함수가 실행되고 이 콜백 함수는 예외를 발생시킵니다. 하지만 이 예외는 catch 블록에서 catch되지 않습니다. 이미 try catch 문을 빠져나온 다음에 setTimeout 함수가 1초 뒤에 실행되기 때문입니다.
게다가 보통 비동기 함수들은 두 개의 콜백 함수를 전달받습니다. 요청이 성공한 경우에 실행할 콜백 함수와 오류가 발생했을 때 실행할 콜백 함수가 필요하기 때문입니다. 이로 인해 복잡성이 2배 이상 증가하죠.
그래서 등장한 프로미스
하지만 프로미스를 활용하면 콜백 함수 문제를 간편하게 만들 수 있습니다. 프로미스는 콜백 함수를 인자로 받는 대신에, 성공과 실패에 대응하는 메서드를 제공합니다. 그리고 콜백 함수를 중첩하지 않고, 여러 개의 비동기 동작을 연결할 수 있는 방법을 제공합니다.
프로미스는 비동기 작업을 전달받아서 응답에 따라 두 가지 메서드 중 하나를 호출하는 객체입니다. 프라미스는 비동기 작업이 성공하거나 충족된 경우 then() 메서드에 결과를 넘겨줍니다. 비동기 작업에 실패하거나 거부되는 경우에는 catch() 메서드를 호출합니다. then()과 catch() 메서드에는 모두 함수를 인자로 전달합니다. 이때 두 메서드에 전달되는 함수에는 비동기 작업의 결과인 응답만이 인수로 전달됩니다.
프로미스는 두 개의 인자, resolve() 와 reject() 를 전달받습니다. resolve()는 코드가 정상적으로 동작했을 때 실행됩니다. resolve()가 호출되면 then() 메서드에 전달된 함수가 실행됩니다. 프로미스를 설정할 때 then()과 catch() 메서드를 모두 사용할 수 있습니다. then() 메서드는 성공한 경우를 처리하고, catch() 메서드는 거절된 경우를 처리합니다.
new Promise(function(resolve, reject) {
// ...
}).then(resolve()).catch(reject())
JavaScript
복사
then()을 연속으로 사용하면, 여러 개의 중첩된 콜백 함수에 데이터를 전달하는 대신 여러 개의 then() 메서드를 통해 데이터를 아래로 내려줄 수 있습니다. 그리고 프로미스를 반환하므로, 암묵적인 반환을 이용하는 화살표 함수로 모든 코드를 한 줄로 만들 수 있습니다.
getUserPreferences()
.then(preference => getMusic(preferenc.theme))
.then(music => { console.log(music.album) })
JavaScript
복사
여기서 프로미스를 연결하는 경우에는 catch() 메서드를 개별적으로 연결할 필요가 없습니다. catch() 메서드를 하나만 정의해서 프로미스가 거절되는 모든 경우를 처리할 수 있습니다. 이제 비동기 작업을 처리할 수 있는 도구를 다룰 수 있게 되었습니다. 이런 프로미스가 가장 많이 사용되는 상황 중 하나는 바로 API에서 데이터를 가져오는 경우입니다. 그렇다면 이 프로미스를 활용해 자바스크립트에서 데이터를 가져오는 fetch에 대해 알아봅시다.
대화하고 싶은 fetch
규모가 있는 자바스크립트 앱을 개발하는 경우에는 필연적으로 API를 다루게 됩니다. API를 이용하면 페이지가 아닌 데이터를 가져올 수 있어서, 화면을 새로 고침 하지 않아도 요소를 갱신할 수 있습니다. 즉 문서를 애플리케이션으로 만들 수 있다는 것이죠.
최근 프론트엔드의 트렌드 중 하나인 단일 페이지 웹앱(Single Page Application)은 자바스크립트가 인기를 끄는 이유 중 하나이지만, Ajax로 데이터를 가져오는 작업은 오랜 시간 동안 꽤 번거로운 방법이었습니다. 그래서 오랜시간 개발자들은 복잡도를 낮춘 jQuery와 같은 라이브러리를 사용했는데요. 개발자들은 문제를 해결하는 사람들이다 보니, 기존의 복잡도를 낮춘fetch라는 도구를 만들어냈습니다.
fetch하고 싶다면
fetch()를 사용하려면 API 끝점(엔드 포인트)이 필요합니다. 이 엔드포인트를 간단하게 다루기 위해 jsonplaceholder (opens new window)라는 서비스를 이용할 겁니다. 이 서비스는 가상의 블로그 데이터를 제공하고 있습니다.
먼저 요청할 내용은 간단한 GET 요청입니다. 데이터를 가져오는 fetch() 호출은 간단한데요. 아래와 같이 끝점 url을 인자로 해서 fetch()를 호출하면 됩니다.
fetch('https://jsonplaceholder.typicode.com/posts/1')
JavaScript
복사
위 요청을 하면 응답 본문은 블로그 게시물에 대한 정보를 담고 있습니다.
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provid...",
"body": "quia et suscipit\nsuscipit rec..."
}
JavaScript
복사
참 쉽죠?
요청을 보내고 나면 fetch()는 응답을 처리하는 프로미스를 반환합니다. 이어서 해야 할 작업은 then() 메서드에 응답을 처리하는 콜백함수를 추가하는 것입니다. 사실 우리가 필요한 것은 응답 본문인 경우가 대부분입니다. 그렇지만 응답 객체는 본문 외에도 상태 코드, 헤더 등 다양한 정보를 가지고 있습니다. 그리고 그 형태가 자바스크립트에서 일반적으로 다루는 JSON 형태가 아닐 수 있습니다. 그래서 json()이라는 메서드를 호출해 응답객체를 JSON으로 변환할 수 있습니다.
이때 주의할 점은 json()메서드도 프로미스를 반환하기 때문에 then() 메서드를 추가해야 합니다. 추가한 then() 메서드에서 콜백에서 파싱된 데이터를 처리할 수 있습니다. 예를 들어 블로그 게시물 중 제목만 필요한 경우에 아래와 같이 사용할 수 있습니다.
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(data => {
return data.json
})
.then(post => {
console.log(post.title)
})
JavaScript
복사
그런데 한 가지 안타까운 사실이 있습니다. fetch() 프로미스는 상태 코드가 40x 에러로 인해 요청에 실패한 경우에도 응답 본문을 반환합니다. 즉 요청이 실패하는 경우를 catch 메서드만으로 처리할 수 없다는 것인데요. 응답에는 응답 코드가 200에서 299 사이인 경우 true로 설정되는 ok라는 필드가 있습니다. 이 필드를 이용해서 응답을 확인하고, 오류가 있는 경우 오류 처리를 할 수 있도록 할 수 있습니다.
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(data => {
if (!data.ok) {
throw new Error(data.status)
}
return data.json()
})
.then(post => {
console.log(post.title)
})
.catch(error => {
console.log(error)
})
JavaScript
복사
이제 새로운 블로그 게시물을 추가하는 API를 활용해볼까요? POST 요청은 GET 요청과는 다르게, 두 번째 인자로 조건을 담은 객체를 전달해야 합니다. 먼저, POST 요청을 보내기 때문에 POST 메서드를 사용한다고 선언해야 합니다. 그리고 새로운 블로그 게시물을 생성하는 JSON 데이터를 넘겨줘야 합니다. 그리고 JSON 데이터를 보내기 때문에 헤더의 Content-Type을 application/json으로 설정해야 합니다. 마지막으로 JSON 데이터를 담은 문자열로 요청 본문을 추가해야 합니다.
const newPost = {
title: 'jun',
body: 'jun is cool boy',
userId: '1'
}
const option = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(newPost)
}
fetch('https://jsonplaceholder.typicode.com/posts/1', option)
.then(data => {
if (!data.ok) {
throw new Error(data.status)
}
return data.json()
})
.then(post => {
console.log(post.title)
})
.catch(error => {
console.log(error)
})
JavaScript
복사
Async Await
이런 프로미스는 async/await을 이용해서 더욱 편하게 다룰 수 있게 발전했습니다. 프로미스는 콜백과 비교하면 엄청난 발전이지만, 인터페이스가 여전히 다소 투박합니다. 그리고 프로미스를 사용해도 여전히 메서드에서 콜백을 다뤄야 합니다. 다행히 언어는 계속 발전합니다.
async 키워드를 이용해서 선언한 함수는 비동기 데이터를 사용한다는 것을 의미합니다. 비동기 함수의 내부에서 await 키워드를 사용하면 값이 반환될 때까지 함수의 실행을 중지시킬 수 있습니다.
async function getUser() {
cosnst eastjun = await getTargetUser();
return eastjun;
}
JavaScript
복사
비동기 함수의 재밌는 점은 프로미스로 변환된다는 것입니다. 즉 getUser()을 호출하는 경우 then() 메서드가 필요합니다. 뭔가 크게 달라진 것 같지 않을 수 있는데, async 함수가 빛나는 때는 여러 개의 프로미스를 다룰 때입니다.
async function getArtistPreference() {
const theme = await getUSerPreferences()
const album = await getMusic(theme)
const artist = await getArtist(album)
return artist
}
getArtistPreference()
.then(artist => console.log(artist))
.catch(error => console.error(error))
JavaScript
복사
정리
프로미스는 어떤 시점에 결과를 제공하는 미랫값이다.
프로미스는 성공과 실패에 대응하는 메서드를 제공하고, 비동기 작업을 전달받아서 응답에 따라 두 가지 메서드 중 하나를 호출한다.
프로미스는 두 개의 인자 resolve()와 reject()를 전달받는다.
fetch는 네트워크 통신을 포함한 리소스 최득을 위한 인터페이스가 정의된 API이다.
fetch()를 불러들이는 경우 엔드포인트를 지정해야 한다.
fetch()는 Promise를 리턴한다.
async/awiat을 이용하면 프로미스를 훨씬 깔끔하게 다룰 수 있다.