티스토리 뷰
자바스크립트는 아시다시피 콜백큐에서 한줄씩 실행되는 싱글스레드 언어이다.
thread란?
- 스레드는 어떠한 프로그램이 실행되는 작업을 말한다.
- 싱글 스레드는 한 번에 하나의 작업만 수행할 수 있으며, 멀티 스레드는 한 번에 여러 개의 작업을 수행할 수 있다.
싱글스레드인 자바스크립트가 비동기가 가능한 이유는?
자바스크립트 역시 싱글 스레드로 한 번에 하나의 코드만 실행시킬 수 있다.
따라서 애니메이션이 지속됨과 동시에 상품 상세페이지로 이동하기 위한 클릭 등의 작업이 불가하다.
하지만 브라우저에서 이와 같은 업무가 가능하다. 왜 그럴까?
정답!! 이벤트루프
변수가 저장되는 heap이라는 공간, 코드를 실행시켜주주는 콜스택이있다. 콜스택은 한번에 한줄의 코드만 실행시킨다. 전문용어로 싱글스레드
기다림이 필요한 특수함수들 ajax요청, 이벤트핸들러 , setTimeout등은 콜스택으로 바로 올라가지 않는다.
대기실인 콜백큐에 가게되고 이벤트루프에의해서 콜스택으로 옮겨지게된다.
여기서 주의해야할점은 이벤트루프는 콜스택이 비워져있는지를 확인하고 비워져있어야지만 콜스택으로 한개씩 옮긴다.
console.log('Hi')
setTimeout(function cb1() {
console.log('cb1')
}, 5000)
console.log('Bye')
콜 스택에서 바로 큐로 넘어가는게 아니라 중간에 Web APIs를 한 번 거쳐 큐로 넘어간다. 이는 어떤 함수나 이벤트가 종료될 때까지 시간이 오래 걸릴 수 있기 때문에, 자바스크립트 엔진이 직접 처리하는 것이 아니라 브라우저에 위임한다. 위 예제에서는 setTimeout() 함수가 5초 뒤에 실행되기 때문에, Web APIs가 해당 연산을 마치고(5초 후) 콜 스택에서 바로 실행될 수 있는 상태가 되었을 때 큐에 등록한다.
큐
이벤트 루프는 해당 순서대로 대기하고 있는 함수들을 보고 있다가 차례대로 콜 스택에 가져와 실행한다.
console.log('처음')
setTimeout(() => {
console.log('setTimeout - 태스크 큐')
}, 0)
Promise.resolve()
.then(() => {
console.log('promise1 - 마이크로 태스크 큐')
})
.then(() => {
console.log('promise2 - 마이크로 태스크 큐')
})
requestAnimationFrame(() => {
console.log('requestAnimationFrame - rAF 큐')
})
console.log('마지막')
처음
마지막
promise1 - 마이크로 태스크 큐
promise2 - 마이크로 태스크 큐
requestAnimationFrame - rAF 큐
setTimeout - 태스크 큐
각 큐에 대한 실행 우선 순위는 마이크로 태스크 큐 > rAF 큐 > 태스트 큐 순서이다.
따라서 위와 같은 결과가 나와야하지만 실제로는 그렇지 않았다.
몇 번의 새로고침을 반복하다보면 setTimeout()이 requestAnimationFrame()보다 먼저 console에 찍히는걸 볼 수 있다.
그래서 비동기 프로그래밍!
비동기 처리는 현재 실행중인 것이 완료되지 않더라도 다음 코드를 실행하는 방식을 말한다.
동시에 여러 작업을 수행할 수 있다는 큰 장점이 있지만, 비동기 함수가 많을 경우 어떤 코드가 먼저 실행되는지 알 수 없고 가독성이 나쁘다는 평을 들어왔다.
이런 문제를 해결하기 위해 여러 비동기 프로그래밍 방법이 생겼고 크게 콜백(Callback) 함수, Promise, async/await 패턴이 존재한다.
콜백(Callback) 함수
콜백은 다른 함수의 인자로 함수를 넘기는 것을 말한다. 콜백 함수로 비동기 프로그래밍을 짤 수 있지만, 모든 콜백 함수가 비동기이진 않다. 예를 들어 map(), filter()의 첫 번째 인자로 들어가는 콜백 함수는 동기식으로 호출된다.
// 해당 코드는 [자바스크립트 Deep Dive, 이웅모 (2020)]의 프로미스(p842)에서 가져왔습니다.
const POSTS_URL = 'https://jsonplaceholder.typicode.com/posts'
const getPosts = url => {
const xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.send()
xhr.onload = () => {
if (xhr.status === 200) {
console.log(JSON.parse(xhr.response))
} else {
console.error(`${xhr.status} ${xhr.statusText}`)
}
}
}
const posts = getPosts(POSTS_URL)
console.log('posts: ', posts)
여기서 post를 console로 찍었을 때 어떤 결과가 나올까?
xhr.onload()가 비동기로 동작하기 때문에 post는 undefined라는 결과를 반환한다. 이렇듯 비동기로 동작하는 함수는 외부에서 그 값을 바로 참조하지 못하여, 무조건 콜백 함수 내부에서 그 처리를 진행해야 한다.
const getPosts = (url, whenSuccess, whenFail) => {
const xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.send()
xhr.onload = () => {
if (xhr.status === 200) {
whenSuccess(JSON.parse(xhr.response))
} else {
whenFail(xhr.status, xhr.statusText)
}
}
}
const handlePosts = response => {
// ...
}
const errorHandling = (status, statusText) => {
// ...
}
const posts = getPosts(POSTS_URL, handlePosts, errorHandling)
따라서 콜백의 후속 처리를 모두 그 콜백 함수 내에서 처리해야 하기 때문에, 위처럼 다시 콜백함수를 넘기는 수 밖에 없게 되었다.
그런데 만약 해당 콜백 함수에 또 예외 처리를 해야 하거나, 여러 에러 상황에 각기 다른 조치를 취해야 한다면 어떻게 해야할까? 끔찍하게도 이것 역시 또 다른 콜백함수로 넘겨야 한다. 그리고 이런 상황이 곧 ‘콜백 헬(callback hell)’이라는 단어로 불러졌다.
Promise
Promise도 콜백 헬을 해결할 수 없었지만, 비동기 함수의 후속 처리가 콜백 함수에 비해 다루기 훨씬 쉬워졌다.
const getPostsWithPromise = url => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.send()
xhr.onload = () => {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.response))
} else {
reject(xhr.status, xhr.statusText)
}
}
})
}
const posts = getPostsWithPromise(POSTS_URL)
console.log('posts: ', posts)
콜백 함수와 달리 post를 console로 찍어보면 Promise {<pending>}이란 값이 나온다. 비동기 함수가 수행되기 전이기 때문에 resolve나 reject가 아닌 pending이 반환된 것이다.
posts
.then(res => console.log(res))
.catch(err => console.error(err))
.finally(() => console.log('끝'))
그리고 Promise로 생성된 posts는 각각 then, catch, finally로 후속 처리가 가능하다.
fetch() 함수가 바로 Promise 기반으로 만들어진 HTTP 요청 전송 기능인 클라이언트 사이드 Web API다. 쓰임새도 Promise와 매우 유사하다.
fetch(POSTS_URL)
.then(res => console.log(res))
.catch(err => console.error(err))
.finally(() => console.log('끝'))
Promise는 비동기 함수 처리를 쉽게 할 수 있다는 것 외에도, 여러 비동기 처리를 병렬 처리할 때 사용하는 Promise.all(), 여러 비동기 처리를 다룰 때 가장 먼저 fulfilled된 처리 결과를 반환하는 Promise.race() 등 일반 콜백 함수로 다루는 것보다 보다 더 다양한 작업이 가능하다.
async/await
그러나 Promise는 여전히 콜백 함수를 사용하기 때문에, 콜백 헬의 문제를 해결할 수 없었다. ES6에 제너레이터가 도입되어 비동기를 동기처럼 구현했지만, 코드가 장황해지고 가독성이 나빠졌다. 이에 뒤따라 ES8에서 보다 간단하고 가독성 좋게 비동기 처리를 동기 처리처럼 구현하는 async/await가 도입되었다.
async/await는 Promise를 기반으로 동작하며, then/catch/finally와 같은 후속 처리 메서드 없이 마치 동기 처리처럼 사용할 수 있다.
const getPostWithAsync = async url => {
try {
const response = await fetch(url)
return await response.json() // 혹은 다른 형태로 데이터 전처리 가능
} catch (err) {
console.err(err)
} finally {
console.log('끝')
}
}
const posts = getPostWithAsync(POSTS_URL)
console.log('posts: ', posts)
posts.then(console.log)
'Javascript' 카테고리의 다른 글
Web Worker란?? (0) | 2022.11.28 |
---|---|
[javascript]연산자 (0) | 2022.05.27 |
promise 의 리턴값과 then (1) | 2022.05.20 |
[Javascript] 프로그래밍, 자바스크립트 (0) | 2022.04.19 |
섹션2 HA2 까다로웠던 부분 (0) | 2021.12.13 |