[ JS ] 비동기

메이·2024년 5월 9일

JavaScript

목록 보기
11/12

🔎 먼저 동기를 알아보자

동기(Synchronous) 코드는 작성된 순서대로 실행되며, 하나의 작업이 끝나기 전에는 다음 작업이 시작되지 않는다.

console.log(1)
console.log(2)
alert('확인!')
console.log(3)
console.time('Loop!')
for (let i = 0; i < 1000000000; i++) {}
console.timeEnd('Loop!')
console.log(4)

🔎 그럼 비동기는 ?

비동기(Asynchronous) 코드는 작성된 순서대로 실행되지만, 특정 작업이 끝나기 전에 다음 작업이 시작될 수 있다.

✍ 예제 1

console.log(1)
console.log(2)
console.log(3)
console.time('Loop!')
setTimeout(() => {
  for (let i = 0; i < 1000000000; i++) {}
  console.timeEnd('Loop!')
  console.log(5)
}, 0) // 대표적인 비동기 코드
console.log(4)

✍ 예제 2

console.log(1)
const h1El = document.querySelector('h1')
h1El.addEventListener('click', () => {
  console.log('클릭!')
})
console.log(2)

1,2 는 먼저 뜨고 h1을 누르면 클릭! 이 나옴


✍ 예제 3

console.log(1)
fetch('http://api.heropy.dev/v0/users')
  .then(res => res.json)
  .then(data => console.log(data))
console.log(2)
  • 데이터가 도착하지 않았더라도 다음 코드를 실행할 수 있어야 화면이 멈추지 않고 동작할 수 있다.

✍ 예제 4

const h1El = document.querySelector('h1')
h1El.textContent = 'Loading'
const timer = setInterval(() => {
  h1El.textContent += '.'
}, 500)

const imgEl = document.createElement('img')
imgEl.src = 'https://picsum.photos/3000/2000'
imgEl.addEventListener('load',() => {
  document.body.append(imgEl)
  clearInterval(timer)
  h1El.textContent = 'Done!'
})


콜백 / 콜백 지옥

콜백

  • 실행하는 위치를 보장해준다!

✍ 예제 1

// timer.js
export function timer(callback) {
  setTimeout(() => {
    console.log(1)
    callback()
  }, 2000) 
}


// main.js
import { timer } from "./timer.js"

timer(() => {
  console.log(2)
})

📍 2초 뒤에 1이 뜨고 2가 뜸


✍ 예제 2

function renderImage(callback) {
  const imgEl = document.createElement('img')
  imgEl.src = 'http://picsum.photos/500/500'
  imgEl.addEventListener('load', () => {
    document.body.append(imgEl)
    callback()
  })
}

renderImage(() => {
  console.log('Done!')
})

📍 사진이 화면에 출력되고 나서 콘솔창에 Done!이 뜸


콜백 지옥

function renderImage(callback) {
  const imgEl = document.createElement('img')
  imgEl.src = 'http://picsum.photos/500/500'
  imgEl.addEventListener('load', () => {
    document.body.append(imgEl)
    callback()
  })
}

renderImage(() => {
  console.log('Done 1')
})
renderImage(() => {
  console.log('Done 2')
})
renderImage(() => {
  console.log('Done 3')
})
renderImage(() => {
  console.log('Done 4')
})

새로고침을 하면 계속 바뀜

function renderImage(callback) {
  const imgEl = document.createElement('img')
  imgEl.src = 'http://picsum.photos/500/500'
  imgEl.addEventListener('load', () => {
    document.body.append(imgEl)
    callback()
  })
}

renderImage(() => {
  console.log('Done 1')
  renderImage(() => {
    console.log('Done 2')
    renderImage(() => {
      console.log('Done 3')
      renderImage(() => {
        console.log('Done 4')
      })
    })   
  })
})
  • 순서대로 차례대로 실행되지만, 비동기 코드를 동기적으로 사용하면서 생긴 코드의 작성 패턴이
    들여쓰기 되면서 안쪽으로 들어가게됨
  • 마치 개미지옥같다고해서 콜백지옥이라 불림 - 독성이 떨어지고 유지보수를 어렵게 만듦 지양해야함!
  • 콜백지옥을 개선하기 위해 promise 클래스를 사용함

promise 클래스 사용

function renderImage() {
  return new Promise((callback) => {
    const imgEl = document.createElement('img')
    imgEl.src = 'http://picsum.photos/500/500'
    imgEl.addEventListener('load', () => {
      document.body.append(imgEl)
      callback()
    })
  })
}

renderImage()
  .then(() => {
    console.log('Done 1')
    return renderImage()
  })
  .then(() => {
    console.log('Done 2')
    return renderImage()
  })
  .then(() => {
    console.log('Done 3')
    return renderImage()
  })
  .then(() => {
    console.log('Done 4')
    return renderImage()
  })


promise

  • 비동기 작업의 완료나 실패 시점을 지정하고 그 결과를 반환할 수 있다.
  • const promise = new Promise((resolve, reject) => { })
  • promise.then(( ) => { })
const myPromise = new Promise(() => {
  // 비동기 작업 처리~!
})

✍ 예제 1

/**
 * Promise : 비동기 작업을 처리하는 객체!
 * Promise 객체는 new 키워드와 생성자 함수를 통해 생성할 수 있다.
 * 생성할 때는 인자로 콜백함수를 넣을 수 있다.
 * 콜백함수 안에는 비동기 코드를 작성

 * resolve: 정상적인 결과 값을 반환 (이행) - 쉽게 말해 return대신 쓰였다고 생각하면 됨
 * reject: 정상적이지 않았던 값을 반환 (거부)
*/
const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const text = prompt('"hello"를 입력하면 선물을 드립니다!')
    if (text === 'hello') {
      resolve('💻')
    } else {
      reject('error message!')
    }
  }, 2000)
})

/**
 * Promise는 상태를 가지고있음
 * - 대기 (pending): 비동기 작업을 처리하는 중..
 * - 이행 (fulfilled): 비동기 작업이 정상적으로 처리가 된 경우!
 * - 거부 (rejected): 비동기 작업이 정상적으로 처리되지 않은 경우!
 * 
 * 메소드
 * - then(): 이행되었을 때
 * - catch(): 거부되었을 떄
 * - finally(): 이행되거나 거부되더라도 항상!
 */

//약속의 결과
myPromise
  .then((result) => {
    console.log('result: ', result)
  })
  .catch((err) => {
    console.log('error: ', err)
  })
  .finally(() => {
    console.log('complete!')
  })

//resolve 에 넣었던 결과값이 then의 파라미터로 떨어지고 
//reject 에 넣었던 결과값이 catch의 파라미터로 떨어진다

✍ 예제 2
바꾸기 전

function timer(callback) {
  setTimeout(() => {
    console.log(1)
    callback('1 is Done!')
  }, 2000)
}

timer((msg) => {
  console.log(msg)
  console.log(2)
})

바꾼 후

function timer() {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log(1)
      resolve('1 is Done!')
    }, 2000)
  })
}

timer()
  .then(msg => {
  	console.log(msg)
  	console.log(2)
  	return timer() // 다음 then 메소드를 사용하려면 promise 인스턴스를 반환해야한다
  })
  .then(msg => {
  	console.log(msg)
  	console.log(2)
  	return timer()
  })

promise 체이닝

: Promise 객체에 연속적으로 메소드를 호출하는 것

myPromise
  .then((result) => {
    console.log('result: ', result)
  	return `선물은 : ${result}` // 두번째 결과의 파라미터 즉, 아래의 result로 들어감
  })
.then((result) => {
    console.log('result: ', result)
  })
  .catch((err) => {
    console.log('error: ', err)
  })
  .finally(() => {
    console.log('complete!')
  })

Async & Await

바꾸기 전

/* html */

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <;title>Document</title>
    <script defer src="./main.js"></script>
    <style>
        .users {
          padding: 0;
        }
        .users li {
          height: 30px;
          display: flex;
          align-items: center;
          gap: 10px;
        }
        .users li img {
          width: 24px;
          height: 24px;
          border-radius: 50%;
        }
        .users li.no-photo img {
          filter: grayscale(100%) opacity(50%)
        }
    </style>
</head>
<body>
    <h1>Hello world!</h1>    
</body>
</html>
/* javascript */
const h1El = document.querySelector('h1')
const ulEl = document.createElement('ul')
ulEl.classList.add('users')
document.body.append(ulEl)

h1El.addEventListener('click', () => {
    ulEl.textContent = 'Loading...'
    fetch('http://api.heropy.dev/v0/users')
      .then(res => res.json())
      .then(data => {
        console.log(data)
        const { users } = data // 객체 구조 분해 할당
        const liEls = users.map(user => {
          const liEl = document.createElement('li')
          liEl.textContent = user.name
          const imgEl = document.createElement('img')
          imgEl.src = user.photo?.url || 'http://heropy.dev/favicon.png'
          if (!user.photo) {
            liEl.classList.add('no-photo')
          }
          liEl.prepend(imgEl)
          return liEl
        })
        ulEl.textContent = ''
        ulEl.append(...liEls)
      })
})

바꾼 후


const h1El = document.querySelector('h1')
const ulEl = document.createElement('ul')
ulEl.classList.add('users')
document.body.append(ulEl)

h1El.addEventListener('click', async () => {
    ulEl.textContent = 'Loading...'
    const res = await fetch('http://api.heropy.dev/v0/users')
    // 패치 함수가 호출되는 것을 기다리게 만들어줌
    const data = await res.json()
    console.log(data)
    const { users } = data // 객체구조분해할당
    const liEls = users.map(user => {
      const liEl = document.createElement('li')
      liEl.textContent = user.name
      const imgEl = document.createElement('img')
      imgEl.src = user.photo?.url || 'http://heropy.dev/favicon.png'
      if (!user.photo) {
        liEl.classList.add('no-photo')
      }
      liEl.prepend(imgEl)
      return liEl
    })
    ulEl.textContent = ''
    ulEl.append(...liEls)
})
profile
프론트엔드 개발자를 꿈꾸는 코린이₊⋆ ☾⋆⁺

0개의 댓글