[Javascript] 자바스크립트가 비동기를 처리하는 방법 - callback function, Promise, async/await

김뀨뀨·2022년 8월 10일
0

Javascript

목록 보기
4/4
post-thumbnail

동기 vs. 비동기

💡 동기적(synchronized)

동기적이라는 것은 시작 시점과 완료 시점이 같은 상황을 말한다.

예를 들어, 동기적인 카페에서 직원 1명이 주문을 받았다.
👧🏻 손님 A: 아이스 카페라떼 주문이요!
라고 했을 때, 직원은 손님 A에게 ☕️ 카페라떼를 만들어 주었다.
그리고 손님 A의 주문에 대한 동작이 끝남과 동시에 손님 B의 주문을 받는다.
🧑🏻 손님 B: 아이스 카페모카 주세요
직원은 한명이기 때문에 동시에 카페라떼와 카페모카를 만들 수 없다. 이런 식으로 동작이 순차적으로 처리되는 것을 동기적이라고 한다.

동기적인 것은 blocking 하다. 직원이 손님 A의 카페라떼를 만드는 동안 손님 B의 주문을 받을 수 없기 때문이다.

💡 비동기적(asynchronized)

비동기적인 것은 동기적인 것과 반대로 시작 시점과 완료 시점이 같지 않고, 요청에 대한 결과가 동시에 일어나지 않는다.

☕️ 비동기적인 카페에서, 직원은 손님 A의 주문과 손님 B의 주문을 모두 받고 한꺼번에 카페라떼와 카페모카를 만든다. 어떤 것이 먼저 완성될지는 예측할 수 없다. 나중에 주문한 손님 B가 먼저 커피를 받을 수도 있다!

따라서 비동기적인 것은 non-blocking 하다. 손님 A의 주문을 받았다고 손님 B의 주문을 처리하지 못하는 것이 아니다.

비동기적인 Javascript

📌 Node.js

javascript의 런타임인 node.js은 non-blocking하고 비동기적이다.

📌 비동기의 사례

  • DOM Element의 이벤트 핸들러: 마우스, 키보드 입력, 페이지 로딩 등등
  • 타이머: 타이머 API(setTimeout 등), 애니메이션 API(requestAnimationFrame)
  • 서버에 자원요청 및 응답: fetch API, AJAX
    등이 있다.

📌 비동기적이어야 효율적인 작업

  • 백그라운드 실행, 로딩 창 실행
  • 서버로 요청을 보내고 응답을 기다리는 작업
  • 큰 용량의 파일을 로딩하는 작업


💭 유튜브 화면에서, 영상이 비동기적으로 로딩되는 동안 다른 버튼을 클릭할 수도있고, 다른 페이지로 이동할 수도 있다. 만약 동기적으로 로딩된다면 사용자는 영상이 로딩되는 동안 다른 작업을 하지 못하고 꼼짝없이 기다려야 한다.

비동기 호출(asynchronous call)

callback function

🧐 그렇다면 javascript에서 어떻게 비동기적으로 작업을 수행할 수 있을까?
→ 콜백 함수를 통해 구현할 수 있다!

✏️ callback function
다른 함수의 전달인자(argument)로 넘겨주는 함수

callback 함수를 넘겨 받은 함수에게는 선택지가 있다.
1. callback 함수를 즉시 실행할 수도 있고,
2. 나중에 비동기적으로(asynchronously) 실행할 수도 있다.

document.querySelector('#btn').addEventListener('click', function (e) {
	console.log('button clicked');
});

버튼 요소에 이벤트 핸들러를 연결해주는 코드이다. addEventListner는 콜백 함수를 두번째 인자로 받고 있다.
이 때, 함수 자체를 연결하는 것이지, 함수 실행을 연결하는 것이 아니다.

const handleBtnClick = (e) => { console.log('button clicked'); }

document.querySelector('#btn').addEventListenr('click', handleBtnClick);

따라서 위 코드처럼 이벤트 핸들러가 따로 구현되어 있으면 전달할 때 함수 자체를 전달해주어야 한다.

callback 비동기 함수 전달 패턴

const printString = (string) => {
	setTimeout(
		() => {
			console.log(string)
		},
		Math.floor(Math.random() * 100) + 1
		)
}

const printAll = () => {
	printString('A')
	printString('B')
	printString('C')
}
printAll()

위 코드에서, setTimeout 함수는 인자로 콜백 함수를 받고 있다.

콜백 함수는 비동기적으로 실행되기 때문에, 비록 printString의 인자로 'A', 'B', 'C'를 차례로 넘겨주었다고 해도, random 함수에 의해 기다리는 시간이 결정되고 실행 결과를 예측할 수 없다.

이렇게 asynchronously 하게 처리하면 결과를 예측할 수 없다는 문제가 생긴다.

callback 핸들링

위 코드에서 A, B, C를 차례대로 출력하고 싶다면 어떻게 해야 할까? callback 함수를 이용하면 된다.

const printString = (string, callback) => {
	setTimeout(
		() => {
			console.log(string)
			callback()
		},
		Math.floor(Math.random() * 100) + 1
		)
}

const printAll = () => {
	printString('A', () => {
		printString('B', () => {
			printString('C', () => {})
		})
	})
}
printAll()

마찬가지로 random 함수에 의해 기다릴 시간이 결정되지만 순차적으로 실행된다.

Promise

🧐 그런데 비동기적으로 관리해야 할 일들이 많아진다면?
위와 같은 방식으로는 무한 callback의 굴레에 빠질 수 있다. 그래서 등장한 것이 Promise이다. Promise는 callback chain을 관리한다.

MDN: Promise
공식 문서에 따르면

✏️ Promise 객체는 비동기 작업이 맞이할 미래의 완료 또는 실패와 그 결과 값을 나타낸다.
또한, Promise는 세가지 상태를 가질 수 있다.
1.대기(pending): 이행하지도, 거부하지도 않은 초기 상태
2.이행(fulfilled): 연산이 성공적으로 완료됨
3.거부(rejected): 연산이 실패함

Promise를 사용하면 비동기 연산을 동기 연산처럼 사용할 수 있다.
위에서 봤듯이 async 하다는 것은 요청에 대한 결과가 동시에 일어나지 않기 때문에 완료 시점을 예측할 수 없다는 말인데, Promise는 이 비동기 작업이 미래의 어떤 시점에 결과를 제공해줄 것이라고 약속(Promise)을 해준다.

Promise로 callback chain 관리하기

자세한 문법에 대한 설명은 생략하고, 앞서 계속 봤던 A, B, C 출력하기를 Promise를 통해 해보자.

const printString = (string) => {
	return new Promise((resolve, reject) => {
		setTimeout(
			() => {
				console.log(string)
				resolve()
				},
				Math.floor(Math.random() * 100) + 1
				)
		})
}

const printAll = () => {
	printString('A')
		.then(() => {
			return printString('B')
		})
		.then(() => {
			return printString('C')
		})
}
printAll()

.then() 이라는 메서드를 사용해 순차적으로 함수를 실행할 수 있다.
하지만 마찬가지로 처리해야할 작업이 많아진다면 무한 .then()의 굴레에 빠질 수 있다 🥲

async, await

그래처 최종적으로 async와 await가 등장한다.

✏️ async function
async 키워드를 통해 AsyncFunction 객체인 비동기 함수를 정의할 수 있다. 이 async function은 Promise를 사용해 결과를 반환한다.
다음과 같은 형태로 선언할 수 있다.

async function name([param..]) { statements }

✏️ await
async 함수는 await 식을 포함할 수 있다.
await 식은 async 함수의 실행을 일시 중지하고 전달된 Promise의 해결을 기다린 다음 async 함수의 실행을 다시 시작한다.

차례대로 A, B, C를 출력하는 작업을 async/await로 해보자.

const printString = (string) => {
	return new Promise((resolve, reject) => {
		setTimeout(
			() => {
				console.log(string)
				resolve()
				},
				Math.floor(Math.random() * 100) + 1
				)
		})
}

const printAll = async () => {
	const a = await printString('A');
	
	const b = await printString('B');
	
	const c = await printString('C');
}
printAll()

callback 감옥에서 벗어날 수 있다!

profile
개발로 밥벌이 하고 싶은 사람

0개의 댓글