[JavaScript] 콜백함수, 프로미스, async/await

D uuu·2023년 12월 17일
0

JavaScript

목록 보기
4/8

비동기 복습하기

자바스크립트는 싱글 스레드이기 때문에 한번에 한가지 일만 처리할 수 있습니다. 함수가 호출 되는 순서대로 순차적으로 실행 되기 때문에 순서가 보장된다는 장점은 있지만, 네트워크 요청과 같이 시간이 걸리는 작업을 할때는 동기적으로 문제를 해결하다 보면 블로킹(blocking) 현상이 발생하여 사용자 경험을 떨어트리고 애플리케이션 활용도 역시 떨어지게 됩니다.

그래서 이를 해결하기 위해 비동기 처리 방식이 나왔습니다. 비동기는 현재 실행 중인 코드가 종료 되지 않은 상태라해도 기다리지 않고 다음 코드를 바로 실행 하는 방식을 비동기 처리 방식이라고 합니다. 비동기 처리는 블로킹이 발생하지 않는다는 장점이 있지만, 순서를 보장받지 못한다는 단점이 있습니다.
대표적인 비동기 함수로는 타이머 함수인 setTimeout, setInterval, 네트워크 요청, 이벤트 핸들러가 있습니다.

비동기 콜백 함수(call back)

비동기 함수는 결과를 외부에 반환할 수 없고 상위 스코프의 변수에도 할당할 수가 없습니다. 왜냐하면 비동기 함수가 실행되는 시점에는 이미 동기 코드는 실행 되고 종료 된 상태이기 때문입니다.

예제1
setTimeout 의 시간을 0초로 주었을때, 콘솔에 어떻게 찍힐까요 ?
setTimeout 함수는 대표적인 비동기 함수로 실행 컨텍스트가 모두 비었을때 비로소 호출 스택에 쌓이게 됩니다. 따라서 순서는 1 → 3 → 2 순서가 됩니다.

console.log(1)

setTimeout(() => {
	console.log(2)
},0)

console.log(3)

따라서 비동기 함수의 처리 결과(서버의 응답) 는 비동기 함수 내부에서 수행해야 합니다. 이때 콜백 함수를 전달하여 처리 결과에 대한 응답을 받을 수 있습니다.

콜백 지옥 (Callback hell)

예를 들어 신발을 구매한다고 할때, 신발 종류 중에서 운동화를 찾은 다음 원하는 신발을 골라 나한테 맞는 사이즈가 있는지를 확인해서 구매 하는 코드를 아래와 같이 콜백함수를 인자로 전달하여 작업을 수행할 수 있는데, 콜백 함수 → 콜백함수 → 콜백함수로 이어져 콜백 지옥을 만들어 낼 수 있습니다.

그리고 보다시피 콜백으로 비동기를 처리할 경우 비동기 작업이 끝나면 바로 콜백 함수가 실행 되기 때문에 요청과 응답을 분리할 수가 없습니다. 요청해서 받은 데이터를 가지고 콜백 함수에서 활용하고 또 다른 콜백 함수를 호출하고를 반복하기 때문에 아래와 같이 콜백 지옥이 생기게 되어 가독성이 매우 떨어지고 나중에 유지보수 하기가 힘들 수도 있습니다.

function getData() {
	axios.get("url 주소/products/sneakers", function(result, error) {
    	if(!error) {
        	let shoseId = result.id
         }        
        axios.get("url 주소/products/sneakers/shoseId, function(result, error {
        	if(!error) {
            	let selectedShoses = result.shoses
                  let size = selectedShoses.find((detail) => {
                	return (detail.size === size)
               	 }
             }
        		axios.get("url 주소/products/sneakers/shoseId/?size=size", function(result, error) {
             		   //// 결제 작업 //// 
  		})
	 })
})


Promise 로 개선된 비동기 처리 방법

위 예제와 같이 콜백 함수의 단점을 보완하며 비동기를 보다 효율적으로 처리하기 위해 ES6 에서 Promise 가 생겨났습니다.

프로미스는 자바스크립트의 특수 객체입니다. 프로미스는 두개의 콜백 함수를 인자로 전달 받는데, 작업이 성공적으로 완료 되었을 때 결과를 반환하는 reslove 함수 와 작업에 실패했을때 에러를 반환하는 reject 함수가 있습니다.

프로미스의 3가지 상태(state)

여기서 말하는 상태란 프로미스의 처리 과정을 의미합니다. 생성자 함수인 new Promise() 로 프로미스를 생성하고 종료될 때까지 3가지 상태를 갖습니다.

  • pending(대기) : 비동기 처리 로직이 아직 완료되지 않은 상태
  • fulfilled(이행) : 비동기 처리가 완료되어 프로미스가 결과 값을 반환해준 상태
  • rejected(실패) : 비동기 처리가 실패하거나 오류가 발생한 상태

pending(대기)

new Promise() 메서드로 호출하면 대기 상태가 됩니다.

new Promise() 메서드를 호출할때 콜백 함수를 선언할 수 있고 콜백 함수의 인자는 resolve, reject 입니다.

new Promise(function(resolve, reject) {
	//...
})

fulfilled(이행)

여기서 콜백 함수 인자 resolve 를 아래와 같이 실행 하면 이행 상태가 됩니다. 즉 무언가를 요청하여 응답(결과) 을 받았다는 의미 입니다.

function getData() {
  return new Promise(function(resolve, reject) {
    const data = 100;
    resolve(data);
  });
}

// resolve()의 결과 값 data를 resolvedData로 받음
getData().then(function(resolvedData) {
  console.log(resolvedData); // 100
});

rejected(실패)

new Promise()로 프로미스 객체를 생성하면 콜백 함수 인자로 resolve와 reject를 사용할 수 있다고 했습니다. 여기서 reject를 아래와 같이 호출하면 실패(Rejected) 상태가 됩니다. 실패 상태가 되면 실패한 이유를 catch() 로 받을 수 있습니다.

function getData() {
  return new Promise(function(resolve, reject) {
    reject(new Error("Request is failed"));
  });
}

// reject()의 결과 값 Error를 err에 받음
getData().then().catch(function(err) {
  console.log(err); // Error: Request is failed
});

Promise 코드 예제

프로미스를 사용하면 네트워크 요청과 응답에 대한 처리를 구분해서 작성할 수 있다는 점이 가장 큰 장점입니다. getData 함수로 프로미스를 생성해 데이터를 요청하고 받아온 데이터를 가지고 활용하고 싶을때는 then() 메서드를 사용해서 받아온 데이터를 가지고 활용할 수 있습니다.


const getData = new Promise((function(resolve, reject) {
	$.get('url 주소/products/sneakers', function(response) {
    	if(response) {
        	resolve(response)
       }
       reject(new Error('Request is failed'))
    })
  })

 
 getData.then((result) => {
 	/// result 사용
 })

Promise hell 도 존재한다

프로미스도 콜백 패턴을 사용하고 있습니다. 아래와 같이 then 메서드를 통해 결괏값을 인자로 받아와서 작업을 수행하고 다시 then 메서드를 호출하고 then, then, catch... 와 같이 작업이 무수히 길어지기 때문에 콜백 헬에 이어 프로미스 헬이라는 표현이 생겨나기도 했습니다.

 getData().then(function(sneakersData) {
 	/// sneakersData 로 어떠한 작업 
 }).then((data) => {
 	/// data 로 어떠한 작업
 }).then((result) => {
 	/// console.log(result)
 }).catch(function(err) {
 	console.error(err)
 })

async/await 의 등장

콜백 함수의 단점을 보완하며 비동기를 보다 효율적으로 처리하기 위해 ES6 에서 Promise 가 생겨났다면, Promise 를 조금 더 간편하고 가독성 좋게 사용하기 위해 async/await 라는 개념이 등장했습니다.

aynsc/await 는 프로미스를 기반으로 동작합니다.
aysnc/await 을 사용하면 프로미스의 then/catch/finally 후속 처리 메서드에 콜백 함수를 전달해서 비동기 처리 결과를 처리할 필요 없이 마치 동기처럼 프로미스를 사용할 수 있습니다.

const getData = async(url) => {
	const response = await fetch(url)
    const result = await response.json()
    console.log(result)
}

await 키워드는 반드시 async 함수 내부에서 사용해야 합니다. async 를 사용해 정의하면 언제나 프로미스를 반환합니다.
함수 앞에 async 를 붙여주고 데이터 요청과 같이 오래 걸리는 작업을 수행할 경우 그 앞에 await 을 붙여주면 영어 단어 그대로 fetch 함수가 반환한 프로미스settled 상태가 될때까지 기다렸다가 settled 상태가 되면 프로미스가 resolve 한 결과를 변수에 담아줍니다.

async/await 예외처리

async/await에서 예외를 처리하는 방법은 바로 try catch 를 사용 하는 것 입니다. 프로미스에서 에러 처리를 위해 .catch()를 사용했던 것처럼 async에서는 catch{} 를 사용하시면 됩니다.

조금 전 코드에 바로 try catch 문법을 적용해보겠습니다.

const getData = async(url) => {
 try {
    const response = await fetch(url)
    const result = await response.json()
    console.log(result)
 } catch (error) {
    console.log(error)
  }
}

위의 코드를 실행하다가 네트워크 통신 오류 뿐만 아니라 일반적인 오류까지도 catch 문으로 잡아낼 수 있습니다. 발생한 에러는 error 객체에 담기기 때문에 catch 문 안에서 에러 유형에 맞게 코드를 처리해주면 됩니다.



정리하며

콜백 함수, Promise, async/await 계속 등장하는 개념에 머리가 아프고 정리가 잘 되지 않았는데요. 결국 개발자 입장에서 비동기 처리를 보다 사용하기 편하게 개선해 나갔구나 정도로 이해하면 조금 더 쉽게 개념이 와닿을 것 같다는 생각이 들었습니다.

콜백 함수로 비동기를 처리했을때의 불편한 점에 대해서 고민해보고 그것을 Promise 에서는 어떻게 개선했는지, Promise 는 또 어떤 불편한점이 있고, 그걸 async/await 이 어떻게 개선했는지에 집중하면서 공부를 위한 공부가 아닌 필요에 의한 공부를 할 수 있었던 것 같습니다. 앞으로 부족한 내용이나 잘못된 부분은 수정해나가겠습니다.

profile
배우고 느낀 걸 기록하는 공간

0개의 댓글