callback, Promise, async/await

DevSheryl·2022년 2월 23일
0

Javascript 정복

목록 보기
6/9
post-thumbnail

출처: 프론트엔드 3주차 수업 (Core Javascript 1)

동기(Synchronous)

현재 실행 중인 코드가 끝나야 다음 코드를 실행
= 현재 실행 중인 task가 종료할 때까지 다음 task는 대기

  • 장점: 코드 실행 순서가 보장된다.
  • 단점: 현재 실행 중인 task가 끝날 때까지 다음 task가 'blocking'된다.

동기 blocking 예제

const arr = [];

for (let i = 0; i < 1000; i++) {
  arr.push(i);  // (1000번 실행)
}

alert("안녕하세요");  // 확인을 누를 때까지 console 코드로 넘어가지 않음
console.log(arr);

비동기(Asynchronous)

현재 실행 중인 코드가 완료되지 않아도
나중에 실행하라고 브라우저에 맡겨놓고 다음 코드로 넘어감

// 코드 1

axios.get(url)
	.then(response => {
		// api 호출하고 응답받으면 실행
	})

// 코드 2
  • api 호출하고 응답은 언제 올지 모름

    1. 코드 1 하고
    2. api 호출하고
    3. 코드 2 하다가
    4. 응답이 오면 로직을 실행하고
    5. 다시 다음 로직 진행
  • 장점: blocking(작업 중단)이 발생하지 않는다.

  • 단점: 코드 실행 순서가 보장되지 않는다.

비동기 처리가 필요한 이유

자바스크립트 엔진이 싱글 스레드이기 때문이다.

싱글 스레드 (Single Thread)
호출 스택이 하나여서 한 번에 하나의 task만 실행할 수 있는 시스템
(처리에 시간이 걸리는 작업을 실행하면 blocking이 발생한다)

=> 콜백함수의 등장으로 자바스크립트에서도
서로 실행 시점이 다른 비동기를 처리할 수 있게 되었다.

주의점: 콜백함수는 동기에서도 쓰이는 개념이다.

콜백함수(Callback Function)

  • 함수 안에 인수로 전달되는 함수

고차 함수(Higher Order Function): 콜백함수를 가지는 함수 (예: map)

cf. 고차 컴포넌트 Higher Order Component(HOC)
: 컴포넌트를 매개변수로 받아서 컴포넌트를 반환하는 컴포넌트

이렇게 함수의 인수로 함수를 전달할 수 있는 것은,
자바스크립트가 일급함수를 가질 수 있는 함수형 프로그래밍 언어이기 때문이다.

일급함수란?

변수처럼 기능을 하는 함수

  1. 함수를 다른 함수에 인수로 제공할 수 있다.
  2. 리턴문에 함수를 반환할 수 있다.
  3. 변수에 함수를 할당할 수 있다.

비동기(= 발동과 실행 시점이 다른) 함수 예시

이벤트 리스너 (addEventListener)

특정 이벤트 발생 시

타이머 (setTimeout, setInterval 등)

일정 시간 경과 후

통신 요청 (fetch, AJAX, axios 등)

서버에 통신 요청 시

AJAX(Asynchronous JavaScript and XML)
브라우저가 서버에게 자바스크립트를 사용하여
비동기 방식으로 데이터를 요청하고, 응답한 데이터를 수신하여
웹페이지 일부를 동적으로 갱신하는 방식

콜백지옥 (Callback Hell)

예시

다음과 같이 api에 필요한 정보가 한꺼번에 담겨서 오면
프론트엔드 입장에서는 편하다. (api 호출 한번으로 필요한 데이터를 한번에 꺼낼 수 있어서)

[{
	"id": 1,
	"name": "운동화",
	"price": 30000
	"comments": [{ 
		"comment": "강추합니다",
		"username": "Kim",
		"likes": [{ 
			"like": true,
			"username": "lee"
		}]
	}]
}]

하지만 만약 3개의 api로 나뉘어 와 버렸다면?

[{
	"id": 1,
	"name": "운동화",
	"price": 30000
}]
[{
	"comment": "강추합니다",
	"username": "Kim"
}]
[{
	"like": true,
	"username": "lee"
}]

호출을 3번 해야 한다.

  1. 상품 목록을 가져오는 api를 호출한다.
  2. 가져온 상품 목록 중에서 첫 상품의 id로 해당 상품의 후기 api를 호출한다.
  3. 가져온 후기 중에서 첫 후기의 id로 좋아요 수를 가져오는 api를 호출한다.

결과: 콜백지옥의 탄생

$.get('https://api.test.com/proudcts', function(response) {
	var firstProductId = response.products[0].id;

	$.get('https://api.test.com/proudct/comments?id='+firstProductId, function(response) {
		var firstCommentId = response.comments[0].id;
	
		$.get('https://api.test.com/proudct/comment/likes?id='+firstCommentId, function(response) {
			var likes = response.likes;
			var likesCount = likes.length;
		});
	});
});

Promise

  • Promise는 콜백 지옥을 해결하고 비동기 동작을 처리하기 위해 ES6에서 도입되었다.
  • Promise는 클래스이다.
  • 사용법
    • Promise 클래스를 인스턴스화 해서 promise 객체를 만든다.
      (let promise = new Promise())
    • 반환된 promise 객체(let promise)로 원하는 비동기 동작을 처리한다.

Promise 구현

let promise = new Promise(function(resolve, reject) {
    // 비동기 로직 작성
});
  • resolve는 로직이 성공했을 때 실행할 비동기 함수
  • reject는 로직이 실패했을 때 실행할 비동기 함수

resolve

let promise = new Promise(function(resolve, reject) {
    setTimeout(function() {
		// resolve 함수에 인자를 넘기면
        resolve('hello world');
    }, 2000);
});

promise.then(function(msg) {
    console.log(msg);  // 여기서 실행된다 (2초 뒤에 hello world 출력)
});

reject

let promise = new Promise(function(resolve, reject) {
	setTimeout(function() {
      	// reject가 실행되면
        reject('실패');
  }, 2000);
});

promise.then(function() {
	console.log('resolve');        // 여기는 넘어가고
}, function(msg) {
	console.log('reject', msg);    // 여기만 실행됨 ('reject 실패' 출력)
});

then으로 resolve, reject를 다 받는 것보다
catch가 가독성이 더 좋다.

let promise = new Promise(function(resolve, reject) {
	setTimeout(function() {
        reject('실패');
  }, 2000);
});

promise
	// 위에처럼 then에 ','로 두 개를 연결하지 않고
	.then(function() {}) // resolve를 받는 then과
	.catch(function(err) { // reject를 받는 catch로 나누었다.
		console.log(err);
	});

🍮 resolve, reject 사용 시 주의점

let promise = new Promise(function(resolve, reject) {
    setTimeout(function() {
        resolve(1);
        resolve(2);
    }, 1000);
});

promise.then(function(msg) {
    console.log(msg);
});
  • 같은 게 여러 개 있으면 첫 번째꺼 하나만 실행된다. (위의 경우 1 하나만 출력)

Quiz

(답 확인: F12 - console에 코드 복붙)

문제 1

let promise = new Promise(function(resolve, reject) {
    setTimeout(function() {
        console.log(1);
        resolve(2);
        console.log(3);
        resolve(4);
    }, 1000);
});

promise.then(function(data) {
    console.log(data);
});

문제 2

let promise = new Promise(function(resolve, reject) {
  resolve(1);

  setTimeout(function () { 
		resolve(2);
	}, 1000);
});

promise.then(function(data) {
    console.log(data);
});

문제 3

주의: 중간에 catch 있음 + 성공 시 chaining 실행

function job() {
    return new Promise(function(resolve, reject) {
        reject();
    });
}

job()
	.then(function() {
    console.log(1);
	})
	.then(function() {
    console.log(2);
	})
	.then(function() {
    console.log(3);
	})
	.catch(function() {
    console.log(4);
	})
	.then(function() {
    console.log(5);
	});

Promise 상태

  • promise 객체는 3가지 상태(status)를 갖고 있다.
    • Pending(대기): 비동기 처리 로직이 아직 완료되지 않은 상태
    • Fulfilled(이행): 비동기 처리가 완료되어 프로미스가 결과 값을 반환해준 상태
    • Rejected(실패): 비동기 처리가 실패하거나 오류가 발생한 상태

Quiz

문제 1

let promise = new Promise(function(resolve, reject) {
    setTimeout(function() {
      console.log('before', promise)
      resolve(1);
      console.log('after', promise);
    }, 1000);
});

promise.then(function(data) {
    console.log(data);
});

문제 2

let promise = new Promise(function(resolve, reject) {
    setTimeout(function() {
      console.log('before', promise)
      reject(1);
      console.log('after', promise);
    }, 1000);
});

promise.then(function(data) {
    console.log('resolve', data);
}, function (data) {
    console.log('reject', data);
});

추가 내용

Promise.all()

  • 여러 프로미스를 모두 성공시킨 후에 완료 로직을 실행하고 싶은 경우
    (Promise.all을 사용하면 resolve가 여러 개여도 모두 실행시킬 수 있다!)
    ```jsx
    Promise.all([
      new Promise(resolve => setTimeout(() => resolve(1), 9000)), // 1
      new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
      new Promise(resolve => setTimeout(() => resolve(3), 1000))  // 3
    ]).then(function(value) {
    	console.log(value);
    });
    ```
    
    - 결과: 9초 뒤에 `[1, 2, 3]` 출력

Promise.all이 사용되는 예시

let urls = [
  'https://api.github.com/users/iliakan',
  'https://api.github.com/users/remy',
  'https://api.github.com/users/jeresig'
];

let requests = urls.map(url => fetch(url));

Promise.all(requests)
  .then(responses => responses.forEach(
    response => alert(`${response.url}: ${response.status}`)
  ));

async & await

  • ES8에 도입되었다.
  • 비동기 함수를 선언하고 Promise를 반환한다.
  • await는 async 함수 안에서만 쓸 수 있다.

즉시 실행함수(Immediately invoked function expression, IIFE)

(async function() {
	// 내용
})();

// 또는 function 없이

(async () => {
	// 내용
})();

예: 즉시실행함수를 쓰지 않은 코드

axios('https://api.test.com/proudcts')
  .then(function(response) {
    let firstProductId = response.products[0].id;
		return axios('https://api.test.com/proudct/comments?id='+firstProductId);
  })
  .then(function(response) {
		let firstCommentId = response.comments[0].id;
    return axios('https://api.test.com/proudct/comment/likes?id='+firstCommentId)
  })
	.then(function(response) {
			let likes = response.likes;
			let likesCount = likes.length;
  });

위의 코드를 async/await + try/catch + 즉시실행함수 조합으로 바꾸면

(async () => {
	try {
	  let productResponse = await fetch('https://api.test.com/proudcts');
	  let firstProductId = productResponse.products[0].id;

	  let commentResponse = await fetch('https://api.test.com/proudct/comments?id='+firstProductId);
		let firstCommentId = commentResponse.comments[0].id;

	  let likeResponse = await fetch('https://api.test.com/proudct/comment/likes?id='+firstCommentId);
		let likesCount = likeResponse.likes.length;
	} catch(error) {
		console.log(error);
	}
})();

이벤트루프

이벤트루프에 대한 자세한 내용은 이전 포스트 참고 👉https://velog.io/@yena1025/%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A3%A8%ED%94%84%EB%9E%80

Quiz

const foo = () => console.log("First");
const bar = () => setTimeout(() => console.log("Second"), 500);
const baz = () => console.log("Third");

bar();
foo();
baz();
profile
2024 가보자고~!

0개의 댓글