자바스크립트에서는 데이터를 가져올 때 거의 비동기적으로 처리를 한다.
자바스크립트는 싱글 스레드로 작동하기 때문에 위에서부터 아래로 동작을 순서대로 처리하는데 비동기식 처리는 작업을 병렬적으로 처리하는 방법이다.
비동기처리를 하는 방법이 여러가지라 헷갈렸는데 대표적인 내용 위주로 비교적 간단하게 정리해 놓으려고 한다.
📍추가 - 비동기 함수의 처리과정
자바스크립트 엔진은 하나의 실행컨텍스트(콜 스택)를 가지고 있고 함수가 호출되면 콜 스택에 쌓인다. 이 때 비동기 함수의 콜백함수는 Web API의 타이머함수에 의해 브라우저 환경이 가지고있는 태스크 큐에 쌓이고 바로 동작하지 않고 대기하다가 콜 스택이 비워졌을 때 브라우저 환경 내의 이벤트 루프에 의해 콜 스택으로 이동하면서 실행된다.(이벤트 루프가 자바스크립트의 동시성을 지원하고 있음.) 자바스크립트 엔진은 싱글 스레드로 동작하지만 이처럼 브라우저가 멀티 스레드로 동작하기 때문에 병행처리되어 비동기 함수를 처리하게 된다.
- 비동기 함수는 콜백 헬이 생기고 에러 처리가 어렵다.
Promise는 new연산자와 함께 호출하여 프로미스 객체를 생성한다.
callback함수를 많이 사용하게 되는 경우 코드가 복잡해지기 때문에 만들어졌다.
프로미스를 사용하면 비동기 처리에서 곤란했던 부분들에 대한 처리가 쉬워진다.
📍콜백 헬 - 콜백 헬이 생기는 이유
비동기 함수는 콜백 함수의 처리 결과를 외부로 반환하거나 상위 스코프의 변수에 할당하지 못한다. (비동기 함수를 호출하면 함수 내부의 비동기로 동작하는 코드가 완료되지 않았다 해도 기다리지 않고 즉시 종료된다. 때문에 비동기 함수 내부의 코드는 비동기 함수가 종료된 이후에 완료된다.)
=> 비동기 함수의 처리결과에 대한 후속 처리는 비동기 함수 내부에서 수행해야 하는데 비동기 처리 결과를 가지고 또다시 비동기 함수를 호출해야 한다면 콜백 함수 호출이 중첩되어 복잡도가 높아지고 가독성이 떨어지게 된다. 이것을 콜백 헬이라고 한다.
promise는 다음 중 하나의 상태를 가진다.
데이터를 제공하는 producer
프로미스 객체를 만드는 순간 pending(진행)상태에 들어간다.
프로미스 생성자 함수는 비동기 처리를 수행할 콜백함수를 인수로 전달받는데
resolve
(완료 상태)와 reject
(거부,에러 상태) 를 이용한다.
프로미스 함수가 만들어진 순간, 이 executor함수가 자동적으로 실행이 된다.
데이터를 소비하는 consumer
then
, catch
를 사용해서 값을 받아오고 에러를 캐치할 수 있다.
finally
뒤에 작성하는 내용은 성공, 실패 여부와 상관없이 무조건적으로 호출이 된다.
then은 값을 바로 전달할수도 있고, promise를 전달해도 된다.
이 메서드들은 언제나 프로미스를 반환하므로 연속적으로 호출할 수 있다. (체이닝)
2초 후 값을 불러오는 예제.
const promise = new Promise((resolve, reject) => {
console.log('doing something...');
setTimeout(() => {
resolve('ellie');
}, 2000);
});
promise.then(value => { console.log(value); })
두 개 이상의 비동기 작업을 순차적으로 실행해야 할 때 (순차적으로 각각의 작업이 이전 단계 비동기 작업이 성공하고 나서 그 결과값을 이용하여 다음 비동기 작업을 실행해야 하는 경우) promise chain을 이용하여 해결할 수 있다.
콜백 함수들을 반환된 promise에 chain을 형성하도록 추가해서 콜백 피라미드를 피해서 깔끔하게 작성할 수 있다. 작업이 실패한 후에도 (error catch) 새로운 작업을 수행하는 것이 가능하다.
const one = Promise.resolve('1');
const two = new Promise((resolve) =>
setTimeout(() => {
resolve('2');
}, 2000)
);
const three = Promise.resolve('3');
one.then((oneRes) => {
console.log(oneRes); //1을 반환
return two;
})
.then((twoRes) => {
console.log(twoRes); //2를 반환
return three;
})
.catch(error => {
console.log(error); // 에러를 콘솔에 띄운다. 에러캐치 후에 다시 then으로 체이닝을 한다.
})
.then((threeRes) => {
console.log(threeRes); //3을 반환
})
.finally(() => { //finally: 무조건 마지막에 호출되는
console.log('END')
})
getCharacterData라는 함수안에서 Promise를 생성하고 요청이 성공했을 때와 실패했을때 수행될 작업을 지정해준다.
const getCharacterData = () => {
return new Promise((resolve, reject) => {
const error = false;
// const error = true;
if(!error) {
setTimeout(() => {
resolve({ id: 1, name: 'Rick Sanchez' });
}, 1000);
} else {
reject('Error getting data');
}
})
}
getCharacterData()
.then(response => console.log(response))
.catch(error => console.log(error))
async와 await을 사용하면 Promise를 좀 더 깔끔하게 사용할 수 있다. (프로미스의 후속 처리 메서드 없이 비동기를 동기 처리처럼 결과반환을 할 수 있다.) async
함수는 기본적으로 Promise를 반환한다.
async function foo(){
return 1
}
위 코드는 아래와 동일하다.
function foo(){
return new Promise((resolve, reject) => {
resolve(1)
})
}
await
는 async
가 붙은 함수 안에서 사용한다.
await연산자는 Promise를 기다리기 위해 사용한다. Promise가 await에 넘겨지면, await은 Promise가 이행되기를 기다렸다가 해당 값을 리턴한다.
async function foo(){
await 1
}
위 코드는 아래와 동일하다.
function foo(){
return Promise.resolve(1)
}
함수 앞에 붙은 async가 Promise.resolve와 비슷하게 작용하고 await이 then과 비슷한 작용을 한다고 보면 된다.
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function getApple() {
await delay(3000);
return ‘apple’;
}
async, await가 무조건적으로 좋은것은 아니고 처음에 async로 만들었던 함수를 사용하는 곳에서는 항상 async, await를 붙여줘야 하는 번거로움도 생긴다.
하지만 then체이닝을 줄줄히 하지 않아서 깔끔하고( then체이닝은 return으로 스코프를 유지해야 하므로.) 함수내부에서 자유롭게 스코프를 이용할 수 있는 장점이 있다. 그러므로 상황에 맞게 잘 선택해서 사용하는게 가장 좋다.
📍추가 - 에러 처리가 곤란한 이유 (promise가 나오기 이전에)
에러는 호출자 방향으로 전파되는데 비동기 함수의 콜백 함수를 호출한 것은 비동기 함수 자체가 아니므로 (ex- setTimeout은 콜백 함수가 호출되는 것을 기다리지 않고 즉시 종료된다. 비동기 함수의 콜백 함수가 실행될 때 이미 콜 스택에서 제거된 상태.) catch로 캐치되지 않는다.
axios
는 Javascript를 통해 직접 요청을 보내기 위해 사용되는 라이브러리이다. GET 메소드로 요청을 보내기 위해 axios.get()
함수를 사용할 수 있고 이 때 반환되는 것은 Promise객체이다.
axios 사용하기 -> https://axios-http.com/kr/docs/intro
앞서 Promise객체를 만들어서 처리를 하는것과 형식만 약간 다르고 구체적인것은 사이트를 참고하면 쉽게 사용할 수 있다.
const getBtn = document.getElementById('get-btn');
const postBtn = document.getElementById('post-btn');
const getData = () => {
axios.get(`https://newsapi.org/v2/top-headlines/sources?apiKey=${API_KEY}`)
.then(response => {
console.log(response);
});
}
const sendData = () => {
//post = send
axios.post('https://reqres.in/api/register',{
email: 'eve.holt@reqres.in',
password: 'pistol'
})
//success
.then(response => {
console.log(response);
})
//error handling
.catch(err => {
console.log(err, err.response);
// err.response로 어느부분에서 에러가 있는지 콘솔에서 볼수가 있다. 이경우는 패스워드가 없기 때문에 나타나는 에러 -> data: {error: 'Missing password'}
});
};
getBtn.addEventListener('click', getData);
postBtn.addEventListener('click', sendData);
axios.get()
로 데이터를 요청하고 axios.post()
로 데이터를 보낸다.
성공했을 때, 실패시 작성법은 동일하다.
브라우저에 내장된 fetch()
함수를 이용하여 데이터를 요청할 수 있다.
fetch() 함수는 첫번째 인자로 URL, 두번째 인자로 옵션 객체를 받고, Promise 타입의 객체를 반환한다. 반환된 객체는, API 호출이 성공했을 경우에는 응답(response) 객체를 resolve하고, 실패했을 경우에는 예외(error) 객체를 reject한다.
const url = 'https://rickandmortyapi.com/api/character/1';
fetch(url)
.then((response) => {
console.log('response.ok:', response.ok);
if(!response.ok){
throw 'Error';
}
return response.json(); //fetch api는 가져올때 JSON메서드를 호출해야 한다.
})
.then((data) => {
console.log('data:', data);
})
.catch(error => {
console.log('error:', error);
})
POST방식으로 데이터를 생성할때는 method
옵션을 POST로 지정해주고, headers
옵션을 통해 JSON 포멧을 사용한다고 알려준다, 요청 전문은 JSON 포멧으로 body
옵션에 작성해주면 된다.
fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "Test",
body: "I am testing!",
userId: 1,
}),
})
.then((response) => response.json())
.then((data) => console.log(data));