동기(Synchronous)와 비동기(Asynchronous)

Daehyeon Yun·2024년 8월 7일

프론트엔드

목록 보기
8/14

📖 Reference
📎 https://tlsdnjs12.tistory.com/15
📎 https://ko.javascript.info/async-await


🤔 JS에서의 동기/비동기 처리란?

동기와 비동기를 나누는 가장 큰 차이점은 어떤 실행 순서를 가지는 지에 있다.

  • 동기적(Synchronous) : 요청을 보낸 후 응답을 받아와야 다음 동작을 실행
    • 먼저 시작된 하나의 작업이 끝날 때까지 다른 작업을 시작하지 않고 기다렸다가 다 끝나면 새로운 작업을 시작한다.
  • 비동기적(Asynchronous) : 요청을 보낸 후 응답과 관계 없이 다음 동작을 실행
    • 먼저 시작된 작업의 완료 여부와는 상관없이 새로운 작업을 시작한다. → 프로세스가 위 사진과 같이 병렬로 배치되어 실행되기에 작업의 순서가 확실하지 않는다. → 심지어 나중에 시작된 작업이 먼저 끝나는 경우도 발생한다.

💡 자바스크립트 비동기처리

자바스크립트는 기본적으로 비동기적(Asynchronous) 으로 동작한다.
JS에서도 아래와 같은 경우에 비동기적 처리가 발생한다.

👉 DOM Element의 이벤트 핸들러
→ 마우스, 키보드 입력 등, 페이지 로딩(DOMContentLoaded 등)

👉 타이머
→ setTimeout, 애니메이션 API(requestAnimationFrame)

👉 서버로 요청 및 응답
→ fetch API, AJAX(XHR)


😢 자바스크립트의 3가지 비동기 처리방식

🚩 1. 콜백 함수(Callback Function)

  • 콜백 함수(CF, Callback Function)이란? → 다른 함수에 매개변수로 넘겨준 함수. 즉, 다른 함수의 매개변수함수를 전달하고, 어떠한 이벤트가 발생했을 시 매개변수로 전달된 함수가 다시 호출되는 것을 말한다.
  • 콜백(Callback)이란? → 어떤 일을 다른 객체에게 시키고, 해당 일이 끝나는 것을 기다리지 않고 끝나고 호출될 때까지 다른일을 수행하는 것. (non-block, 비동기)
  • ❔ 왜 콜백 함수(Callback Function)을 사용하는가? → JS는 이벤트 기반 언어이다. → JS는 비동기적 방식을 사용하는 언어이기에 프로세스가 순차적으로 실행되어야 하는 경우, 작성한 함수가 원치 않는 순서로 실행되어, 결과가 뒤바뀔 수 있다.

    즉, 콜백 함수(Callback Function)는 비동기 처리방식의 문제점을 해결해주기 위해 특점 시점에서 호출이 되도록 만드는 함수.

/* 예제 1. */
function A(callback){
	console.log("A function");
	callback();
}

function B(){
	console.log("B function");
}

A(B);

// 결과 : A function B function
// -> Callback Function B를 A의 매개변수로 전달

/* 예제 2. */
function cal(cb, x, y){
	return cb(x, y);
}

function add(a, b){
	return a+b;
}

console.log(cal(add, 1, 2));
// 결과 : 3

🚩 2. Promise

“A promise is an object that may produce a single value some time in the future”

  • 프로미스(Promise) 란?
    ⇒ 자바스크립트 비동기 처리에 사용되는 객체
❓ 비동기 처리란?
→ 비동기식 동작이 동기적으로 동작하도록 하는 처리.

🤔 Promise가 필요한 이유?

  • 프로미스는 주로 서버에서 받아온 데이터를 화면에 표시할 때 사용한다.
  • 일반적으로 웹 애플리케이션을 구현할 때 서버에서 데이터를 요청하고 응답받기 위해 아래와 같은 APi를 사용한다.
$.get('{url}/bbs/1', function(response){
	...
});

상기의 API는 서버로부터 데이터를 응답받아오는 요청을 보낸다.

만약, 아직 서버로부터 응답을 받아오지 못했는데 데이터를 사용하고자 하면 오류가 발생한다.

⇒ 이를 해결하기 위한 방법 중 하나가 프로미스(Promise)

💡Promise의 기본 문법

const _promise = new Promise((resolve, reject) => {
	// 비동기 작업
}

_promise
	.then(() => {
		console.log("this is then!");
	})
	.catch(() => {
		console.log("this is catch!");
	});
  • then : 해당 promise성공했을 때의 동작을 지정한다.
  • catch : 해당 promise실패했을 때의 동작을 지정한다.

promise약속 이다. promise 의 선언부는 ‘지금 넘겨줄 데이터가 없으니 추후 주겠다’라는 의미

💡Promise의 상태(State)

  • pending : 프로세스(promise)를 수행 중인 상태 (fulfill 혹은 reject 가 되기 전)
  • fulfill(resolve) : 프로세스(promise)가 완료된 상태
  • reject : 프로세스(promise)가 중단 혹은 실패한 상태
  • settled : 프로세스(promise)가 성공이거나 실패가 되었든 일단 완료한 상태.
// promise 선언부 
const _promise = function(param){
	return new Promise(function(resolve, reject){
		// 비동기를 표현하기 위한 setTimeout func
		window.setTimeout(function(){
			if(param){
				resolve("프로세스 완료");
			}
			else{
				reject(Error("프로세스 실패"));
			}
		}, 3000);
	});
}

// 실행부
_promise(true)
.then(function(data){
	console.log(data); // promise 성공
}, function(error){
	console.error(error); // promise 실패
});

위 코드를 해석해보자.

  1. new Promisepromise 객체가 생성된 후, resolve 혹은 reject 가 호출되기 까지의 순간을 pending 상태라 할 수 있다.
  2. 비동기 작업(위 코드는 setTimeout 을 의미)이 끝난 뒤 실행부에서 인수로 넘긴 trueresolve 를 호출하고, promise 작업이 실패했을 경우를 구현하기 위해 false 가 인수로 넘어가면 reject 가 호출된다.
  3. 실행부에서 _promise() 를 호출하면 Promise 객체가 반환된다.

💡Promise의 then과 catch

/* 상황 1. then과 resolve */
const _promise = new Promise((resolve, reject) => {
	setTimeout(() => {
		resolve("resolve되었습니다.");
	}, 3000);
});

_promise
	.then(value => console.log(value));
// then은 resolve된 값들을 받아오는 역할을 수행한다. (즉, promise(fulfill)가 완료되었을때)

/* 상황 2. catch와 reject */

// resolve가 아닌 reject된 값이 넘어온 경우 then을 사용할 시 에러가 발생한다.
// java의 try-catch와 같은 맥락으로 JS에서도 catch를 통해 reject로 넘어온 에러를 잡을 수 있다.
const _promise = new Promise((resolve, reject) => {
	setTimeout(() => {
		reject("reject되었습니다.");
	}, 3000);
});

_promise
	.then(value => console.log(value))
	.catch(error => console.log(error));
// 이때, then과 catch가 순차적으로 작성되었다고 해서 then이 수행된 후 catch가 실행되는게 아니다.
// 단순히 결과가 resolve라면 then으로, reject라면 catch로 넘어간다.

/* 상황 3. Chaining Promise와 catch */
// 만약, promise를 여러번 사용하고자 한다면?
// 예시 : promise를 통해 API로 data를 받아온 뒤, 받아온 데이터를 promise를 통해 복호화하는 경우
// 이와 같이, promise를 여러번, 순차적으로 사용하는 경우를 Chaining Promise라 한다.

const _promise = new Promise((resolve, reject) => {
	resolve(2);
});

const plus = num => num + 1;

_promise
	.then(plus)
	.then(plus)
	.then(plus)
	.then(() => { throw Error("error") })
	.then(result => console.log(result))
	.catch(error => console.log(error));

// 위와 같이 Chaining Promise를 사용할 때 각각의 then에 모두 catch를 지정할 필요 없이
// 한 번의 catch로 모든 then을 해결할 수 있다.

// Promise 예외처리 문법 정리
try{
	// 예외가 발생할 수 있는 명령 작성 (pending)
}catch(error){
	// 에러가 발생하였을 때 (reject), error는 에러 객체를 뜻한다.
} finally {
	// 예외가 발생하거나, 발생하지 않아도 무조건 실행시킬 명령
	// 즉, try가 실행되든 catch가 실행되는 finally는 무조긴 실행된다.
}

🚩 3. Promise를 활용한 async/await

비동기 프로그래밍에서 Callback function을 사용해야한다.

하지만, 콜백함수의 깊이가 깊어질수록 코드가 매우 복잡해지며 가독성이 떨어지는 이른바 콜백지옥 이 발생한다.

위와 같은 콜백지옥 을 해결하기 위해 ES6 에서 Promise 를 도입하였지만, promiseresolve 되거나 reject 되는 경우를 대비하기 위해 체이닝 프로미스(Chaining promise) 를 작성한다.

아래의 코드를 보자.

function callbackHell(num){
	const _promise = new Promise((resolve, reject) => {
		setTimeout(() => {
			const result = number + 10;
				if(result > 50){
					const err = new Error("너무 큰 숫자입니다.");
						return reject(err);
				}
				resolve(result);
		}, 1000);
	})
	return _promise;
}

// promise로 넘어가는 props이 50보다 작으면 resolve, 크면 reject
callbackHell(0)
	.then((num) => { console.log(num); return callbackHell(num); }) // 10
	.then((num) => { console.log(num); return callbackHell(num); }) // 20
	.then((num) => { console.log(num); return callbackHell(num); }) // 30
	.then((num) => { console.log(num); return callbackHell(num); }) // 40
	.then((num) => { console.log(num); return callbackHell(num); }) // 50
	.then((num) => { console.log(num); return callbackHell(num); }) // Error
	.catch((err) => { console.log(err); });

위와 같이 코드가 직관적으로 들어오지않아 이해하기 힘들다.

이러한 체이닝 프로미스 를 사용할 때 발생되는 불편함을 해결하기위해 ES8 에서 async/await 를 도입하였고, 그 결과 비동기 처리를 직관적이며 깔끔하게 수행할 수 있게 되었다.

function callbackHell(num){
	const _promise = new Promise((resolve, reject) => {
		setTimeout(() => {
			const result = number + 10;
				if(result > 50){
					const err = new Error("너무 큰 숫자입니다.");
						return reject(err);
				}
				resolve(result);
		}, 1000);
	})
	return _promise;
}

async function startTasks(){
	try{
		let result = await callbackHell(0);
		console.log(result);
		result = await callbackHell(result);
		console.log(result);
		result = await callbackHell(result);
		console.log(result);
		result = await callbackHell(result);
		console.log(result);
		result = await callbackHell(result);
		console.log(result);
		result = await callbackHell(result);
		console.log(result);
		result = await callbackHell(result);
	}catch(err){
		console.log(err)
	}
	}
  • async : async는 항상 function 앞에 위치한다. async가 수식된 함수는 항상 Promise 를 반환한다. 만약, Promise가 아닌 값을 반환하더라도 이행 상태 프라미스(resolved promise)로 값을 감싸 resolve된 promise를 반환한다.
  • await : await는 반드시 async 함수 안에서만 동작한다. JS는 await를 만나게 되면 Promise가 처리될 때까지(resolve) 기다린다. 즉, JS의 비동기 방식동기 로 편하게 처리해주는 역할을 수행한다.
    • 만약, promise 가 거부(reject)를 반환하면 어떻게 해야하나?

      async function test(){
      	await Promise.reject(new Error("에러 발생"));
      }
      
      // 위 코드는 아래와 동일하다.
      
      async function test(){
      	throw new Error("에러 발생");
      }
    • 위와 같이 promise가 거부되면 await는 에러를 반환한다.

    • await가 던진 에러는 try-catch 문을 사용하여 처리할 수 있다.

🤔 async와 await은 항상 옳을까?

  • 모든 경우에서 asyncawait 가 맞는 방법은 아니다.
    • callback의 깊이가 그리 깊지 않을 때는 간편한 콜백 함수를 사용하거나 Promise를 사용하는 것이 더 효율적인 방법일 수도 있다.
    • asyncawaitPromise를 사용하기 때문에 반드시 Promise 를 알아야한다. → 즉, async/await이 할 수 없는 동작을 단순히 promise로 해결할 수 있는 경우도 존재한다.

⇒ 💡 결론은 시기적절하게 promise와 async/await를 사용하자.


👏 번외 : JS의 fetch API

fetch('api url')
	.then(response => response.json())
	.then(response => { 
		// 서버로부터 받아온 데이터를 처리하는 로직 작성 
	});
  • JS에서 지원하는 fetch 는 서버로 요청(Request) 를 보내고 응답(Response) 을 받을 수 있도록 해주는 메서드이다.
  • 🤔 XMLHttpRequest 와의 차이는? → fetch APIPromise 기반 메서드이기에 더욱 간편하게 사용할 수 있다는 차이가 있다.
  • fetch를 호출하면 브라우저는 서버로 요청(Request)을 보낸 뒤 Promise 객체를 반환한다.
    • 요청(Request)이 완료되면 호출 성공 여부와는 관련 없이 Promise 객체는 resolve 되어 응답(Response) 객체가 반환된다.
    • 요청(Request)가 실패하면 Promise 객체는 rejected 된다. 이 경우 catch 를 통해 예외처리할 수 있다. (네트워크 이슈 혹은 404 요청)
  • fetch 로부터 받아오는 응답(Response) 객체는 서버에서 응답한 데이터에 대한 정보를 가지고 있다.
    • ok , status 속성 등을 이용하여 응답 성공 여부를 확인할 수 있다.
  • CORS, HTTP Origin header semantics 와 같은 개념들을 정의하고, 수정할 수 있다.
const serverAPI = async (params) => {
	try{
		const res = await fetch("API URL");
		const result = await res.JSON();
		console.log("서버로부터 응답받은 데이터", result);
	}catch(error){
		console.log("에러발생", error);
	}finally{
		console.log("async 종료");
	}
}

// fetch API 실행
serverAPI();
profile
열심히 살아야지

0개의 댓글