비동기와 동기/callback/promise/이벤트 루프

셀라문·2022년 3월 14일
0

JavaScript

목록 보기
25/27

동기, 비동기

동기는 요청 후 응답을 받아야 다음 동작을 실행하는 방식을 말하며
비동기는 요청을 보낸 후 응답과 관계없이 다음 동작을 실행하는 방식이다.

자바스크립트는 단일 스레드 프로그래밍 언어로 단일 호출 스택이 있어 한 번에 하나의 일을 처리할 수 있다. 그러므로 자바스크립트는 동기 방식으로 진행이 된다.
하나의 호출 스택만 있기 때문에 하나의 함수를 처리하는데 매우 오랜 시간이 걸린다면 다음 실행해야할 함수에 지장을 줄 수 있다는 문제가 발생한다.

예를 들어 웹 페이지를 사용자에게 보여줄 때 해당 웹 페이지에 있는 모든 데이터(사진, 글 등)를 받고 나서야 화면이 보여진다고 하자.
서버에서 데이터를 모두 받아올 때까지 시간이 오래 걸릴 수 있으며 사용자 입장에서 웹 페이지를 보는데 너무 느려서 답답할 수 있다.
그러나 데이터를 받아오는 일을 하는 도중에 우선 웹 페이지의 기본 레이아웃을 보여주고 보여줄 수 있는 것들을 우선 보여주는 것이 더 바람직할 것이다.
마치 세탁기가 돌아가는 도중에 라면을 끓이는 것처럼!

이것이 비동기의 필요성이다. 그리고 아래와 같은 방법으로 이 문제를 해결할 수 있다.

  • 비동기적 callback 함수 사용
  • ES6 Promise
  • ES8 async await

앞서 자바스크립트는 하나의 호출스택을 가진 단일 스레드 프로그래밍 언어라고 말했다.
따라서 혼자서 비동기를 구현할 수 없다. 위 코드에서 어떻게 비동기가 구현된 것일까?

자바스크립트 엔진만으로는 비동기적으로 구현할 수 없으므로 자바스크립트 실행 환경(Runtime)은 브라우저에서 제공하는 Web API를 사용하여 비동기를 구현하게 된다.
DOM 이벤트, setTimeout과 같은 비동기 함수는 web API를 호출하여 콜백 함수를 콜백 큐에 넣는다.
콜백 함수들이 담긴 큐는 특정 시점에서 콜백을 실행시키는 방식이다.

Callback

콜백 함수란 다른 함수의 인자로 이용되는 함수이며 어떤 이벤트에 의해 호출되는 함수이다. 콜백 함수는 아래 코드와 같이 동기적으로 사용될 수도 있다. primtImmediately 라는 이벤트 함수가 인자로 함수를 받는 코드라는 점을 주목하자.

function printImmediately(callBackFunction) { 
  callBackFunction() 
}
printImmediately(()=>console.log('synchronous callback'))

단순히 인자로서 함수를 받아 그 함수를 실행한다.
일반적인 자바스크립트 코드이기에 당연히 함수 호출 스택에 따라 동기적으로 실행된다.
우리가 구현하고자 하는 비동기적 과정은 여기서 일어나지 않는다.
우리는 비동기적 callback 함수를 사용해야 한다.

아래와 같이 비동기적인 콜백 함수Asynchronous callback 예제를 보자.

function printWithDelay(callback, sec){
  setTimeout(callback, sec*1000)
}
printWithDelay(()=>console.log("async callback"), 2000)
console.log("hello")

동기적인 방식을 따른다면 printWithDelay() 가 모두 완료한 뒤 "hello"를 출력해야 한다.
하지만setTimeout 을 사용해 2초 뒤에 "async callback"이 비동기적으로 출력된다.
따라서 "hello"가 먼저 출력이 되고 2초 뒤에 "async callback"이 출력이 된다.

다른 예시로 사용자가 어떤 버튼을 클릭했을 때 실행할 함수도 비동기 콜백 함수이다. 그 이전까진 실행하지 않다가 클릭이라는 이벤트가 발생했을 때 콜백 함수를 실행하는 것이다.

  • 1000 = 1초

🍓 구현 과정

(consol.log "끝"이 출력되기전에 setTimeout"중간"의 타임아웃이 먼저 끝나 callback queue로 간다고 가정)
call stack에 consol.log "시작"이 들어감과 동시에 출력
setTimeout"중간"이 call stack에 들어갔다가 비동기함수이기 때문에 Web API에 들어감
call stack에 consol.log "끝" 들어감
Web API에 있던 setTimeout"중간"의 timeout이 끝나서 callback queue로 감
하지만 아직 콜스택이 비어있지 않은 상태. consol.log "끝" 이 출력되고 빈 자리에
setTimeout"중간"이 들어감
비로소 setTimeout"중간" 출력

콜백지옥

비동기 구현을 위한 첫 번째 방법인만큼 큰 문제가 있다.
콜백 함수가 콜백 함수를 부르고, 그 콜백 함수가 또 다른 콜백함수를 부르는 이른바 콜백 지옥이 발생하는 것이다.

많은 중첩함수가 생겨 가독성과 유지보수면에 끔찍한 코드가 발생한다. 이를 해결하기 위해 Promise가 나오게 되었다.

promise

Promise는 프로미스가 생성된 시점에는 알려지지 않았을 수도 있는 값을 위한 대리자로, 비동기 연산이 종료된 이후에 결과 값과 실패 사유를 처리하기 위한 처리기를 연결할 수 있습니다.
프로미스를 사용하면 비동기 메서드에서 마치 동기 메서드처럼 값을 반환할 수 있습니다.
다만 최종 결과를 반환하는 것이 아니고, 미래의 어떤 시점에 결과를 제공하겠다는 '약속'(프로미스)을 반환합니다.

promise는 동기이나, then을 만나면 엔진은 프로미스를 비동기로 인식합니다.

Promise는 다음 중 하나의 상태를 가집니다.

  • 대기(pending): 이행하지도, 거부하지도 않은 초기 상태.
  • 이행(fulfilled): 연산이 성공적으로 완료됨.
  • 거부(rejected): 연산이 실패함.

resolve(성공)가 되면 .then 실행
reject(실패)면 .catch 실행
.finally는 아무 상관 없이 마지막에 실행

콜백 함수를 사용하지 않고 Promise object를 틍해 지옥에 빠지지 않고 어떻게 깔끔하게 비동기 함수를 처리할 수 있는지 바로 코드부터 확인해보자.
에러 처리에 관한 내용은 이 글에서 자세히 다루지 않는다.

const myPromise = new Promise((resolve, reject)=>{
  console.log("doning some heavy work: network, read files")
  setTimeout(()=>{
    // resolve('hi');
    reject(new Error('this is error msg'));
  }, 2000);
})

myPromise.then(value=>{
  console.log(value) // resolve 가 있다면 'hi' 출력 
})
.catch(error=>{
  console.log(error) // reject에 있는 'this is error msg' 출력
})
.finally(()=>{
  console.log('finally!!')
})

위 코드의 경우 2초뒤에 'this is error msg'와 'finally!!' 가 출력된다. 만약, resolve('hi') 가 주석처리 되지 않았다면 .then() 에서 'hi' 가 출력되었을 것이다.

콜백 함수의 중첩과 같이 여러 promise 객체를 만들어 중첩시킬 수도 있다. 아래 예시 코드를 보자. 포켓몬 파이리의 진화 과정을 1초마다 보여주는 코드이다.

const initialPokemon = () =>
  new Promise((resolve, reject)=>{
    setTimeout(()=>resolve('파이리'), 1000)
  });

const nextPokemon = prevPokemon =>
  new Promise((resolve, reject)=>{
    setTimeout(()=>resolve(`${prevPokemon} => 리자드`), 1000)
  });

const finalPokemon = prevPokemon =>
  new Promise((resolve, reject)=>{
    setTimeout(()=>resolve(`${prevPokemon} => 리자몽`), 1000)
  });

initialPokemon() // 1초 소요
  .then(prev=>{
    console.log(prev) // 파이리
    return nextPokemon(prev) // 1초 소요
  })
  .then(prev=>{
    console.log(prev) // 파이리 => 리자드
    return finalPokemon(prev) // 1초 소요
  })
  .then(console.log) // 파이리 => 리자드 => 리자몽

확실히 콜백 지옥보다 훨씬 로직이 깔끔하고 가독성도 뛰어나다는 것을 알 수 있다.
위 코드들을 사용하는 방법의 경우 promise chaining으로 then , catch 매소드를 사용하여 비동기를 관리하고 있다.

만일 1234가 있는데, 124는 성공 3은 실패이나 실패코드가 없다면 3부터 4까지도 오류로 뜬다. 3에 catch를 넣어준다면 4는 resolve가 적절히 작동된다.

const starbucks = function (coffeeName) {
	return new Promise((resolve, reject) => {
		if (coffeeName === '아메리카노') {
			resolve('아메리카노 한잔입니다.');
		} else {
			reject('아메리카노는 없습니다.');
		}
	});
};

starbucks('아메리')
	.then((res) => console.log(res))
	.catch((rej) => console.log(rej))
	.finally(() => console.log('감사합니다'));
    // 아메리카노는 없습니다. 감사합니다
    
    starbucks('아메리카노')
	.then((res) => console.log(res))
	.catch((rej) => console.log(rej))
	.finally(() => console.log('감사합니다'));
    // 아메리카노 한잔입니다. 감사합니다

🍓 구현 과정

(setTimeout"중간"의 타임아웃이 then보다 먼저 끝났다고 가정(먼저 callback queue로 들어감))

  • call stack에 consol.log "시작"이 들어감과 동시에 출력
  • setTimeout"중간"이 call stack에 들어갔다가 비동기함수이기 때문에 Web API에 들어감
  • 비동기가 된 promise .then도 Web API에 들어감
  • call stack에 consol.log "끝" 들어감
  • setTimeout"중간"의 타임아웃이 then보다 먼저 끝났다고 가정(먼저 callback queue로 들어감)
  • 이벤트 루프가 돌아가면서 콜스택에 뭔가 있음을 감지. consol.log "끝" 출력함.
  • 우선순위에 의해 promise .then이 먼저 call stack에 들어감
    console.log ("프로미스")를 출력하면서 then도 빠져나옴
  • 빈 call stack에 setTimeout"중간" 들어가고 출력됨

async & await

asynce await 는 비동기처리의 최신문법이다. 기존의 promise와 다른 것은 아니고, syntatic sugar일 뿐이다.
promise를 사용할 경우에 callback처럼 chaining이 일어나는 것은 마찬가지이다.
따라서 콜백 지옥의 문제가 어느정도 나타날 수 있다는 것이다.

하지만 async await을 사용하면 promise를 '깔끔한 스타일'로 작성할 수 있다.
그러나 무조건 async await이 절대적으로 깔끔한 방법은 아니고 상황에 따라 적절한 것을 선택하면 된다.

  • promise.resolve는 async로, then은 await으로 생각하면 된다.

위 사진과 밑 사진은 똑같이 작동한다.
promise를 async를 사용해 간략하게 만들었다.

await는 async가 쓰인 함수 안에서만 작동한다.


이렇게 작성하게 된다면 여기서도 콜백 지옥에 빠질 수 있다.


이렇게 바꿔쓸 수 있다.

하지만, 저렇게 병렬 처리하는 건 지저분하므로

유용한 promise를 사용 할 수 있다.

바나나를 1초, 사과를 2초로 변경 한 후 바나나먼저 출력할 수 있게 하는 방법

async & await가 무조건 좋은건 아니고, 상황에 맞게 쓰면 된다.

참고 : 드림코딩 by 엘리

profile
취미로 하는 공부기록장

0개의 댓글