자바스크립트는 동기적(Synchronous)으로 작동합니다. 동기적 작동이란, 코드가 순차적으로 실행되는 것을 말합니다. 반대로 비동기(Asynchronous)는 코드가 순차적으로 실행되지 않는 것을 말합니다.
console.log(1);
setTimeout(()=>{console.log(2)} , 1000); //1초 뒤 코드 실행
console.log(3);
//1
//3
//2
setTimeout 이외에도 비동기를 다루는 기술과 비동기 코드를 동기식으로 작동하게 하는 방법들에 대해 알아보겠습니다.
비동기 코드를 동기식으로 작동하는 것처럼 하기 위해 콜백을 사용합니다. 콜백
이란, 함수의 매개변수로 또 다른 함수를 넣는 것을 말합니다.
콜백 패턴은 다음과 같습니다. 다음은 비동기식으로 작동하는 코드입니다.
const a = () => {
setTimeout(()=>{
console.log(1);
} , 1000)
}
const b = () => {
console.log(2);
}
a();
b();
//2
//1
이 코드를 동기식으로 작동시키기 위해 콜백 함수를 사용해 보겠습니다.
const a = (callback) => {
setTimeout(()=>{
console.log(1);
callback();
} , 1000)
}
const b = () => {
console.log(2);
}
a(() => {
b();
});
//1
//2
a함수의 매개변수로 콜백 함수를 받고 setTimeout 안에서 콜백 함수를 실행시켰습니다. 이제는 동기적으로 코드가 실행되고 있습니다.
만약 동기적으로 처리하고 싶은 코드가 더 많다면 콜백 함수는 더 많아질 것입니다.
const a = (callback) => {
setTimeout(()=>{
console.log(1);
callback();
} , 1000)
}
const b = (callback) => {
setTimeout(()=>{
console.log(2);
callback();
} ,
}
const c = () => {
console.log(3);
}
a(() => {
b(() => {
c();
});
});
//1
//2
//3
이렇게 콜백으로 여로 코드 블록을 차례대로 연결해 나갈 때 발생하는 상황을 콜백 지옥
이라고 부릅니다. 과도한 함수 중첩으로 코드가 굉장히 복잡해집니다.
이런 콜백 지옥을 벗어나기위해 프로미스
를 사용합니다.
프로미스
는 비동기 코드를 동기적으로 처리하기 위해 사용하는 클래스로, 비동기 작업의 최종 성공 또는 실패를 나타내는 객체를 만들어 사용합니다.
프로미스 클래스로 객체를 만들어 봅시다.
const promise = new Promise((resolve, reject) => {
...
})
프로미스의 성공을 알리기 위해서는 resolve
를, 실패를 알리기 위해서는 reject
를 호출하면 됩니다.
위에서 다뤘던 콜백 패턴을 프로미스를 이용해 개선해 보겠습니다.
const a = () => {
return new Promise((resolve) => {
setTimeout(()=>{
console.log(1);
resolve();
} , 1000)
})
}
const b = () => {
console.log(2);
}
a().then(() => {
b()
})
//1
//2
프로미스 객체를 생성해서 매개변수로 콜백함수를 넣어줍니다. 콜백 함수의 매개변수는 resolve를 받고있고, 함수 내 비동기 코드가 실행됩니다.
a함수가 프로미스 객체를 return하기 때문에 프로미스의 then()
메서드를 사용할 수 있습니다. then 안에는 비동기 코드가 성공했을 때 실행 할 콜백 함수를 넣어주었습니다. 콜백 함수는 resolve 매개변수로 들어가서 resolve가 호출될 때 실행됩니다.
프로미스를 이용하면 콜백 지옥을 다음과 같이 개선할 수 있습니다.
const a = () => {
return new Promise((resolve) => {
setTimeout(()=>{
console.log(1);
resolve();
} , 1000)
})
}
const b = () => {
return new Promise((resolve) => {
setTimeout(()=>{
console.log(2);
resolve();
} , 1000)
})
}
const c = () => {
return new Promise((resolve) => {
setTimeout(()=>{
console.log(3);
resolve();
} , 1000)
})
}
const d = () => {
console.log(4);
}
a().then(() => {
b().then(() => {
c().then(() => {
d();
})
})
})
//1
//2
//3
//4
그러나 위의 코드는 콜백 지옥 패턴과 다를 바 없어보입니다. 프로미스는 이를 해결하기 위해 프로미스 체이닝
을 제공합니다.
프로미스의 성공 또는 실패 여부와 무관하게 이전 프로미스에서 반환된 것을 다음 프로미스의 기반으로 사용하여 프로미스를 계속 체이닝(연결)할 수 있습니다.
a().then(() => {
return b()
}).then(() => {
return c()
}).then(() => {
d()
}).catch(err=>{
console.error(err);
})
프로미스가 성공한 경우뿐만 아니라 실패한 경우에도 연쇄적으로 원하는 만큼 많은 프로미스를 연결할 수 있으며, 콜백 지옥 패턴보다 더 읽기 쉽고 간결합니다.
다음은 일반 함수의 콜백으로 에러 핸들링한 코드입니다.
const a = (index, callback, errorCallback) => {
setTimeout(() => {
if(index > 10){
errorCallback(`index는 ${index}보다 클 수 없습니다.`);
return;
}
console.log(index);
callback(index + 1);
} , 1000)
}
a(
13,
res => console.log(res)
err => console.error(err)
)
//index는 13보다 클 수 없습니다.
프로미스를 이용하여 에러 핸들링을 다뤄보겠습니다.
const a = (index) => {
return new Promise ((resolve, reject)=>{
setTimeout(() => {
if(index > 10){
reject(`index는 ${index}보다 클 수 없습니다.`);
return;
}
console.log(index);
resolve(index + 1);
} , 1000)
})
}
a(13)
.then(res => console.log(res))
.catch(err => console.error(err))
//index는 13보다 클 수 없습니다.
프로미스가 성공할 때의 값을 얻는 데에 then()
을 사용하고, 프로미스가 실패 할 때의 오류를 처리하는 데에는 catch()
를 사용합니다. then() 안의 콜백 함수는 resolve 매개변수에 들어가고, catch() 안의 콜백 함수는 reject 매개변수에 들어가게 됩니다.
프로미스의 성공과 실패 상관없이 항상 실행되는 메서드 입니다.
a(13)
.then(res => console.log(res))
.catch(err => console.error(err))
.finally(() => console.log('done!'))
Promise.all()은 모든 프로미스가 성공할 경우에만 성공하는 하나의 프로미스를 반환합니다. 프로미스 중 하나가 실패하면 다른 모든 프로미스가 성공하더라도 실패한 결과를 반환합니다.
const promise1 = new Promise((resolve, reject) => {
resolve("first");
});
const promise2 = new Promise((resolve, reject) => {
reject(Error("error!"));
});
Promise.all([primise1, promise2])
.then(data => {
console.log(data);
})
.catch(err => {
console.log(err);
})
// Error: error!
ES2017에서는 새로운 프로미스 작업 방식을 위해 async/await
키워드를 도입했습니다. 위에서 봤던 일반적인 프로미스 작업 방식을 async/await 문법을 통해 다시 작성해 보겠습니다.
const a = () => {
return new Promise((resolve) => {
setTimeout(()=>{
console.log(1);
resolve();
} , 1000)
})
}
const b = () => {
console.log(2);
}
const wrap = async () => {
await a();
b();
}
wrap();
//1
//2
프로미스 객체를 반환하는 함수 앞에 await를 써서 작업이 끝날 때까지 기다립니다. 그다음 b함수를 호출합니다. 주의할 점은, await키워드는 비동기 함수 내에서만 작동합니다. 비동기 함수를 만드려면 함수앞에 async 키워드를 넣으면 됩니다. 따라서 async와 await는 항상 같이 사용해야 합니다.
프로미스의 reject를 사용한 에러 핸들링을 async와 await를 이용해서 작성해보겠습니다.
const a = (index) => {
return new Promise ((resolve, reject)=>{
setTimeout(() => {
if(index > 10){
reject(`index는 ${index}보다 클 수 없습니다.`);
return;
}
console.log(index);
resolve(index + 1);
} , 1000)
})
}
//프로미스 이용한 에러 핸들링
a(13)
.then(res => console.log(res))
.catch(err => console.error(err))
//async와 await를 이용한 에러 핸들링
const wrap = async() => {
try{
const res = await a(13);
console.log(res);
}catch(err){
console.error(err);
}finally{
console.log('done!');
}
}
wrap();
//index는 13보다 클 수 없습니다.
//index는 13보다 클 수 없습니다.
//done!
try-catch 구문을 이용해서 에러 핸들링을 할 수 있습니다. resolve 함수가 호출되는 경우에는 try 안의 코드가 실행되고, reject 함수가 호출되면 catch 안의 코드가 실행됩니다.
fetch는 비동기 함수로, 네트워크를 통해 url에 요청을 보낸 뒤 웅답으로 데이터를 받아올 수 있습니다. fetch는 프로미스 객체를 반환하므로 프로미스의 then()과 catch()를 사용할 수 있습니다.
const data = fetch('api-url')
.then(res => res.json())
.then(res => console.log(res));
console.log(data);
fetch가 호출된 직후에 있는 console.log(data) 에는 undefined가 출력됩니다. fetch는 비동적기적으로 수행되기 때문에 fetch가 완료될 때까지 코드 실행을 멈추는게 아니라 계속해서 다음 코드를 실행합니다.
반복문 안에서 비동기를 동기식으로 작동하게 하려면 forEach문을 쓰면 안되고 for문을 써야합니다.
const dataAPI = (name) => {
return new Promise(resolve => {
fetch(`api-url?name=${name}`)
.then(res => res.json())
.then(res => resolve(res));
})
}
const names = ['사과', '딸기', '바나나'];
//순서 보장 안됨
names.forEach(async(name)=>{
const res = await dataAPI(name);
console.log(name, res);
})
//순서 보장 됨
const wrap = async() => {
for(name of names){
const res = await dataAPI(name);
console.log(name, res);
}
}
wrap();