본문 바로가기
Javascript

이벤트루프, 동기,비동기,Promise

by jennyiscoding 2022. 10. 4.

1. 자바스크립트 엔진에서 비동기를 처리하나?

자바스크립트 엔진은 하나의 메인스레드로 구성되며 비동기 처리를 제공하지 않는다.

대신, 비동기 코드는 정해진 함수를 제공하여 사용한다. 그것을 API라고 한다. API의 예시로 setTimeout, XMLHttpRequest, fetch등의 Web API가 있다. node.js의 경우 파일처리 API, 암호화 API등이 있다. 

 

2.1 동기란?

현재 실행중인 태스크가 종료될 때 까지 다음에 실행될 태스크가 대기하는 방식

태스크를 순서대로 하나씩 처리하므로 실행순서가 보장된다는 장점이 있지만 앞선 태스크가 종료될때까지 이후 태스크들이 블로킹되는 단점이 있다. 

 

예제)

function sleep(func, delay){
	const delayUntil = Date.now() + delay;
	while(Date.now() < delayUntil);
	func();
}
function foo(){
	console.log('foo')
}
function bar(){
	console.log('bar')
}

sleep(foo, 3*1000);
bar()
console.log('hello')

2.2 비동기란?

현재 실행중인 태스크가 종료되지 않은 상태라 하더라도 다음 태스크를 곧바로 실행하는 방식. 실행 순서를 보장할 수 없다는 점이 있다. 타이머함수인 setTimeout, setInterval, HTTP요청, 이벤트핸들러는 비동기 처리방식으로 동작한다. 

function sleep(func, delay){
	setTimeout(()=>{
		func();
	},delay);
}


function foo(){
	console.log('foo')
}
function bar(){
	console.log('bar')
}

sleep(foo, 3*1000);
console.log('hello')
bar()

3. 처리요소

3.1 콜스텍

실행컨텍스트가 콜스택이다. 함수를 실행하면 함수실행 컨텍스트가 순차적으로 콜스택에 푸시되어 순차적으로 실행된다. 자바스크립트는 하나의 콜스택을 사용하기 때문에 최상위 실행 컨텍스트가 종료되어 콜스택에서 제거되기 전까지는 다른 어떤 테스크도 실행하지 않는다. 

 

3.2 태스크 큐

호출 스케줄링을 위한 타이머설정과 함수의 등록은 브라우저 / Node.js가 담당한다. 

setTimeout이나 setInterval과 같은 비동기 함수의 콜백함수 또는 이벤트핸들러가 일시적으로 보관되는 영역이다. 

 

3.3 이벤트루프

이벤트루프는 콜스택에 현재 실행중인 실행 컨텍스트가 있는지, 그리고 태스크 큐에 대기중인 함수(콜백함수, 이벤트 핸들러 등)이 있는지 반복해서 확인한다. 만약 콜스택이 비어있고 태스크 큐에 대기중인 함수가 있다면 이벤트 루프는 순차적으로 태스크 큐에 대기중인 함수를 콜스택으로 이동시킨다. 

 

4. 실행순서 

- 비동기 API가 작동을 해서 함수를 Task Queue에 저장한다. 

- Task Queue는 자바스크립트 엔진이 아닌 별도의 환경에서 동작을 한다. 

- 그 환경이 이 함수의 종료와 동시에 Task Queue에 넣고 실행을 해야한다고 알린다.

- 자바스크립트 메인쓰레드는 현재 하는일이 없을 때 가져와서 실행을 하게 된다.  

 

func()

function func(){

  func2()

}

라는 코드가 있다

4.1. Call stack에 func, func2가 쌓인다. 

4.2. setTimeout같은 경우에는 다른데서 처리가 되는데, 브라우저같은 경우에는 Web API모듈이 있고 이 모듈에서 setTimeout코드에서 10초 뒤에 Task queue에 들어가도록 설정이 되어있다. 즉, 동기처리 종료 후에 10초가 끝나면, 테스크 큐에 콜백함수가 들어간다. 

4.3. 이벤트 루프가 하는 일은 비동기 코드가 끝났을 때 메인쓰레드가 콜스텍이 비워지면, 이벤트루프가 Task queue에 일이 남아있는지 체크한다. 이벤트 루프는 콜스텍이 비워질 때 테스크 큐에서 콜백함수를 꺼내 콜스텍에 넣는다. 

4.4. 그리고 Call stack이 비워졌을 때 Task queue가 다시한번 체크가 된다. 

 

*비동기 코드를 처리하는 이벤트 루프는 자바스크립트 엔진 외부에 있다. 

*API 모듈은 요청을 처리 후 테스크 큐에 콜백 함수를 넣는다. 

*자바스크립트 엔진은 콜스텍이 비워지면 테스크 큐의 콜백함수를 실행한다. 

*call stack은 들어온 역순으로 실행이 되고 task queue는 들어온 순서대로 처리가 된다. 

 

5. Promise

전통적인 콜백 패턴이 가진 단점을 보완하여 비동기 처리시점을 명확하게 표현한 것이다. 

Promise는 Task queue가 아닌 Job queue를 사용한다(Job queue는 Task queue보다 우선순위가 높다)

아래를 예로 보면, 

let g=0;
setTimeout(()=>{g=10},0)
console.log(g)

setTimeout함수는 비동기함수이므로 g=10이 나중에 선언이 되어서 g에는 10은 들어가지 않고 0이 들어간다. 

 

다른 예를 보면은

const get = (url, successCallback, failureCallback) => {
	const xhr = new XMLHttpRequest;
	xhr.open('GET', url)
	xhr.send();
	xhr.onload = () => {
		if(xhr.status == 200){
			return JSON.parse(xhr.response)
		}else{
			console.error('error')
		}
	}
}
get('https://jsonplaceholder.typicode.com/posts/1')

이거는 순서대로 return 되지 않는다. 

그래서 순서대로 작동시키려면은 아래와 같이 해야한다. 

const get = (url, successCallback, failureCallback) => {
	const xhr = new XMLHttpRequest;
	xhr.open('GET', url)
	xhr.send();
	xhr.onload = () => {
		if(xhr.status == 200){
			successCallback(JSON.parse(xhr.response))
		}else{
			failureCallback(xhr.status)
		}
	}
}
get('https://jsonplaceholder.typicode.com/posts/1',console.log,console.error)

하지만 이 방법의 경우 콜백헬에 빠지게 된다. 

 

세번째 예로, 에러처리가 되지 않는다. 

try {
	setTimeout(() => {
		throw new Error('에러 발생');
	}, 1000);
} catch (error) {
	console.error('내가 에러를 잡을 꺼야!');
}

==> 내가 에러를 잡을 꺼야 는 출력되지 않는다. 

 

이제 프로미스의 필요성은 알게되었다. 

 

5.1 프로미스의 생성

Promise생성자 함수를 new연산자와 함께 호출하면 프로미스 객체를 생성한다. Promise생성자 함수는 비동기 처리를 수행할 콜백함수를 인수로 전달받는데, 이 콜백함수는 resolve와 reject 함수를 인수로 전달받는다. 

const promise = new Promise((resolve, reject)=>{
	if(/*성공 */){
		resolve('result')
	}else{
		reject('failure reason')
	}
})

Promise생성자 함수가 인수로 전달받은 콜백함수 내부에서 비동기처리를 수행한다. 이때 비동기처리가 성공하면 resolve함수를 호출하고 실패하면 reject함수를 호출한다. callback는 executor라고 표현을 한다. 이 executor함수는 내부에 reject, resolve를 사용함으로써 조작할 수 있다. 프로미스는 현재 비동기 처리가 어떻게 진행되고 있는지를 나타내는 상태 정보를 갖는다. 

1. pending : 비동기처리가 아직 수행되지 않은 상태 - 프로미스가 생성된 직후 기본 상태

2. settled상태 : 비동기 처리가 수행된 상태. 아래 2가지로 나뉨. 

2.1 fulfilled : 비동기 처리가 수행된 상태(성공) - 비동기 처리가 성공 시 resolve 함수 호출 - 결과 : value

2.2 rejected : 비동기 처리가 수행된 상태(실패) - 비동기 처리가 실패 시 reject 함수 호출 - 결과 : error

 

예)

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <input type="text">
    <script src="index.js"></script>
</body>
</html>

index.js

const fulfilled = new Promise(resolve => {
	resolve(10)
})

 

그리고 크롬에서 fullfilled를 쳐보면, 

state는 fulfilled가 나오고 Result는 10이 된다. 성공했으니 value가 나오는 것이다. 

 

프로미스의 비동기 처리상태가 변화하면 후속처리 메서드에 인수로 전달한 콜백함수가 선택적으로 호출된다. 모든 후속처리 메서드는 프로미스를 반환하며 비동기로 동작한다.

 

5.1 프로미스의 후속처리 메서드

Promise.prototype.then

첫번째 콜백함수 : resolve 함수가 호출된 상태가 되면 호출한다. 

두번째 콜백함수 : resject 함수가 호출된 상태가 되면 호출한다. 

 

예시)

성공 시

new Promise((resolve, reject) => resolve('fulfilled'))
	.then(v => console.log(v), e => console.error(e))

실패 시

new Promise((resolve, reject) => {
	reject(new Error('rejected'))
})
	.then(v => console.log(v), e => console.error(e))

Promise.prototype.catch

catch메서드는 콜백함수를 인수로 전달받는다. catch메서드의 콜백함수는 프로미스가 reject상태인 경우에만 호출된다. 

예제)

new Promise((_,reject)=>reject(new Error('rejected')))
	.catch(e => console.log(e))

Promise.prototype.finally

finally메서드는 한개의 콜백함수를 인수로 받는다. 성공/실패와 상관없이 무조건 한번 호출된다. 

예제)

new Promise((_,reject)=>reject(new Error('rejected')))
	.finally(() => console.log('finally'))

 

5.2 프로미스의 에러처리

비동기 함수는 Promise를 반환한다. 이것을 받아 에러처리를 하는 방식은 2가지가 있다. 

then의 두번째 콜백함수로 넣는것, 그리고 catch를 이용하는것. 

 

5.3 프로미스체이닝

then,catch,finally후속처리처리 메서드는 언제나 프로미스를 반환하므로 연속적으로 호출할 수 있다. 이것을 프로미스체이닝이라고 한다. 

const promiseGet = url => {
	return new Promise((resolve, reject)=>{
		const xhr = new XMLHttpRequest();
		xhr.open('GET',url);
		xhr.send();

		xhr.onload = () => {
			if(xhr.status === 200){
				resolve(JSON.parse(xhr.response));
			}else{
				reject(new Error(xhr.status))
			}
		}
	}) 
}

const url = 'https://jsonplaceholder.typicode.com';

promiseGet(`${url}/posts/1`)
	.then(({userId})=> promiseGet(`${url}/users/${userId}`))
	.then(userInfo => console.log(userInfo))
	.catch(err => console.error(err))

 

6. 프로미스와 비동기의 처리순서

console.log('start');

setTimeout(() => console.log('setTimeout'), 0);

Promise.resolve('Promise').then((res) => console.log(res));

console.log('End');

순서 : start -> End -> Promise -> setTimeout 순서로 처리가 된다. 

setTimeout비동기보다 Promise가 먼저 처리된다고 생각하면 된다. 

Promise는 잡큐(마이크로테스크)에 들어가고 그냥 setTimeout은 Task Queue(메크로테스크)에 들어가기 때문이다. 잡큐는 테스크큐보다 항상 먼저 실행이 된다. 

 

7. 일반 콜백함수를 Promise로 변환하기

일반 콜백함수 

function runInDelay(callback, seconds) {
	if (!callback) {
		throw new Error('callback 함수가 아니다 ');
	}
	if (!seconds || seconds < 0) {
		throw new Error('0 보다 큰 ms초 단위가 들어와야한다');
	}
	setTimeout(callback, seconds * 1000);
}

try {
	runInDelay(() => {
		console.log('타이머 종료');
	}, 2);
} catch (error) {
	console.log(error);
}

프로미스 

function runTimer(seconds) {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			if (!seconds || seconds < 0) {
				reject(new Error('0 보다 큰 ms초 단위가 들어와야한다'));
			} else {
				resolve(console.log(`${seconds} 초가 지났습니다`));
			}
		}, seconds * 1000);
	});
}

runTimer(2)
	.then(() => console.log('타이머 종료'))
	.catch(console.error)
	.finally(() => console.log('프로그램 종료'));

 

7.2 일반 콜백함수를 Promise로 변환하기

일반

class User {
	constructor(id, password) {
		this.id = id;
		this.password = password;
	}
}

class UserManager {
	loginUser(user, input, onLogin, onFail) {
		setTimeout(() => {
			if (input[0] === user.id && input[1] === user.password) {
				onLogin(user);
			} else {
				onFail(new Error('login failed'));
			}
		}, 2000);
	}

	getRoles(user, onSuccess, onError) {
		setTimeout(() => {
			if (!user || !(user instanceof User)) {
				onError(new Error('access denied'));
			} else if (user.id === 'admin') {
				onSuccess({ name: user.id, role: '관리자' });
			} else {
				onSuccess({ name: user.id, role: '회원' });
			}
		}, 1000);
	}
}

const um = new UserManager();
const user1 = new User('admin', '1234');
const user2 = new User('test', '1111');

const input1 = ['admin', '1234'];
const input2 = ['test', '1111'];
const input3 = ['test', '3333'];

um.loginUser(
	user2,
	input2,
	(user2) => {
		um.getRoles(
			user2,
			(member) => {
				console.log(`${member.name}님은 ${member.role} 입니다 `);
			},
			(error) => {
				console.log(error);
			}
		);
	},
	(error) => {
		console.log(error);
	}
);


console.log(undefined == null)

 

Promise

class User {
	constructor(id, password) {
		this.id = id;
		this.password = password;
	}
}

class UserManager {
	loginUser(user, input) {
		return new Promise((resolve, reject) => {
			setTimeout(() => {
				if (input[0] === user.id && input[1] === user.password) {
					resolve(user);
				} else {
					reject(new Error('login failed'));
				}
			}, 2000);
		});
	}

	getRoles(user) {
		return new Promise((resolve, reject) => {
			setTimeout(() => {
				if (!user || !(user instanceof User)) {
					reject(new Error('access denied'));
				} else if (user.id === 'admin') {
					resolve({ name: user.id, role: '관리자' });
				} else {
					resolve({ name: user.id, role: '회원' });
				}
			}, 1000);
		});
	}
}
const um = new UserManager();
const user1 = new User('admin', '1234');
const user2 = new User('test', '1111');

const input1 = ['admin', '1234'];
const input2 = ['test', '1111'];
const input3 = ['test', '3333'];

um.loginUser(user2, input2)
	.then((user) => {
		return um.getRoles(user);
	})
	.then((member) => {
		return `${member.name}님은 ${member.role} 입니다 `;
	})
	.then((result) => {
		return console.log(result);
	})
	.catch((error) => {
		return console.log(error);
	});

맨 마지막 부분은 아래과 같이 줄일 수 있다. 

um.loginUser(user2, input2)
	.then(um.getRoles)
	.then((member) => `${member.name}님은 ${member.role} 입니다 `)
	.then(console.log)
	.catch(console.log);

 

8. Promise.all

Promise.all을 사용하지 않았을 때(총 4초가 걸린다)

Dog가 실행된 다음에 Cat이 실행된다. 

function getCat() {
	return new Promise((resolve) => {
		setTimeout(() => {
			resolve({ kind: '고양이', name: '나비' });
		}, 1000);
	});
}

function getDog() {
	return new Promise((resolve) => {
		setTimeout(() => {
			resolve({ kind: '강아지', name: '바둑이' });
		}, 3000);
	});
}

function getHamster() {
	return Promise.reject(new Error('햄스터 도망감'));
}

getCat()
	.then((cat) => getDog().then((dog) => [cat, dog]))
	.then((result) => console.log(result));

Promise.all을 사용했을 때

병렬로 실행하고싶은 Promise함수들을 넣어준다. 

function getCat() {
	return new Promise((resolve) => {
		setTimeout(() => {
			resolve({ kind: '고양이', name: '나비' });
		}, 1000);
	});
}

function getDog() {
	return new Promise((resolve) => {
		setTimeout(() => {
			resolve({ kind: '강아지', name: '바둑이' });
		}, 3000);
	});
}

Promise.all([getCat(), getDog()]) 
	.then((pets) => console.log(pets));

결과

8.1 Promise.all의 문제점

function getCat() {
	return new Promise((resolve) => {
		setTimeout(() => {
			resolve({ kind: '고양이', name: '나비' });
		}, 1000);
	});
}

function getDog() {
	return new Promise((resolve) => {
		setTimeout(() => {
			resolve({ kind: '강아지', name: '바둑이' });
		}, 3000);
	});
}

function getHamster() {
	return Promise.reject(new Error('햄스터 도망감'));
}

//병렬처리의 문제점 ? 하나라도 잘못된게 있으면 에러를 띄워버린다. 
Promise.all([getCat(), getDog(), getHamster()]) 
	.then((pets) => console.log(pets))
	.catch(console.log);

8.2 Promise.all의 문제점 해결방법 : Promise.allSettled

function getCat() {
	return new Promise((resolve) => {
		setTimeout(() => {
			resolve({ kind: '고양이', name: '나비' });
		}, 1000);
	});
}

function getDog() {
	return new Promise((resolve) => {
		setTimeout(() => {
			resolve({ kind: '강아지', name: '바둑이' });
		}, 3000);
	});
}

function getHamster() {
	return Promise.reject(new Error('햄스터 도망감'));
}

Promise.allSettled([getCat(), getDog(), getHamster()]) 
	.then((pets) => console.log(pets))
	.catch('error: ', console.log);

결과 : 

 

9. Promise.race

function getCat() {
	return new Promise((resolve) => {
		setTimeout(() => {
			resolve({ kind: '고양이', name: '나비' });
		}, 1000);
	});
}

function getDog() {
	return new Promise((resolve) => {
		setTimeout(() => {
			resolve({ kind: '강아지', name: '바둑이' });
		}, 3000);
	});
}

function getHamster() {
	return Promise.reject(new Error('햄스터 도망감'));
}

//처음에 들어오는 애만 나옴. //결과 : { kind: '고양이', name: '나비' }
Promise.race([getCat(), getDog()]) 
	.then((pets) => console.log(pets));

둘중 빠른거 1개만 실행한다. 

 

 

 

 

 

 

 

 

 

 

 

질문할 것. 

const PromiseFunction = new Promise((resolve, reject)=>{
    resolve("hey")
})

PromiseFunction
    .then((result)=>{
        console.log(result)
    })

이것은 가능한데!

const PromiseFunction = () => {
    return new Promise((resolve, reject)=>{
        resolve("hey")
    })
}

PromiseFunction
    .then((result)=>{
        console.log(result)
    })

이것은 왜 에러가 나나요 ㅠㅠ?