티스토리 뷰

Javascript

[javascript] 비동기프로그램

이채야채 2022. 6. 1. 14:52

자바스크립트는 아시다시피 콜백큐에서 한줄씩 실행되는 싱글스레드 언어이다.

 

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
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함