비동기 코드란 실행 순서가 정해져 있지 않은 코드를 의미합니다
바꿔 말하면 언제 실행될지 알 수 없다는 말과 같습니다
이것은 우리가 일반적으로 코드를 작성할 때 사용하는 동기 코드와는 달리,
코드의 실행이 순차적으로 진행되지 않을 수 있어서
코드의 흐름을 예측하기 어렵기 때문입니다
이러한 비동기 코드는 주로 서버와 통신할 때 사용됩니다
서버는 보통 1대 다수를 상대하기 때문에
이와 관련한 작업을 일반적인 동기 코드로 작성할 경우
전체 작업이 완료될 때까지 클라이언트 측의 대기시간의 텀이 한없이 길어질 수 있습니다
이를 피하기 위해서는 작업을 비동기적으로 처리해야 합니다
(비동기 코드의 연산은 일반적으로 브라우저의 자바스크립트 엔진이 처리합니다)
비동기 코드를 사용해야만 하는 이상
이것을 다룰 방법에 대해서도 알아야겠죠
우리는 이러한 비동기 코드들의 발동시점을 특정하기 위해
조건을 거는 등의 여러 기법을 활용해서 처리할 수 있습니다
socket.on('data', (chunk)=>{
console.log(chunk)
})
net.createServer((socket)=>{
// '데이터가 넘어왔을 때' 다음 콜백함수가 실행됩니다
socket.on('data',(chunk)=>{
const req = request(chunk)
if() {
// 어쩌고 저쩌고...
}
})
})
(↑ TCP 통신에서 살펴본 비동기 예제...)
클라이언트 측에서 서버에 코드를 전달할 때는
그것이 언제 도착할 지를 특정할 수 없기 때문에
비동기 코드로 작성할 수 밖에 없습니다
하지만 이런 작업을 하다보면 자연스레 코드 안에서 코드,
또 그 안에서 코드가 실행되는 중첩 구조가 형성되면서
점점 코드의 뎁스(depth)가 커지게 되는데
이러한 현상을 '콜백 지옥(Callback Hell)'이라고 부릅니다
const taskA = () => {
setTimeout(() => {
console.log("A Clear");
}, 3000);
};
const taskB = () => {
setTimeout(() => {
console.log("B Clear");
}, 2000);
};
const taskC = () => {
setTimeout(() => {
console.log("C Clear");
}, 1000);
};
위 코드를 바탕으로 taskA가 클리어되면 taskB를 요청하고,
taskB가 클리어되면 taskC를 요청하는 구조의 코드를 작성해야 한다면
어떻게 해야 할까요?
const task A = () => {
setTimeout(() => {
console.log("A Clear");
taskB()
}, 3000);
};
const taskB = () => {
setTimeout(() => {
console.log("B Clear");
taskC()
}, 2000);
};
const taskC = () => {
setTimeout(() => {
console.log("C Clear");
}, 1000);
};
// > A Clear
// B Clear
// C Clear
↑ 이것으로 원하는 결과는 얻었습니다
하지만 taskA를 실행하면 무조건 taskB와 taskC가 순차적으로 실행된다는 것,
하나의 함수만 출력할 수는 없다는 문제가 생깁니다
다시 말해서 위와 같은 코드는 확장성이 크게 떨어지는 해결법입니다
이런 문제에 대응하기 위해 사용할 수 있는 것이 바로 콜백(callback)입니다
const taskA = (callback) => {
setTimeout(() => {
console.log("A Clear");
callback()
}, 3000);
};
const taskB = (callback) => {
setTimeout(() => {
console.log("B Clear");
callback()
}, 2000);
};
const taskC = (callback) => {
setTimeout(() => {
console.log("C Clear");
callback()
}, 1000);
};
// 호출
taskA(() => {
taskB(()=>{
taskC(()=>{
console.log("end")
})
})
})
// >
// A Clear
// B Clear
// C Clear
// end
↑ 이렇게 함수 안에 호출용 콜백을 끼워넣는 것만으로도
보다 확장성이 높은 코드가 만들어집니다
또환 위와 같은 방식을 사용하면
taskA > taskB > 다시 taskA를 실행하는 것도 가능합니다
taskA(()=> {
taskB(()=>{
taskA(()=>())
})
})
단, 위에서도 언급했다시피 콜백을 끼워넣고 호출하는 방법은
호출 단계에서 '가독성이 크게 떨어진다'는 문제를 피할 수 없습니다
↓ 콜백지옥 예시
// taskA req&res
// taskA req&res
// taskC req&res
// taskB req&res
// taskA req&res
// taskB req&res
taskA(() => {
taskA(() => {
taskC(() => {
taskB(() => {
taskA(() => {
taskB(() => {
console.log("end");
});
});
});
});
});
});
위와 같은 코드는 가독성이 떨어지고, 코드를 수정하기도 어렵습니다
이제 이러한 콜백지옥을 피하기 위해 탄생한
Promise와 async/await에 대해 알아볼 차례입니다
new Promise((resolve, reject)=>{ // 비동기 코드를 담을 코드 블럭입니다 })
프로미스는 자바스크립트의 비동기 처리에 사용되는 객체입니다
주된 목적은 함수의 사용(호출)을 보다 깔끔하게 하기 위함이며,
리턴 타입이 객체이기 때문에 보통 프로미스 객체라고 불립니다
const pr = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('OK');
}, 1000);
});
pr.then(n => {
console.log(n);
});
// > 1초 뒤 OK 출력
1) Promis는 resolve
(성공)와 reject
(실패)라는 두 가지 인자를 받습니다
resolve()
의 인자값으로 넣습니다reject()
에 담습니다2) 프로미스 객체를 호출할 때는 then
, catch
, finally
라는
세 가지 메서드를 이용할 수 있습니다
const a = () => {
return new Promise ((resolve, reject) => {
reject(new Error('failed'))
})
}
a().then().catch((err)=>{console.log(err)})
then
메서드가 발동합니다catch
메서드가 발동합니다finally
메서드를 사용하면 프로미스가 처리될 때3) 프로미스 객체에는 기본적으로 '상태'라는 것이 존재합니다
console.log(pr)
// Promise { state : <pending> result : <undefined> }
// Promise { state : <fulfilled> result : 'OK' }
// Promise { state : <rejected> result : 'OK' }
pending
: 아직 비동기 코드가 완료되지 않은 상태입니다 (요청중)fulfilled
상태가 되고 (then()
실행완료),rejected
상태가 됩니다 (catch()
실행완료)여기서 잠깐!
const a = () => {
return new Promise ((resolve, reject) => {
reject(new Error('failed'))
})
}
a().then().catch((err)=>{console.log(err)})
// > Promise {<fulfilled>: undefined}
위 코드를 실행했을 때 반환되는 프로미스 객체의 상태는
rejected
가 아닌 fulfilled
입니다
그렇다면 코드의 실행 결과에서 Promise 객체의 상태가
fulfilled
로 출력된 이유는 무엇일까요?
이는 then
메서드의 첫번째 인자인 콜백 함수(resolve()
)가
정의되지 않은 상태에서 then
메서드가 호출되었기 때문입니다
앞서 살펴본대로 then
메서드의 첫번째 인자는
작업이 성공적으로 완료된 경우에 호출되는 콜백 함수입니다
이 콜백 함수가 정의되지 않고 then
메서드가 호출된 경우에는
프로미스 객체의 상태가 fulfilled
로 출력된다는 것을 알아둡시다
그러면 앞서 callback hell을 유발한 예제를 다시 살펴보겠습니다
const taskA = () => {
return new Promise((resolve, reject)=>{
setTimeout(()=> {
resolve(" A Clear")
},3000)
})
}
const taskB = () => {
return new Promise((resolve, reject)=>{
setTimeout(()=> {
resolve("B Clear")
},2000)
})
}
const taskC = () => {
return new Promise((resolve, reject)=>{
setTimeout(()=> {
resolve("C Clear")
},1000)
})
}
// 프로미스 체이닝
console.time("x")
taskA().then((data)=> {
console.log(data)
return taskA()
})
.then((data)=>{
console.log(data)
retrun taskC()
})
.then((data)=>{
console.log(data)
retrun taskB()
})
.then((data)=>{
console.log(data)
retrun taskA()
})
.then((data)=>{
console.log(data)
retrun taskB()
})
.then((data)=> {
console.log(data)
console.timeEnd("x")
})
// > 결과는 순차적으로 실행됩니다
출력 결과는 콜백을 끼워넣었을 때와 같습니다
하지만 프로미스 객체를 사용할 경우, 위와 같이 함수 몇개를 실행하더라도
호출부의 뎁스가 변하지 않다는 장점을 가지게 됩니다
이것이 프로미스 객체를 사용하는 주된 목적입니다
프로미스 객체를 사용할 경우, 콜백을 통한 비동기 처리에 비해서
함수 호출이 보다 나아질 지는 몰라도 사용 방식이 까다롭다는 문제점은
여전히 남아 있습니다
객체를 생성하는 과정도 번거롭고, 함수의 호출 또한 코드 뎁스가 얕다고는 해도
다소 번잡하다는 느낌을 지울 수 없습니다
이제 이러한 프로미스의 단점을 해결해줄 async
/ await
에 대해 알아볼 차례입니다
async
: 함수 앞에 사용되며 그 함수 내부에서await
키워드를 사용할 수 있게 해줍니다
await
:async
키워드로 생성된 함수에 대해 비동기 작업이 완료될 때까지 기다리게 합니다
async
/ await
은 프로미스 객체를 기반으로 구현되어 있습니다
async
와 await
역시 코드의 가독성을 높이기 위해 탄생한 기능이며,
프로미스 객체를 보다 쉽게 사용할 수 있도록 해줍니다
그러면 async
/ await
의 사용법에 대해 알아보겠습니다
async
를 사용하면 프로미스 객체를 보다 쉽게 생성할 수 있습니다const taskA = async()=> {
return ' A Clear'
}
console.log(taskA())
// > Promise {<fulfilled>: ' A Clear'}
// 프로미스 객체가 반환됩니다
taskA().then((data)=> {
console.log(data)
})
// > A Clear
// async를 써서 만든 프로미스 객체에도 then이나 cath 메서드를 쓸 수 있습니다
await
은 async
를 써서 생성한 함수의 비동기 작업이 끝날 때까지const init = async () => {
const result1 = await taskA() // 3초 후
console.log(result1)
const result2 = await taskA() // 3초 후
console.log(result2)
const result3 = await taskC() // 1초 후
console.log(result3)
const result4 = await taskB() // 2초 후
console.log(result4)
const result5 = await taskA() // 3초 후
console.log(result5)
const result6 = await taskB() // 1초 후
console.log(result6)
}
init()
// > A Clear
// > A Clear
// > C Clear
// > B Clear
// > A Clear
// > B Clear
await
은 async
함수 안에서만 사용할 수 있습니다const taskA = async()=> {
return setTimeout(()=> {
console.log(" A Clear")
},3000)
}
let rst = 'AAA'
const init = async() => {
const result = await taskA()
rst = result
}
init()
console.log(rst)
// >
// AAA 즉시 실행 (console.log(rst)에 대한 결과)
// 3초 뒤 A Clear 출력 (init() 호출 결과)
+) 굳이 Promise 문법을 활용하는 것이 나은 순간들도 있습니다
아래 예제와 같이 프로미스 객체를 묶어서 일괄처리해야 할 경우에는
Promise.all
메서드를 사용하는 것을 추천합니다
// Promise.all([Promise{}, Promise{}, Promise{}...])
// 프로미스 객체들을 모아주는 역할을합니다
const init = async () => {
console.time("x")
const result = await Promise.all([taskA(), taskB(), taskC()])
console.log(result)
console.timeEnd("x")
}
init()
// > 3초 후 [' A Clear', 'B Clear', 'C Clear']
+) 추가 예제 코드
const example = async() => {
const successPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Hello world!');
}, 1000);
});
const failurePromise = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('에러 발생!'));
}, 2000);
});
try {
const result = await successPromise;
console.log(result);
} catch (error) {
console.error(error);
}
try {
const result = await failurePromise;
console.log(result);
} catch (error) {
console.error(error);
}
}
example();
// > 1초 뒤 'Hello world!' 출력
// > 1초 뒤 'Error: 에러 발생!' 출력