JS

[JS] 비동기 처리 방법(Callback, Promise, Async/Await)

hyeone22 2024. 4. 19. 18:05

비동기적 코드 결과 처리 방법

  • 콜백 함수(Callback)
  • Promise
  • Async/Await

콜백 함수(Callback)

Callback ≠ 비동기 함수

  • Callback 자체로는 비동기 함수와 직접관계는 없으나 Callback 이 많이 사용되는 곳이 비동기일 뿐
    • “비동기 로직이 끝나거든 그 비동기 로직의 결괏값을 통해 Callback을 실행해 줘"

 

전통적인 비동기 처리 방식이다.

함수(콜백)를 파라미터로(일급함수) 넘겨 파라미터를 받은 함수에게 실행권을 넘기는 것

  • 특정 시점에 실행되는 함수
  • 메서드를 콜백 함수로 사용하면 함수 취급

 

Callback Hell / 콜백 지옥

Callback 결괏값이 순차적으로 필요할 때 발생한다. Callback 결괏값이 서로 의존성으로 연결되어 있다.

  • Callback의 결과가 그다음 Callback 실행에 필요한 경우
  • 코드의 가독성 저하
  • 유지보수성 저하
fetchData((data) => {
    parseData(data, (parsed) => {
        filterData(parsed, (filtered) => {
            sortData(filtered, (sorted) => {
                console.log(sorted); // 최종 결과 처리
            });
        });
    });
});

 

Promise

위와 같이 콜백 지옥(Callback Hell)을 해결하기 위해 등장한 것이 Promise다.

 

Callback + Asynchronous = Promise

Promise를 사용하면 비동기 연산을 동기 연산처럼 사용할 수 있다.

Promise는 이 비동기 작업이 미래의 어떤 시점에 결과를 제공해 줄 것이라고 약속(Promise)을 해준다.

Promise를 생산자 - 소비자 패턴 ( Producer - Consumer Pattern )이라고도 한다.

 

Promise는 세 가지 상태를 가질 수 있다.

Promise는 비동기를 위해 탄생한 개념(Callback + Asynchronous)이기에 상태를 갖는다.

  • 대기(Pending)
    • 이행하지도, 거부하지도 않은 초기 상태 ( 비동기 처리 로직이 처리가 되지 않은 상황 )
  • 이행(Fulfilled)
    • 연산이 성공적으로 완료된 상태 ( 성공적으로 처리가 완료되어, Promise가 resolve를 통해 결과 값을 반환 )
  • 실패(Rejected)
    • 연산이 실패한 상태 ( 실패하거나 오류가 발생해 그 원인을 reject를 통해 반환 )

 

Promise 생성

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

new 생성자를 통해 생성한다.

생성자는 실행함수 resolve, reject를 파라미터로 받는다.

 

resolve -> 비동기 작업이 성공적으로 완료되면 호출, 상태값을 fulfilled 상태로 변경

reject -> 비동기 작업이 실패하면 호출, 상태값을 reject 상태로 변경

 

Promise 사용

Promise
	.then((result) => {
		// 성공시 동작할 코드
	})
	.catch((error) => {
		// 실패시 동작할 코드
	})
	.finally(() => {
		// 성공 유무에 상관 없이 마지막에 실행할 코드
	})

 

Promise 상태 : resolve 성공 시 콜백 then으로 정의

- then : promise가 성공적으로 이행되었을 때 실행될 콜백 함수 등록

 

Promise 상태 : Reject 실패 시 콜백 → catch으로 정의

- catch : promise가 거부되었을 때 실행될 콜백 함수 등록

 

- finally : 성공/실패 여부와 상관없이 마지막에 항상 실행되는 콜백

 

Promise Hell / Nested Promise

Callback Hell에서 유래

Promise 결괏값을 순차적으로 연결할 때 발생

Promise의 결과가 그다음 Promise 실행에 필요한 경우

fetchData()
    .then(data => {
        parseData(data)
            .then(parsed => {
                filterData(parsed)
                    .then(filtered => {
                        sortData(filtered)
                            .then(sorted => {
                                console.log(sorted); // 최종 결과 처리
                            })
                            .catch(error => {
                                console.error(error); // sortData 에러 처리
                            });
                    })
                    .catch(error => {
                        console.error(error); // filterData 에러 처리
                    });
            })
            .catch(error => {
                console.error(error); // parseData 에러 처리
            });
    })
    .catch(error => {
        console.error(error); // fetchData 에러 처리
    });

 

Promise Chain

Promise Hell의 해결 방법 Promise Chain으로 변환 여러 비동기 프로미스 작업을 순차적으로 실행하기 위해!

fetchData()
    .then(data => parseData(data))
    .then(parsed => filterData(parsed))
    .then(filtered => sortData(filtered))
    .then(sorted => {
        console.log(sorted); // 최종 결과 처리
    })
    .catch(error => {
        console.error(error); // 에러 처리
    });

 

 

Async/Await

Async/Await 문법을 사용하면 Promise를 좀 더 편하게 사용할 수 있다.

 

Async → Promise 상자반환 (Promise.resolve로 감싸져 있으면 바로 반환, 아니면 상자 포장 반환)

Await → Promise 상자열기 (Promise 객체를 기다렸다가 상자를 열어 내부 값을 반환)

 

Async

async는 function 앞에 위치한다.

function 앞에 async를 붙이면 해당 함수는 항상 Promise를 반환한다.

Promise가 아닌 값을 반환하더라도 Promise로 감싸서 반환한다.

 

명시적 Promise 반환할 때

Promise { <pending> }은 함수에서 반환된 Promise를 직접 기록할 때 발생한다.

Promise가 생성되었지만 직접 기록할 때 아직 해결되지 않았으므로 <pending> 보류 상태이다. 

 

Await

자바스크립트는 await 키워드를 만나면 Promise가 처리될 때까지 기다린다.

await 키워드는 async 함수 안에서만 동작한다.

async 함수가 아닌데 await을 사용하면 문법에러가 발생한다.

await은 최상위 레벨 코드에서 작동하지 않는다.

함수를 호출하고, 함수 본문이 실행되는 도중에 A 줄에서 실행이 잠시 중단되었다가 Promise가 처리되면 실행이 재개된다.

이때 Promise 객체의 result값이 변수 result에 할당된다. 그렇기 때문에 3초 뒤에 완료! 가 출력된다.

 

에러 처리 방법 : try catch

try.. catch

try {
	// 코드
    
} catch (err) {
	// 에러 핸들링
    
}

1. try  {...} 안의 코드가 실행된다.

2. 에러가 없다면 try {...} 코드의 마지막 줄 까지 실행되고 catch 블록은 건너뛴다.

3. 에러가 있다면, try {...} 코드가 중지되고 catch 블록으로 넘어간다. 변수 err(아무 이름이나 사용 가능)는 무슨 일이

일어났는지에 대한 설명이 담긴 에러 객체를 포함한다.

이렇게 try {…} 블록 안에서 에러가 발생해도 catch에서 에러를 처리하기 때문에 스크립트는 죽지 않는다.

이 코드는 비동기 함수 f()를 정의하고, 그 안에서 fetch를 사용하여 유효하지 않은 URL에 대한 HTTP 요청을 보내고 있다.

그러나 유효하지 않은 URL을 사용하였기 때문에 네트워크 요청이 실패하게 되고, 해당 Promise는 rejected 상태가 된다.

따라서 catch 블록이 실행되고, "error" 문자열이 콘솔에 출력된다.

종합하면, 코드는 네트워크 요청이 실패하여 에러가 발생하고, 이 에러를 catch 블록에서 처리하여 "error"를 콘솔에 출력하는 것이다..

 

Promise { <pending> }

f()를 호출하면 백그라운드에서 웹사이트에서 데이터를 가져오는 것과 같은 작업을 시작합니다.

이 작업에는 다소 시간이 걸릴 수 있습니다. 이 작업을 수행하는 동안 JavaScript는 나머지 코드를 계속 실행합니다.

이제 f()는 작업을 완료할 수 없는 경우(예: 데이터를 가져오려는 웹사이트가 존재하지 않는 경우)

발생하는 일을 처리하지 않기 때문에 JavaScript가 약간 혼란스러워집니다. 뭔가 잘못되면 어떻게 해야 할지 모릅니다.

그래서 "안녕하세요, 뭔가 일어나고 있습니다(약속). 하지만 아직 어떻게 해야 할지 모르겠습니다!"라고 말합니다.

이것이 바로 콘솔에 Promise { <pending> }이 표시되는 이유입니다.

이는 JavaScript가 "f()가 제대로 끝나는지 기다리고 있습니다."라고 말하는 것과 같습니다.

 

async/await과 promise.then/catch

async/await을 사용하면 await가 대기를 처리해 주기 때문에 then이 거의 필요하지 않다.

여기에 더하여 catch 대신 일반 try.. catch를 사용할 수 있다는 장점도 생깁니다. 항상 그러한 것은 아니지만, promise.then을 사용하는 것보다 async/await를 사용하는 것이 대개는 더 편리하다.

그런데 문법 제약 때문에 async 함수 바깥의 최상위 레벨 코드에선 await를 사용할 수 없습니다.

 

async/await과 Promise.all

여러 개의 Promise가 모두 처리되길 기다려야 하는 상황이면 Promise들을 Promise.all로 감싸고 await과 사용할 수 있다.

 

// Promise 처리 결과가 담긴 배열을 기다립니다.

let results = await Promise.all([
	fetch(url1),
	fetch(url2),
	...
]);

 

실패한 Promise에서 발생한 에러는 보통 에러와 마찬가지로 Promise.all로 전파된다.

에러 때문에 생긴 예외는 try.. catch로 감싸 잡을 수 있다.

 

 

 

출처 : https://ko.javascript.info/async-await