- Published on
비동기 원리
- Authors
- Name
- 길재훈
비동기에 대해서..
비동기를 알기 위해서는 다음의 진화과정이 존재한다 콜백함수 -> 프로미스 -> asyn/await
그럼, callback
함수먼저 알아보자.
콜백함수
콜백함수는 함수의 인자로 함수를 받아 그 함수를
실행
하는 함수이다.
function aaa(func) {
// 함수 로직
}
aaa(() => {})
이렇게 사용하는 callback
함수는 외부 API
의 데이터를 요청하는 로직을 사용하는데 많이 사용한다.
즉, 비동기를 실행하기 위해 함수로 인자로 받는 방식 이다.
function aaa(qqq) {
// 외부 API 에 데이터 요청하는 로직
const result = '요청으로 받아온 데이터'
// 요청이 끝나면 qqq 실행
qqq(result)
}
aaa((result) => {
console.log('요청이 끝났습니다.')
console.log(`요청으로 받아온 데이터는 ${result} 입니다.`)
})
그렇다면, 무엇하러 비동기
를 callback
, promise
, async/await
으로 나누어 처리하는가?
그이유는 다음과 같이 같은 로직을 3개로 나누어 비교해본다.
callback
const myCallback = () => {
const aa = new XMLHttpRequest()
number.open('get', numberApi)
number.send()
number.addEventListener('load', (res) => {
const num = res.target.response.split(' ')[0]
const post = new XMLHttpRequest()
post.open('get', koreanJson(num))
post.send()
post.addEventListener('load', (res) => {
console.log(res) // API 요청 결과
const userId = JSON.parse(res.target.response).UserId // 8 (작성자 ID)
const posts = new XMLHttpRequest()
posts.open('get', koreanJsonPost(userId))
posts.send()
posts.addEventListener('load', (res) => {
console.log(res) //최종 api 요청 결과
})
})
})
}
callback
지옥을 경험할 수 있다. 코드가 굉장히 어려우며, 보는데 두려움마저 생긴다.
callback
안에서 또다시 callback
을 호출하는 이 굉장한 상황이 무섭다.
Promise
Promise
const myPromise = () => {
axios
.get(numberApi)
.then((res) => {
return axios.get(koreanJson(res))
})
.then((res) => {
return axios.get(koreanJsonPost(res))
})
.then((res) => {
console.log(res)
})
}
이 깔끔한 상황을 바라보자. 이전의 callback
과 비교했을때, 훨씬 보기 좋다.
하지만, 이러한 Promise 는 비동기로 작동하기에 예상치 못한 상황이 생기기 마련이다.
다음을 보자.
const myPromise = () => {
console.log('1번째로 실행됩니다!!!')
axios
.get(numberApi)
.then((res) => {
console.log('2번째로 실행됩니다!!!')
return axios.get(koreanJson(res))
})
.then((res) => {
console.log('3번째로 실행됩니다!!!')
return axios.get(koreanJsonPost(res))
})
.then((res) => {
console.log('4번째로 실행됩니다!!!')
console.log(res)
})
console.log('5번째로 실행됩니다!!!')
}
실행은 다음과 같인 실행된다.
1 -> 5 -> 2 -> 3 -> 4
좋지 않다.
우리 인간은 위에서 아래로 차례대로 실행되길 바란다.
비동기
는 예측하기 힘들다.
즉 비동기지만, 함수 내부에서는 동기적
으로 작동하면 좋겠다.
이러한 것을 구현한것이 async/await
이다.
Async/Await
const myAsynAwait = async () => {
const number = await axios.get(numberApi)
const post = await axios.get(koreanJson)
const posts = await axios.get(koreanJsonPost)
console.log(posts)
}
이렇게 동기적으로 작동되게 만들면 앞으로 나올 상황이 예측 가능해진다.
정리
여기서 알수 있는 것은 callback
지옥의 문제로 인해 promise
가 탄생했으나, promise
의 비동기로 인해 인간의 생각대로(동기적으로..
) 작동하지 않을 수 있다.
함수 내부에서 동기적
으로 작동하는 처리를 구현해 낸것이 async/await
이다.
우리는 async/await
으로 인해 비동기
로 작동하는 여러 예측 못할 상황에서 벗어났으며, 코드를 동기
적으로 생각할수 있게 되었다.
굉장히 멋진 일이다!!
프로미스의 작동원리
다음을 가정해보자.
만약, 강아지
라는 data
를 2초
에 걸려서 받아온다고 하자.
이러한 가정을 만들기 위해 setTimeout
을 사용하여 2초
뒤 실행하여 값을 resolve
에 넘긴다.
const onClickThen = () => {
new Promise((resolve, reject) => {
try {
settimeout(() => {
const res = '강아지' // 2초가 걸려서 backend에서 `강아지` data 를 받아옴
resolve(res)
}, 2000)
} catch (err) {
reject(err.message)
}
})
}
이렇게 만들어진 Promise
를 then
및 catch
를 통해 받을 수 있다,
const onClickThen = () => {
new Promise((resolve, reject) => {
try {
settimeout(() => {
const res = '강아지' // 2초가 걸려서 backend에서 `강아지` data 를 받아옴
resolve(res)
}, 2000)
} catch (err) {
reject(err.message)
}
})
.then((res) => {
console.log(res) // 강아지
})
.catch((err) => console.log(err))
}
onClickThen
이 실행된후, err
가 발생하면 catch
에서 받고, 제대로 작동된다면 resolve
를 실행한후 then
에서 받아 처리한다.
이러한 원리를 사용하여 간단한 axios
를 만들어 본다.
axios
간단한 axios
역시 Promise
기반이다. 이러한 axios
를 위의 Promise
의 원리를 사용하여 간단하게 구현해본다.
const myaxios = {
get: (url) =>
new Promise((rsv, rej) => {
const req = new XMLHttpRequest()
req.open('get', url)
req.send()
req.addEventListener('load', (res) => {
rsv(res.target.response)
})
}),
}
XMLHttpRequest
는 Promise
가 아니므로 Promise
로 만들기 위해 감싸주어야 한다.
여기서 new Promise
를 사용하여 내부의 XMLHttpRequest
를 통해 받아온 url
의 data
를 rsv (resolve)
로 넘긴다.
그렇게 반환된 값은 promise
를 반환하며, 이후 then
을 통해 받아온 값을 사용가능하다.
const onClickAxios = async () => {
myaxios.get('https://koreanjson.com/posts/1').then((res) => console.log(res))
}
이 onClickAxios
를 onclick
하면, 해당 data
를 받아온후 then
에서 console
로 출력되는것을 볼 수 있다.
Promise
는 이렇게 data
를 받아올 수 있다.
이러한 data
를 비동기
로 작동한다고 했다.
비동기
에 대해 더 명확히 알기 위해서는 macro queue
및 macro queue
에 대해서 알고 있는것이 좋다.
macro-micro-queue 란?
Event loop
상 비동기 함수
는 queue
에 저장한다.
이러한 queue
역시 2가지의 queue
로 나누어지는데 그 2가지가 macro queue
, micro queue
이다.
macro queue
는 setTimeout
및 setInterval
같은 비동기 함수
가 저장된다.
micro queue
는 Promise 함수
가 저정되는 queue
이다.
이때, micro queue
가 macro queue
보다 우선순위가 높다.
setTimeout
과 Promise
를 같이 실행시킨다고 가정하자.
Promise 반환 함수
는 micro queue
에 저장되므로, 먼저실행되는것이 보장된다.
실행순서는 다음과 같다
`Promise 반환함수` --> `setTimeout 반환함수`
여기서
비동기
시 잘 알아야 할 것은, 항상micro queue
에 어떠한 값이 존재한다면,macro queue
는 후순위로 밀려난다는 것이다.
이러한 비동기 개념
이 있지 않은 상황에서 비동기 실행
순서 문제가 발생한다면 해결할 수 없는 문제가 발생한다.
이러한 개념을 알고 있는것은 매우 중요하다.
다음을 살펴보자
const onClickLoop = () => {
console.log('시작!!')
// 비동기작업(매크로큐에 들어감)
setTimeout(() => {
new Promise((rsv) => rsv('철수')).then(() => {
console.log('저는 Promise(setTimeout 안에서 실행될거에요)!!')
})
console.log('저는 setTimeout!! 매크로큐!! 0초뒤에 실행될 거예요!!!')
}, 0)
// 비동기 작업(마이크로큐에 들어감)
new Promise((rsv) => rsv('철수')).then(() => {
console.log('저는 Promise(1)!! 마이크로큐!! 0초 뒤에 실행될 거에요!')
})
// 비동기작업(매크로큐에 들어감)
setInterval(() => {
console.log('저는 setInterval!! 매크로큐!! 0초마다 실행될 거예요!!!')
}, 0)
let sum = 0
for (let i = 0; i <= 9000000000; i += 1) {
sum += 1
}
// 비동기 작업(마이크로큐에 들어감)
new Promise((rsv) => rsv('철수')).then(() => {
console.log('저는 Promise(2)!! 마이크로큐!! 0초 뒤에 실행될 거에요!')
})
console.log('끝!!')
}
이러한 복잡한 logic
들이 비동기
로 작동하며 macro queue
또는 micro queue
에 들어가게 된다.
실행순서가 쉽게 예측가능한가? 그렇지 않다..
이 작업은 다음과 같은 순서로 작동하게 된다.
1) "시작"
2) "끝"
3) "저는 Promise(1)!! 마이크로큐!! 0초 뒤에 실행될 거에요!"
4) "저는 Promise(2)!! 마이크로큐!! 0초 뒤에 실행될 거에요!"
5) "저는 setTimeout!! 매크로큐!! 0초뒤에 실행될 거예요!!!"
6) "저는 Promise(setTimeout 안에서 실행될거에요)!!"
7 + n) "저는 setInterval!! 매크로큐!! 0초마다 실행될 거예요!!!"
비동기
는 알아보기 어렵다. 여러 문제가 발생할 수 있으므로, 이 개념을 확실히 아는것은 중요하다.