초보 개발자

동기 비동기에 대한 고찰.... 본문

AI 웹개발 트랙 - 내배캠/9주차 ~

동기 비동기에 대한 고찰....

taehyeki 2022. 3. 4. 21:53

 

동기 비동기 💢


내가 생각한 동기 비동기는 차례대로 코드들을 읽다가(동기) 어떤 비동기적인 코드를 읽으면 '동시'에 동기코드를 처리해나가면서 비동기 코드또한 처리를 해나간다고 생각해왔다. (옆 화구에 백숙(비동기)'!올려놓고!' 라면(동기) 끓이기 )


근데 스레드가 하나(일하는 사람이 하나)라면 동시에 여러 일을 진행 시킬 수 없으니까. 이건 말이 안된다고 한다.
따라서 비동기는 '동시'에 같이 일을 진행시키는 것이 아니라 일단! 뒤로 미뤄두고
동기코드가 다 끝나면 그제서야 미뤄두었던 일들을 시키는 것이다. 

생각해보면 동기 비동기로 나누는게 당연한 것이다.
왜냐하면 내부가 아닌 외부에서 정보를 받아오는데 받아올 때 까지 주구장창 기다린다면
그 다음 코드들로 넘어가지 않으니까말이다??

(근데 여기서 궁금한게 어떤게 비동기이고 어떤게 동기인지, 어떻게 구분하고 아는건지..? 모르겠다.. 예를들어 이건 비동기니까 async await을 써야지!!라고 생각하는 것 말이다.)


--> 메인함수가 다 끝난 뒤 eventloop에 의해 callback que에 들어있는 비동기 코드 하나씩  callstack 으로 불러와서 실행

 

의문점 1 비동기 안에 비동기?❔


비동기코드 A, B가 있고 A안에 또 비동기코드 C가 있다고 가정해보자
A가 실행이 되고  비동기코드 C를 만났다. 
의문점 1. 그렇다면 callstack [A] callback que [ B, C] 이렇게 B뒤에 C가 생기는가? 따라서 A가 끝나고 C를 실행시키는 것이 아니라. B를 실행시킨뒤에 C를 실행시키는가?

console.log('1')
setTimeout(() => {
    console.log('2')
    setTimeout(() => {
        console.log('3') 
        },1000)
    },1000)
setTimeout(() => {
        console.log('4') 
    },1000)

 

이런 코드를 실행시켰을 때 1 2 4 3 으로 출력되는 것으로 보아 질문 1대로 실행이 되는 것 같다.

 

 

이어서 이야기 해보자,

비동기하면 빼먹을 수 없는 setTimeout이다 예를들어서

console.log('1')
settimeout(()=>{console.log('2')},0) # 0초 뒤에 () => console.log('2')를 callback que에 넣어!
console.log('3')


이러한 코드가 있다면 1 3 찍히고 2가 출력이 될 것이다. setTimeout은 비동기 함수이기 때문에 메인함수가 다 끝난 다음에 실행이 되기 때문이다.

근데 우리가 1 2 3 순으로 출력을 시키고 싶다면, 당연하겠지만 console.log('2')를 찍고 나서 console.log('3')을 찍고 싶다면 콜백함수에 단순히 추가만 해주면 된다. 

console.log('1')
settimeout(()=>{
    console.log('2')
    console.log('3') 
    },0)


근데 여기서 단순히 console.log가 아닌 콜백함수에 또 비동기 함수 setTimeout을 사용할 수 있다. 

console.log('1')
settimeout(()=>{
    console.log('2')
    settimeout(()=>{
       console.log('3') 
       },0)
    },0)


이런식으로 콜백함수에 콜백함수가 이어지는 것을 콜백 지옥이라고 한다. 단순히 하나만 추가했는데 벌써부터 어질어질하다.

 

Promise🧡


따라서 이 복잡성을 해결하기 위해서 Promise라는 것이 나왔다. 

Promise는 pending fullfilled rejected 이 3가지로 이루어져있다고 한다.

  • Pending(대기) : 비동기 처리 로직이 아직 완료되지 않은 상태
  • Fulfilled(이행) : 비동기 처리가 완료되어 프로미스가 결과 값을 반환해준 상태
  • Rejected(실패) : 비동기 처리가 실패하거나 오류가 발생한 상태

 

 

자바스크립트 Promise 쉽게 이해하기 • 캡틴판교 (joshua1988.github.io)

 

자바스크립트 Promise 쉽게 이해하기

(중급) 자바스크립트 입문자를 위한 Promise 설명. 쉽게 알아보는 자바스크립트 Promise 개념, 사용법, 예제 코드. 예제로 알아보는 then(), catch() 활용법

joshua1988.github.io


콜백 함수 안에 콜백함수를 넣는 것이 아닌 프로미스객체.then() 형식으로 fullfilled 상태가 반환되면 then()안에 비동기 코드를 넣어서 훨씬 수월하게 비동기안에 비동기를 처리할 수 있게 되었다고 한다.

 

resolve가 실행 되는 시점은 외부에서 데이터를 받아온 값이 정상일 때다. 받아올 때 까지 콜백함수는 기다리는 듯하다. 

 

 

프로미스 처리 흐름 출처 MDN

 

비동기의 실행순서가 궁금해서 위와같은 실험을 해보았다. 아래와 같은 결과가 출력이 되었는데 

먼저 '--------1---------'이 출력이 되고 그 다음 getData()가 실행이 되어 그 안에 있는 console.log가 찍힌 것이 보인다.

이 때 fulfilled이 되고 then을 만나면 여기서 다시 비동기로 코드가 실행이 되어지는 것 같다.

따라서 callback que에는 then(getData)가 들어 있어 아래와 같은 상황일 것이다.

callback que [then(getData)] 

이 후에 '------------2------------'를 지나고 다시 getData()를 만나서 callback que에는 아래와 같은 상황일 것이다.
callback que [then(getData), then(console.log) ]

이제 메인 함수가 끝이 났으니 callback que에서 하나씩 stack으로 가져와 실행을 시킬 것이다.

앞에 있는 then(getData)이 stack으로 올라가서 실행이 되고  그 안의 내용을 다시 then(getData)를 비동기로 실행시킨다.

따라서 callback que에는 아래와 같은 상황일 것이다.
callback que [ then(console.log), then(getData) ]

이제 stack에 then(console.log)가 들어가고 출력을 한 뒤에 then이 그 뒤에 없으니 다시 비동기 처리를 해줄 게 없다. 따라서 여기서 종료.. 그래서 1이라는 것이 콘솔에 찍힌 것이다.  이 후 callback que에는 아래와 같은 상황일 것이다.

callback que [ then(getData) ] 

stack에 then(getData)가 들어가고 거기서 then.(console.log)를 que에 집어 넣는다.

callback que [ then(console.log)

이제 이 뒤로 then이 없으니 다시 비동기 처리를 해줄 게 없다 3이 찍히고 끝이 나게 된다.

 

여기서 느낀게 나는, promise를 사용하면. 

console.log('--------1--------')
getData().then(getData).then(getData).then(console.log)
console.log('--------2--------')
getData().then(console.log)

마지막 줄에 있는 then(console.log)는 '--------1---------'과 '---------2------------'사이에 있는 promise chain이 끝이 난 후에 실행이 된다고 생각을 했다.. 즉 콘솔에 3이 먼저 찍히고 그 다음에 1이 찍힐 것이라고 예상을 했다. 

연쇄적인 비동기코드 들을 동기처럼 실행시킨다고 생각했다.

 

하지만 실제로 실험을 해본 결과 비동기 코드를 만날 때 마다 que 뒷자리에 집어 넣는 것 같다.

그리고 프로미스 객체 자체는 비동기로 처리 되는게 아닌 거같고 then을 사용하였을 때 비동기로처리가 되는 것 같다.

 

 

 

 

아무튼 promise를 사용하면 이렇게 then을 사용하여 콜백 지옥을 피할 수 있게 되었지만 then이 계속이어지면 이 또한 지옥이 되어버린다고 한다 따라서

 

promise의 sugar syntax에 해당하는 async await이라는 것이 나왔다. 어떤 함수 앞에 async를 붙이면 promise를 반환하고 그 안에서 promise객체를 반환하는 비동기함수 앞에는 await을 붙일 수 있다.  await을 사용하면 그 비동기가 어떤 값을 리턴할 때 동안 그 뒤로 넘어가지 않는다. ( await를 붙이지 않았다면 비동기 함수이니까 callback que에 함수를 넣어두고 그 다음 코드를 읽음 )

 

 

따라서 async await을 사용하면 비동기 함수를 동기화를 시켜준다고 생각한다

즉 async await의 역할은 직관적으로 비동기 함수를 동기 처리할 수 있도록 해주는 것이다. ( 맞나요 ? )

import fetch from 'node-fetch';
const jebal = async () => {
   const first =  await (await fetch('https://jsonplaceholder.typicode.com/users/1')).json()
   console.log('-----------------------------------------first-------------------------------')
   console.log(first)
   const second =  await (await fetch('https://jsonplaceholder.typicode.com/users/2')).json()
   console.log('-----------------------------------------second-------------------------------')
   console.log(second)

}
 
jebal()

순서대로 잘 처리되어진 것을 볼 수 있다.

예제를 살짝 바꿔서 아까 promise에서 사용했던 걸 다시 사용해보자

aynsc await을 사용하면 전에 내가 promise에서 예상했던 실행순서,

즉, 연쇄적인 비동기코드들을 동기처럼 실행시키는 것이 확인이 된다.

then으로 이루어진 promise chain들은 비동기 코드를 만나면 que의 맨 뒷자리로 이동시키지만,

async await으로 이루어진 것들은 비동기 코드를 만나면 que로 보내지 않고 다 끝날 때 까지 기다린 후 다시 그 다음 비동기 코드를 동기처럼 실행시켜준다.

 

이렇게 하고 끝내려고 했다만..... 

 

의문점 2 실행순서?❔

나는 자연스럽게 async 내의 함수는다 동기적으로 실행이 된다고 생각을 했다. 근데 반은 맞고 반은 틀린 것같다??

아래의 코드를 실행시켜보고 나로썬 받아들이기 어려운 충격적인 결과가 나타났다...🔞

하나씩 천천히 살펴보면 async내에 함수가 동기로 하나 하나 실행이 되다가 await을 한번 만나는 순간 바로 일시정지 해버린다. que에다가 await안에 있는 함수를 집어 넣는 것 같다. 그리고 async함수의 밖으로 빠져나가 그 다음 함수를 차례로 실행시킨다. 그리고 main함수가 끝이 나는 순간 (---------네 번 째 콘솔 --------- 이 찍힌 후)

 

현재 que에는 [getData(20) getData(10) getData(40)]이 들어있을 것이다.

제일 먼저 getData(20)이 stack으로 올라가서 뒤에 1을 더한 후에 다시 한번 await을 만나 que의 뒷자리로 넘어오게 된다.

 

현재 que에는 [getData(10) getData(40) getData(21)]이 들어있을 것이다. 

그 다음 getData(10)이 stack으로 올라가고 await이 없으니 콘솔에 '1번 await 시켰습니다' 라고 출력한 뒤 그 뒤의 나머지 코드를 다 실행시킨 후에 종료된다. ( 더 이상 jebal2엔 await 존재 x )

 

 

현재 que에는 [getData(40) getData(21)]이 들어있을 것이다.

그 다음  getData(40)이 stack으로 올라가서 뒤에 1을 더한 후에 다시 한번 await을 만나 que의 뒷자리로 넘어오게 된다.

 

현재 que에는 [getData(21) getData(41)]이 들어있을 것이다.

그 다음  getData(21)이 stack으로 올라가고 더이상 await이 없으니 콘솔에 '2번 await 시켰습니다' 라고 출력한 뒤 그 뒤의 나머지 코드를 다 실행시킨다 ( jebal엔 await 가 하나 더 존재 무려 3겹 )

그리고 await3겹짜리를 만나서 다시 비동기로 getData(30)을 보낸다.

 

현재 que에는 [getData(41) getData(30)]이 들어있을 것이다.

이제 번갈아 가면서 주거니 받거니

que에는 30 42 , 42 31 , 43 32, 이렇게 돌아가면서 끝낸다.

 

여기서 알 수 있는 점💡

1. await에서 비동기함수를 처리할 때는 하나씩 que에 넣어가면서 기존 비동기 함수가 실행 되듯이 실행된다.

앞서 promise와 같음

(나는 await안에 있는 getData를 비동기를 동기로 처리 해주는 줄 알았다. )

 

2. 하지만 async함수 내에서 await이 끝나기 전까지는 그 다음 코드로 이동이 안된다. 그리고 await이 끝나면 그 다음 await이 없다면 다 실행 시킨 뒤 끝나지만 또 await이 있다면 그 await안의 코드를 비동기로 처리하고 끝날 때 까지 그 다음 코드로 이동 X

  

3. async 함수가 여러개 존재하고 그 안에서 await을 사용 했다면 각 async함수의 await들이 번갈아 가면서 비동기 처리가 된다. 1번내용과 같음 

 

이 예제 위에서 아래와 같이 출력이 되었던 이유는

jebal 함수가 하나밖에 없었기 때문이었다.. 하나 밖에 없다면 await가 끝나기 전까지 비동기에서 계속 같은 getData가 돌기 때문

 

 

아래의 사이트에 내가 궁금했던 점들이 정말 잘 설명이 되어있다.

움짤로 보는 자바스크립트 동작 원리(5) - Promises & Async/Await (tistory.com)

 

움짤로 보는 자바스크립트 동작 원리(5) - Promises & Async/Await

원문 (영어) : https://dev.to/lydiahallie/javascript-visualized-promises-async-await-5gke#syntax 저자 : Lydia Hallie 👩🏻‍💻Thanks to Lydia for this amazing article about Javascript! 🖍 Introduction..

kkangdda.tistory.com

 

'AI 웹개발 트랙 - 내배캠 > 9주차 ~' 카테고리의 다른 글

docker hub, pull, run  (0) 2022.03.07
docker란? docker설치  (0) 2022.03.07
팀과제 그림제작 페이지  (1) 2022.03.03
async await  (0) 2022.02.28
promise 도대체 이게 뭐냐고..ㅜㅜㅜㅜ  (0) 2022.02.28