7.16 Dev.Feedback ( 비동기적 프로그래밍 # B Fetch, Async)
Fetch
실제 코딩을 할 때는 Promise를 직접 생성해서 리턴해주는 코드 보다는 어떤 라이브러리의 함수를 호출해서 리턴 받은 Promise 객체를 사용하는 경우가 더 많다. 그 중 대표적인 상황이 REST API를 호출할 때 사용하는 브라우저 내장 함수인 fetch()이다.
fetch() 함수는 API의 URL을 인자로 받고, 미래 시점에 얻게될 API 호출 결과를 Promise 객체로 리턴한다. network latency 때문에 바로 결과값을 얻을 수 없는 상황이므로 이전 포스트에서 설명한 Promise의 사용 목적에 정확히 부합한다.
Promise 객체의 then() 메소드는 결과값을 가지고 수행할 로직을 담은 콜백 함수를 인자로 받는다. 그리고 catch() 메서드는 예외 처리 로직을 담은 콜백 함수를 인자로 받는다.
이해를 위해 randomuser.me API로부터 데이터를 받아오는 것으로 예시를 들어보려 한다.
위 API에는 실제 존재하지 않는 사람에 대한 e-mail,이름,전화번호,집 주소 등의 정보가 들어있어 dummy 데이터로 사용하기 좋다.
fetch("https://api.randomuser.me/?nat=US&results=1").then((res) =>
console.log(res.json())
);
콘솔 창을 보면, 대기 중인 프로미스를 볼 수 있다. 대기 중인 프로미스는 데이터가 도착하기 전의 상태를 표현하는 것이다. .then()이라는 함수를 대기 중인 프로미스에 연쇄 호출하면, 앞에 있는 프로미스가 성공하면(즉 데이터를 받아오면) 콜백이 호출된다.
즉 fetch() 함수는 데이터를 받아오고, then() 함수는 데이터가 도착하면 그 데이터를 가지고 우리가 원하는 기능을 구현하는 것이다.
위의 코드에서 하고자 하는 것은 받은 데이터를 JSON으로 바꾸는 것이다.
then은 프로미스가 정상적으로 완료되면 콜백 함수를 한 번만 호출한다. 이 콜백 함수가 반환하는 값은 그 다음에 오는 then 함수의 콜백에 전달되는 인자가 된다. 따라서 성공적으로 처리가 된 프로미스가 있다면, 그 값을 then 함수를 통해 받아 연쇄적으로 호출할 수 있는 것이다.
fetch("https://api.randomuser.me/?nat=US&results=1")
.then((res) => res.json())
.then((json) => json.results)
.then(console.log)
.catch(console.error);
위의 코드를 마지막으로 다시 한 번 정리를 해보면
우선 fetch를 호출하여 randomuser.me에 대한 GET 요청을 보낸다. GET 요청이 성공해 데이터를 받아오면 응답 본문을 JSON으로 변환한다. 그 후 JSON 데이터 중에서 results를 얻는다. 그 후 콘솔에 results의 값을 출력한다. 마지막에 있는 catch 함수는 fetch가 성공하지 못한 경우 콜백을 호출해준다. 여기서 마지막에 있는 catch 함수는 에러가 없기에 작동하지 않는다.
Async Await
async await을 설명하기 위해 또 다른 dummydata용 API를 가져왔다. https://jsonplaceholder.typicode.com/
JSONPlaceholder - Free Fake REST API
{JSON} Placeholder Free fake API for testing and prototyping. Powered by JSON Server + LowDB As of Dec 2020, serving ~1.8 billion requests each month.
jsonplaceholder.typicode.com
위의 가상 데이터에는 6가지의 기본 리소스가 있다.
/posts 100 posts
/comments 500 comments
/albums 100 albums
/photos 5000 photos
/todos 200 todos
/users 10 users
위를 통해 저자의 이메일을 알아내는 기능을 구현하고자 했다.
function fetchAuthorEmail(postId) {
return fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`)
.then((response) => response.json())
.then((post) => post.userId)
.then((userId) => {
return fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
.then((response) => response.json())
.then((user) => user.email);
});
}
fetchAuthorEmail(1).then((email) => console.log("email:", email));
기존 fetch 함수를 통해 구현한 저자의 이메일 주소 찾아내기
위와 같이 저자의 이메일을 찾아내기 위해 코드를 작성할 수 있는데, Promise 방식에도 문제가 나타나게 됐다.
디버깅의 어려움
.then(user => user1.email);
위 코드의 8번째 줄을 변경하면, 아래와 같은 오류 코드를 보게 된다.
ReferenceError: user1 is not defined
at fetch.then.then.then.then.then (<anonymous>:7:29)
동일한 이름의 메서드인 then()을 연쇄적으로 호출하고 있어서 도대체 몇 번째 then()에서 문제가 발생한 건지 Stack Trace을 보더라도 혼란스러울 수 있고, 또한 전체 코드의 양이 많아지면 인식이 힘들어질 수도 있다. 또한 then() 메서드 호출 부에 break point를 걸고 디버거를 돌리면, 위 코드와 같이 화살표 함수로 한 줄짜리 콜백 함수를 넘긴 경우에는 코드 실행이 break point에서 멈추지 않기 때문에 디버깅이 상당히 불편하다.
예외 처리의 어려움
Promise를 사용하면 try/catch 대신에 catch() 메서드를 사용하여 예외 처리를 해야한다. 이 부분이 비동기 코드만 있을 때는 그렇게 거슬리지 않는데, 동기 코드와 비동기 코드가 섞여 있을 경우 예외 처리가 난해해지거나 예외 처리를 누락하는 경우가 생기기 쉽다.
들여쓰기로 인한 가독성 하락
실제 프로젝트에서는 샘플 코드와 같이 간단한 구조가 아닌 복잡한 구조의 비동기 처리 코드를 작성하게 된다. 따라서, then() 메서드의 인자로 넘기는 콜백 함수 내에서 조건문이나 반복문을 사용하거나 여러 개의 Promise를 병렬로 또는 중첩해서 호출해야하는 경우들이 발생하게 된다. 이럴 경우, 다단계 들여쓰기를 해야할 확률이 높아지며 코드 가독성은 점점 떨어지게 된다.
이를 해결하기 위해 나온 것이 async await이다. async를 사용하는 코드는 동기적인 방식의 코드와 비슷하다. async 함수는 then 함수를 연쇄 호출해 프로미스의 결과를 기다리는 대신 프로미스 다음에 있는 코드를 실행하기 전에 프로미스가 끝날 때까지 기다리라고 명령할 수 있다.
async function fetchAuthorEmail(postId) {
const postResponse = await fetch(
`https://jsonplaceholder.typicode.com/posts/${postId}`
);
const post = await postResponse.json();
const userId = post.userId;
const userResponse = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
const user = await userResponse.json();
return user.email;
}
fetchAuthorEmail(1).then((email) => console.log("email:", email));
위의 코드를 async/await으로 다시 작성한 코드이다. 동일한 결과값을 내지만, 적는 형식이 달라진 것을 볼 수 있다.
우선 함수 선언부 앞에 async가 붙었다. 이 async 키워드는 fetchAuthorEmail 함수를 비동기 함수로 만들어주는 역할을 한다. await 키워드는 async 키워드가 붙어있는 함수 내부에서만 사용할 수 있다. await 키워드는 비동기 함수가 리턴하는 Promise로 부터 결과값을 추출해준다. 즉, await 키워드를 사용하면 일반 비동기 처리처럼 바로 실행이 다음 라인으로 넘어가는 것이 아니라 결과값을 얻을 수 있을 때까지 기다려주는 것이다.
따라서 async/await을 사용하면, 비동기 함수를 일반적인 동기 코드 처리와 동일한 흐름으로 (함수 호출 후 결과값을 변수에 할당하는 식으로) 코드를 작성할 수 있으며, 따라서 코드를 읽기도 한결 수월해지게 된다.
한가지 주의할 점은 async 키워드가 붙어있는 함수를 호출하면 명시적으로 Promise 객체를 생성하여 리턴하지 않아도 Promise 객체가 리턴된다는 것. 따라서 호출부를 보면 Promise 객체를 사용했던 것과 동일한 방식으로 then() 메서드를 통해서 결과값을 출력하고 있다.
하지만 만약 이 호출부가 또 다른 async 키워드가 붙어있는 함수의 내부에 있다면 동일한 방식으로 await 키워드를 사용하여 Promise가 제공할 값에 바로 접근할 수 있다.
async function printAuthorEmail(postId) {
const email = await fetchAuthorEmail(postId);
console.log("email:", email);
}
printAuthorEmail(1);
또한 앞서 말했던 try/catch 문을 동기 비동기 구분 없이 일관되게 사용하여 예외 처리를 깔끔하게 해줄 수 있다는 것도 하나의 장점이다.
async function fetchAuthorEmail(postId) {
const postResponse = await fetch(
`https://jsonplaceholder.typicode.com/posts/${postId}`
);
const post = await postResponse.json();
const userId = post.userId;
try {
const userResponse = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
const user = await userResponse.json();
return user.email;
} catch (err) {
console.log("Fail to fetch user:", err);
return "Unknown";
}
}
fetchAuthorEmail(1).then((email) => console.log("email:", email));
최근 비동기 함수 코드를 작성하는데 있어 가장 우선적으로 고려되는 방식이 async/await이라는 말을 들었다.
이제는 코드 작성을 할 때 최대한 async/await 방식에 알맞게 작성을 하여 취업을 했을 때 문제가 생기지 않게 해야겠다.