비동기 프로그래밍

wh·2022년 4월 25일
0

동시작업

동시동작이 자바스크립트는 한 스레드에서 실행된다. 한 함수가 실행되면 다른 함수는 끝날때까지 기다려야 한다. 따라서, 상호배제 mutex, 교착상태 deadlock 등의 걱정은 필요없다.
오래걸리는 작업은 항상 비동기 asynchronous 로 처리한다. 수행할 작업을 지정하고, 데이타를 이용할수 있거나 오류가 발생했을때 실행한 콜백함수를 제공한다. 그 동안 현재함수는 다음 코드를 이어서 실행한다.

프라미스 생성

프라미스를 만드는 방법을 살펴볼테지만, 실제로는 대부분의 라이브러리가 프라미스를 반환해주기 때문에 직접 프라미스를 만들 필요는 거의 없다.

const produceAfterDaly = (result, delay) => {
  return new Promise((resolve, reject) => {
    const callback = () => resolve(result)
    setTimeout(callback, delay)
  })
}

결과가 이미지인 프라미스를 반환하는 함수

const loadImage = url => {
  return new Promise((resolve, reject) => {
    const request = new XMLHttpRequest()
    const callback = () => {
      if (request.status === 200) {
        const blob = new Blob([request.response], {type: 'image/png'})
        const img = document.createElment('img')
        img.src = URL.createObjectURL(blob)
        resolve(img)
      } else {
        reject(Error(`${request.status}: ${request.statusText}`))
      }
    }
    request.open('GET', url)
    request.responseType = 'blob'
    request.addEventListener('load', callback)
    request.addEventListener('error', (event) => 
    	reject(Error('Network error'))
    );
    request.send()
  })
}

document.addEventListener('DOMContentLoaded', () => {
  const imgdiv = document.getElementById('images')
  const promise = loadImage('../../hanafuda/1-1.png')
  console.log({promise})
  promise.then(img => {
    imgdiv.appendChild(img)
    console.log({promise})
  })
})

즉시 종료되는 프라미스

Promise.resolve(value)를 호출하면 주어진 값으로 프라미스 실행이 바로 완료된다.

const loadImage = url => {
  if (url === undefined ) return Promise.resolve(brokenImage)
  ...
}

프라미스 결과

프라미스가 완료될때까지 기다릴 필요가 없으며 프라미스가 완료될때 결과 또는 오류를 처리할 액션을 제공한다. 이 들 액션을 추가함수가 끝나면 액션함수가 실행된다.
then 메서드로 액션을 지정하면 프라미스가 해석된 다음 액션이 실행된다.

const promise1 = loadImage('../../hanafuda/1-1.png')
promise1.then(img => imgDiv.append(img))

프라미스 체이닝

  {
    const imgdiv = document.getElementById('images3')
    
    loadImage('../../hanafuda/3-1.png')
      .then(img => imgdiv.appendChild(img)) // Synchronous
      .then(() => loadImage('../../hanafuda/3-2.png')) // Asynchronous
      .then(img => imgdiv.appendChild(img)) // Synchronous
  }


  // Caution: The then method discards non-function arguments
  // The last image is loaded but not appended
  {
    const imgdiv = document.getElementById('images6')
        
    loadImage('../../hanafuda/6-1.png')
      .then(img => imgdiv.appendChild(img))
      .then(loadImage('../../hanafuda/6-2.png'))
        // Error—argument of then isn't a function
      .then(img => imgdiv.appendChild(img))
  }
})

거부처리 Rejection Handling

// The finally method
  {
    const imgdiv = document.getElementById('images7')

    const doCleanup = text => imgdiv.appendChild(document.createTextNode(text))

    const url = '../../hanafuda/1-1.png'    
    Promise.resolve()
      .then(() => loadImage(url))
      .then(img => imgdiv.appendChild(img)) 
      .finally(() => { doCleanup('All done. ') })
      .catch(reason => console.log({reason}))
  }

여러 프라미스 실행

프라미스를 이터러블에 저장한후 Promise.all(iterable) 을 호출한다. 그러면 그 결과가 프라미스 순서대로 이터러블에 저장된다. Promise.all 은 모든 태스크를 병렬로 실행하지 않으며 한 스레드에서 순차적으로 실행된다. 하지만 작업순서는 예상할수 없어 어떤 프라미스결과가 먼저일지 알수 없다.

const promises = [
   loadImage('../../hanafuda/1-1.png'),
    loadImage('../../hanafuda/1-2.png'),
    loadImage('../../hanafuda/1-3.png'),
    loadImage('../../hanafuda/1-4.png')
]
Promise.all(promises)
  .then(images => { for (const img of images) imgdiv.appendChild(img) })

거절된 결과를 좀 더 적절하게 처리하기 위해선 Promise.allSettled 메서드를 사용한다.

Promise.allSettled(promises)
    .then(results => { console.log(results) })
// [{status: 'fulfilled', value: img}, {status: 'fulfilled', value: img}, {status: 'rejected', reason: Error}, {status: 'fulfilled', value: img}]

여러 프라미스의 경쟁

처음으로 완료된 프라미스가 결과를 좌우한다. Promise.race(iterable)은 한 프라미스의 결과가 완료될 때가지 프라미스들을 실행한다.

Promise.race(promises)
    .then(img => imgdiv.appendChild(img))

async 함수

async 키워드를 갖는 함수에서만 await 를 사용할 수 있는데, 프라미스 동작이 완료된후 await 이후의 동작이 실행되도록 한다.

const putImage = async (url, element) => {
  const image = await loadImage(url)
  element.appendChild(img)
}
// =
const putImage = (url, element) => {
  loadImage(url)
  	.then(img => element.appendChild(img))
}

await 는 여러번 사용할수 있고 루프도 사용할수 있다.

const putTwoImages = async (url1, url2, element) => {
  const img1 = await loadImage(url1)
  element.appendChild(img1)  
  const img2 = await loadImage(url2)
  element.appendChild(img2)  
}

const putImages = async (urls, element) => {
  for (const url of urls) {
    const img = await loadImage(url)
    element.appendChild(img)
  }
}

async 함수는 다음 상황에 적용할수 있다

// 화살표 함수
async url => { . . . }
async (url, params) => { . . . }

// 메서드
class ImageLoader {
 async load(url) { . . . }
}

// 명명된 함수와 익명함수
async function loadImage(url) { . . . }
async function(url) { . . . }

// 객체 리터럴 메서드
obj = {
 async loadImage(url) { . . . },
 . . .
}

async 의 반환값

async 함수는 프라미스를 반환한다.

const result = await fetch('https://aws.random.cat/meow')
const imageJSON = await result.json()

Fetch API 에서 JSON 처리는 비동기이므로 두번째 await 는 반드시 사용해야 한다.

const getCatImageUrl = async () => {
  const result = await fetch('https://aws.random.cat/meow')
  const imageJson = await result.json()
  return await loadImage(imageJson.file)
}

async 함수가 await 를 만나기전에 값을 반환하면 Promise.resolve 로 감싼다.

// An async function returning a value (actually a resolved promise)
const getJSONProperty = async (url, ...keys) => {
  if (url === undefined) return null
  // Actually returns Promise.resolve(null)
  const result = await fetch(url)
  const json = await result.json()
  let value = json
  for (const key of keys) value = value[key]
  return value
}

동시 await concurrent Await

await 를 연속으로 사용하면 순서대로 실행된다. 동시에 실행하게 하려면 Promise.all 을 활용한다.

const [img1, img2] = await Promise.all([loadImage(url), loadCatImage()])

async 함수의 예외

async 함수에서 예외를 던지면 Promise.reject 를 반환한다.
반면 await 연산자가 거절된 프라미스를 받으면 예외를 일으킨다.

const getAnimalImageURL = async type => {
  if (type === 'cat') {
    return getJSONProperty('https://aws.random.cat/meow', 'file')
  } else if (type === 'dog') {
    return getJSONProperty('https://dog.ceo/api/breeds/image/random', 'message')
  } else {
    throw Error('bad type') // Async function returns rejected promise
  }
}

const getAnimalImage = async type => {
  try {
    const url = await getAnimalImageURL(type)
    return loadImage(url)
  } catch {
    return brokenImage
  }
}

연습문제

로딩된 순서가 아닌 올바른 순서대로 이미지 배치

const loadImage = url => {
  return new Promise((resolve, reject) => {
    const request = new XMLHttpRequest()
    const callback = () => {
      if (request.status == 200) { 
        const blob = new Blob([request.response], {type: 'image/png'})
        const img = document.createElement('img')
        img.src = URL.createObjectURL(blob)
        resolve(img)
      } else {
        reject(Error(`${request.status}: ${request.statusText}`))
      }
    }

    request.open('GET', url)
    request.responseType = 'blob'
    request.addEventListener('load', callback)
    request.addEventListener('error', event => reject(Error('Network error')));
    request.send()
  })  
}

document.addEventListener('DOMContentLoaded', () => {
  const imgdiv = document.getElementById('images')

  let promises = [loadImage('../../hanafuda/1-1.png'), loadImage('../../hanafuda/1-2.png'), loadImage('../../hanafuda/1-3.png'), loadImage('../../hanafuda/1-41.png')]

  Promise.all(promises).then( (images) => {
    for (const img of images) imgdiv.appendChild(img)
  })
})





profile
js

0개의 댓글

관련 채용 정보