TIL

동기(Sync) & 비동기(Async) & Promise

김영재0412 2022. 11. 25. 22:03

동기(Sync) & 비동기(Async)

 

 

"동기로 실행된다" 라고 함은, 먼저 실행된 코드의 결과가 나올때까지 대기하는것을 말한다.

  • 예시로 정원이 30명인 놀이기구가 있다고 가정할 때 놀이기구를 한번 태우는데 약 5분이 걸린다고 가정하면 놀이기구를 막 태우기 시작한 시점에서는 대기자 모두 5분이 지나 이미 놀이기구에 탑승했던 사람들이 다 내리기 전까지는 탑승을 할 수 없다. 이것은 놀이기구 탑승/하차가 동기적으로 관리된다고 볼 수 있다.

일반적으로 "비동기로 실행된다" 라고 함은, 실행된 순서와 관계 없이 결과가 나오는것을 말한다.

  • 예시로 정원이 최대 30명인 맛집이 있다고 가정할 때 입장 순서는 선착순이고 퇴장 순서는 다 먹은 사람이 바로 나올 수 있다. 다 먹은 사람이 나오면 나온 사람 수 만큼 다시 입장 할 수 있고 사람마다 먹는 시간은 모두 다르다. 이것은 입/퇴장이 비동기적으로 처리된다고 볼 수 있다.

 

 

Blocking Model & Non-Blocking Model

Blocking Model은 코드의 실행이 끝나기 전까지 실행 제어권을 다른곳에 넘기지 않아 다른 작업을 하지 못하고 대기하는 것을 말하며

Non-Blocking Model은 코드의 실행이 끝나지 않아도 실행 제어권을 다른곳에 넘겨 다음 코드가 실행될 수 있는것을 말한다.

  • 자바스크립트는 Async + Non-Blocking Model을 채용하여 현재 실행중인 코드의 실행이 끝나지 않아도 다음 코드를 호출한다.
  • 결론적으로 자바스크립트는 각 명령들이 순서대로 실행될 수 있게 구현되어 있지만, Non-Blocking Model에 의해 동기적 명령이 아닌 모든 함수는 비동기적으로 실행된다.

 

즉, 비동기는 제어권을 넘기면(Non-blocking) 다른 코드도 실행될 수 있으므로 비동기 처리가 가능하지만, 동기는 제어권을 넘기지 않으면(Blocking) 비동기 처리가 가능한 환경이어도 비동기 처리가 불가능합니다.

 

  1. setTimeout(first, 1000) : 1초 뒤에 first() 함수 → console.log(’First’); 가 실행되도록 명령한다.
  2. console.log('Middle'); : ‘Middle’ 문자열이 출력된다.
  3. console.log('Last'); : ‘Last’ 문자열이 출력된다.
  4. 1초가 지난 뒤 ‘First’ 문자열이 출력된다.

 

만약 자바스크립트가 Blocking Model이었다면 위 예시 코드는 1초를 기다린 이후에 first() 함수를 먼저 실행하여 'First'를 출력한 뒤, 'Middle', 'Last' 순서로 출력했을 것이다.

 

 

 

 

 

프로미스(Promise)

 

자바스크립트에서 비동기 처리를 동기로 처리할 수 있게 돕는 Built-in(미리 내부적으로 정의된)객체 유형이며 이 객체를 이용하면 비동기 처리를 아주 손쉽게 할 수 있다.

 

 

❓ 비동기 처리를 왜 동기적으로 처리해야하나요?

 

정확히 말하자면 Javascript 관점에서 비동기적인 코드를 동기적인 것 처럼 처리한다는 것이다. 왜 이런 동작이 필요할까?? 이전에 setTimeout 비동기 함수 처리 예제를 통해 비동기 함수보다 동기 명령이 우선적으로 처리되는 상황을 살펴보았는데 예제에서 살펴본 내용은 지극히 합리적이다. 왜냐면 ‘그렇게 의도’ 했기 때문이다.

 

First를 출력하는 것이 Middle과 Last보다 늦는 것이 의도적이라는 것이지만 First-Middle-Last를 순서대로 출력하고 싶다면 상황이 달라진다. 이 때는 First가 출력될 때 까지 얼마가 걸리던 Middle과 Last는 출력되어선 안됩니다. 이럴 때 필요한 것이 동기적인 것 처럼 출력하는 방법이다.

 

예를 하나 더 들어보자면 본인이 유명한 가수라고 했을 때 밤낮으로 앨범문의를 하는 팬들을 상대해야한다. 그래서 앨범이 나오면 자동으로 소식을 주는 카페를 만들었다. 이렇게 팬들도 가수도 둘다 행복해졌다. 위에 예시랑 유사하듯 First(가수)가 앨범이 나와 카페에 올라가기 전까진 Middle-Last는 출력되지않는다.

 

첫 번째로 내릴 수 있는 결론은, 비동기적 진행의 선택은 개발자의 의도에 따라 결정된다는 것이다.

조금 더 현실적인 예를 들어보겠다. 데이터베이스의 데이터를 먼저 가져온 후, 그 데이터를 가공하여 반환하는 함수가 존재한다고 가정해볼 때. 데이터베이스와의 소통은 I/O이고, Javascript 에서 거의 모든 I/O는 비동기적이므로 아마도 데이터를 가져오기도 전에 데이터 가공 명령이 실행되버리고 말 것이며 에러 발생가 발생한다. 이런 상황에서 매우 유용하게 쓰일 수 있는 방법이 Promise이다. 단어 그대로 ‘언제 진행할지 약속’한다고 생각하시면 좋다. 언제 진행할지란, 바로 비동기 명령의 실행이 완료된 이후를 말하는 것이며 데이터베이스의 예로 돌아가서, 데이터를 가져온 이후 데이터를 처리하는 명령어들을 Promise 이후에 진행하도록 작성한다면 데이터를 가져오지도 않았는데 처리가 시작되는 오류상황을 회피할 수 있을 것 이다.

 

❓ callback과 promise의 차이점

callback을 사용하면 비동기 로직의 결과값을 처리하기 위해서는 callback안에서만 처리를 해야하고, 콜백 밖에서는 비동기에서 온 값을 알 수가 없다. 그래서 콜백을 이용한 비동기처리는 콜백지옥을 만든다. 해결책으론 promise를 사용하면 비동기에에서 온 값이 promise 객체에 저장되기 때문에 코드 작성이 용이해진다. 하지만 promise를 사용해도 콜백지옥이 연상되므로 promise와 async + await를 같이 사용하는 것이 좋다.

👉 콜백 지옥

  • 콜백 지옥(callback hell)이란 콜백 함수를 익명 함수로 전달하는 과정에서 또 다시 콜백 안에 함수 호출이 반복되어 코드의 들여쓰기 순준이 감당하기 힘들 정도로 깊어지는 현상을 말한다.
  • 주로 이벤트 처리나 서버 통신과 같은 비동기 작업을 제어하기 위해서 사용되는데 이러한 프로그래밍은 가독성이 떨어지고 코드 수정을 어렵게한다.

콜백지옥을 받아라~

 

 

Promise 생성자 인터페이스

new Promise(executor);

// 예제
new Promise((resolve, reject) => {
	// 명령문
});

let promise = new Promise(function(resolve, reject) {
  // executor (제작 코드, '가수')
});

new Promise에 전달되는 함수는 excutor(promise의 실행자,  실행함수)라고 불리며 함수만 올 수 있으며 Promise가 만들어질 때 자동으로 실행된다. exector의 인수 resolve와 reject는 자바스크립트 자체 콜백이며 개발자는 resolve와 reject를 신경 쓰지 않고 excutor 안 코드만 작성하면 된다.

 

대신 excutor에선 결과를 즉시 얻는 늦게 얻든 상관없이 상황에 따라 resolve, reject 중 하나를 무조건 호출해야한다. 즉, 성공 or 실패다. 

  • resolve(value) — 일이 성공적으로 끝난 경우 그 결과를 나타내는 value와 함께 호출
  • reject(error) — 에러 발생 시 에러 객체를 나타내는 error와 함께 호출

 

 

❓ 생성자(Constructor) 란 무엇인가요?

 

Javascript에서는 원시 타입(String, Boolean 등) 을 제외한 대부분의 타입들이 객체(Object) 로 구성되어 있다. 일반적으로 **객체(Object)**를 생성하는 함수를 생성자(Constructor) 함수라고 부르게 되는데, Promise 또한 객체로 구성되어 있기 때문에 생성자 함수를 이용해 Promise를 선언하게된다.

function printFunc(data){
  console.log(data);
}

// 생성자 함수
const obj = new Object();
const promise = new Promise(printFunc);

obj
// Print : {}

 

 

Promise는 반드시 3가지 상태를 지니며, 대기(Pending) 상태가 아니라면 Promise의 연산이 이미 끝난 상태로 볼 수 있습니다.

 

  • Promise의 상태
    • 대기(Pending): 이행하거나 거부되지 않은 초기 상태.
    • 이행(Fulfilled): 연산이 성공적으로 완료됨.
    • 거부(Rejected): 연산이 실패함.

 

 

 

 

Promise가 만들어 질 때 executor가 실행되며, executor에서 resolve 함수가 호출되기 전까지 firstPromise.then(...) 안에 있는 코드를 실행하지 않으며 이렇게 executor 가 실행되어 resolve(성공)된 프로미스를 Fulfilled Promise라고도 부르며 excutor가 실행됐지만 reject(실패)된 프로미스를 Rejected Promise라 부른다. 그리고 resolved 혹은 rejected 상태의 promise는 처리된(settled) 프라미스라고 부르며 반대되는 대기(pending) 상태의 프라미스가 있다.

 

const timerPromise = new Promise((resolve, reject) => { // 이곳에 정의된 함수가 executor
  setTimeout(() => {
	  console.log('First');
		resolve();
	}, 1000);
});

// 이 시점에서 timerPromise는 Fulfilled Promise라고 부를 수 있다.

timerPromise.then(() => {
	console.log('Middle');
	console.log('Last');
});

// Print: First
// Middle
// Last

 

 

 

 

 

Promise.then

Promise 안에서 resolve가 실행 된 경우 then 메서드에 작성된 함수가 실행된다.

 

const resolvePromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('First');
    resolve('Resolve!'); // resolve를 실행할 때, 안에 데이터를 넣어줄 수 있다.
  }, 1000);
});

resolvePromise.then((data) => {
  console.log('Middle');
  console.log('Last');
  console.log(data);
})

// Print: First -> 1초 뒤에 출력된다.
// Middle
// Last
// Resolve!

 

Promise.resolve 함수 이용

 

프로미스가 값을 반환하는 경우 반환되는 값은 항상 프로미스로 감싸져 있다!

const firstPromise = Promise.resolve('First');

firstPromise.then((value) => {
	console.log(value);
});

// Print: 'First'

 

Promise.then으로 함수형 프로그래밍 체험하기

 

이것이 가능한 이유는 console.log라는 함수 뒤에 괄호를 사용해서 함수를 호출하지 않고, 함수를 그대로 then에 넘겼기 때문입니다.

 

const firstPromise = Promise.resolve('First');

firstPromise.then(console.log);

// Print: 'First'
const countPromise = Promise.resolve(0);

function increment(value) {
	return value + 1;
}

const resultPromise = countPromise.then(increment).then(increment).then(increment);
resultPromise.then(console.log);

//처음 .then일때 1, 다음은 2 다음은 3이 된다.
// Print: 3

 

Promise.catch

 

Promise 안에서 에러가 throw 되거나 reject가 실행되면 catch 메서드에 작성한 함수가 실행된다. .then으로 안 간다.

 

const errorPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
	  console.log('First');
		reject('Error!!'); // 직접 reject를 실행하면 프로미스에서 에러가 발생한것으로 간주한다.
	}, 1000);
});

errorPromise.then(() => {
	console.log('Middle');
	console.log('Last');
}).catch((error) => {
	console.log('에러 발생!', error);
});

// Print: '에러 발생! Error!!'
더보기

🔥 이 외에도 유용한 프로미스 함수

 

 

비동기 함수 (Async Function)

 

비동기 함수는 일반 함수나 화살표 함수와 아주 비슷하지만 딱 두가지만 다르다. 

 

  1. 비동기 함수결과 값항상 Promise 객체로 resolve된다.
  2. 비동기 함수 안에서만 await 연산자를 사용할 수 있다.
// 비동기 + 일반 함수
async function 함수이름() {
	// 명령문
}

// 비동기 + 익명 함수
async function() {
  // 명령문
}

// 비동기 + 화살표 함수
async () => {
	// 명령문
}
function 함수이름() {
	return Promise.resolve('값');
}

// 위와 아래의 함수는 같은 동작을 보여준다.

async function 함수이름2() {
	return '값';
}

함수이름();
// Print: Promise { '값' }

함수이름2();
// Print: Promise { '값' }

function 앞에 async를 붙이면 해당 함수는 항상 프라미스를 반환하며 프라미스가 아닌 값을 반환하더라도 이행 상태의 프라미스(resolved promise)로 값을 감싸 이행된 프라미스가 반환되도록 한다.

 

 

 

❓ 비동기 함수는 왜 쓸까?

await 연산자를 비동기 함수 안에서만 사용할 수 있는데, 이를 활용하면 문법이 훨씬 간결해질 수 있어 콜백지옥을 피해갈 수 있다. 또한 new Promise(executor) 코드로 Promise를 직접 생성하면 executor가 바로 실행되는것과 달리, 비동기 함수는 함수가 실행되기 전까지 Promise를 생성하지 않는다.

 

 

 

await 연산자

await 연산자를 사용하면 Promise가 fulfill 상태가 되거나 rejected될 때 까지 함수의 실행을 중단하고 기다릴 수 있으며 Promise의 연산이 끝나면 함수에서 반환한 값을 얻을 수 있다.await 연산자는 async 함수 안에서만 사용할 수 있으며 일반함수에 쓰면 에러가 난다.

 

const result = await 값;

 

"값" 에는 Promise가 아닌 다른 값도 들어갈 수 있으며 Promise가 아니라면 기다리지 않고 해당 값 자체를 그대로 반환한다.

async function 함수이름() {
	const result = await 'Test!';
	console.log(result);
}

함수이름();
// Print: 'Test!';

 

 

'TIL' 카테고리의 다른 글

HTTP / Web Server  (0) 2022.11.26
객체 리터럴(object literal), Error handling, 클래스(Class)  (0) 2022.11.26
Node.js  (0) 2022.11.25
11일차 - Application Programming Interface (API)  (0) 2022.11.20
11일차 - JSON Web Token  (0) 2022.11.20