본문 바로가기

개발공부/Javascript

[JS] 비동기

동기 (Synchronous)

순차적으로 실행되는 것을 의미한다. 한 작업이 완료 되어야 다음 작업이 시작되는 것이다. 작성하는 코드는 대부분 동기식으로 작동한다. 한 작업이 완료되기 전까지 다음 작업으로 넘어가지 않는다. 장점은 프로그램 흐름을 이해하기 쉽고 예측하기 쉽다. 단점은 작업이 끝나기를 기다리는 동안 다른 작업을 수행할 수 없어 효율성이 떨어진다.

 

비동기 (Asynchronous)

작업들이 독립적으로 실행이 되고, 완료를 기다리지 않고 다른 작업을 시작한다. 장점은 여러 작업을 동시에 처리 가능하여 효율적으로 작동한다. 단점은 프로그램의 흐름을 추적하고 관리하기가 힘들다.

비동기 코드가 어려운 이유는 일반적으로 작성하는 코드는 동기식이다. 이는 명령이 순차적으로 실행되는 것을 의미한다. 비동기 코드는 순차적인 흐름이 아니기 때문에 코드의 실행 흐름을 따라가기가 다소 어렵다. 비동기 코드는 내부적으로 비동기 메커니즘에 의해 동기와는 다른 방식으로 처리가 된다. 개발자는 코드를 동기적으로 작성하지만, 비동기 코드는 시스템의 내부 처리 프로세스에 의해 비동기 식으로 처리가 되는 데서 오는 혼란이 있다. 비동기 코드를 동기식으로 보이도록 발전을 하고 있다. (async await)

 

비동기 코드 종류들

  • HTTP 요청 처리: 웹앱은 지속적으로 서버와 데이터 주고받으며 작동한다. 페이지 전체를 새로고침 하지 않고도 서버에 데이터를 요청하고, 받아와야 한다. 이를 위해 HTTP 요청을 통해 데이터를 교환한다. 비동기 요청을 쓰는 이유는 사용자가 데이터를 기다리는 동안 페이지의 다른 부분을 자유롭게 사용할 수 있게 하기 위함이다. XHR (XMLHttpRequest), Fetch API
  • 비동기 프로그래밍 패턴: callback, Promise(객체), async/await(코드를 동기적으로 보이게 함)
  • 타이머 및 애니메이션
  • 백그라운드 작업 (Web Workers, Service Workers) : 메인스레드와 분리된 백그라운드 스레드에서 코드를 실행할 수 있게 해주고, 복잡한 계산이나 리소스 집약적인 작업을 효율적으로 처리할 수 있게 비동기로 작동이 되고 있다.

전체적인 웹 어플리케이션의 성능, 사용자 경험, 효율성 개선을 위해 비동기 코드를 사용한다. 비동기적인 접근 방식은 UI 반응성을 높이고 백엔드 서버와 효율적인 통신을 가능하게 하고 리소스 사용을 최적화할 수가 있다. 따라서 현대 웹개발에서는 비동기 프로그램 기술을 숙지하고 활용하는 것이 매우 중요하다.

 

동기적인 코드

function fetchData() {
    const data = '비동기 데이터';
    console.log(data);
}

fetchData();
console.log('콘솔 로그 실행');

 

result console

비동기 데이터
콘솔 로그 실행

 

비동기적인 코드

function fetchData() {
    setTimeout(() => {
        const data = '비동기 데이터';
        console.log(data);
    }, 0)

}

fetchData();
console.log('콘솔 로그 실행');

 

result console

콘솔 로그 실행
비동기 데이터

 

비동기가 뒤늦게 작동하는 이유는 브라우저의 이벤트 루프 때문이다. fetch데이터로 가져온 데이터를 코드 순서상 로그를 통해 다른 문자열과 함께 보여주려면 어떻게 해야 할까? 앞선 코드에서 콘솔 로그는 비동기 코드 보다 먼저 실행이 되었다.

callback은 어떤 이벤트나 조건이 발생한 후에 호출되는 함수를 의미한다. 특정 작업이 완료 되었을 때 시스템이나 다른 함수로부터 되돌려 받는 호출을 말한다. 프로그래밍에서 비동기 처리나 이벤트 리스닝 그리고 특정 작업 완료 후 추가 동작을 지정할 때 사용되는 패턴이다. 콜백함수는 비동기 데이터를 받아온 다음 실행이 된다.

 

비동기적인 코드 + 콜백

function fetchData(callback) {
    setTimeout(() => {
        const data = ' 채채입니다.';
        callback(data);
    }, 0)

}

fetchData((data) => {
    console.log('안녕하세요.', data)
});

 

result console

안녕하세요.  채채입니다.

 

단순한 한개가 아니라 여러개 비동기 데이터를 받아와서 모두 순차적으로 보여주어야 한다면?

 

콜백 여러 개

function fetchData1(callback) {
    setTimeout(() => {
        const data = '이름';
        callback(data);
    }, 0)
}

function fetchData2(callback) {
    setTimeout(() => {
        const data = '나이';
        callback(data);
    }, 0)
}

function fetchData3(callback) {
    setTimeout(() => {
        const data = '성별';
        callback(data);
    }, 0)
}

function fetchData4(callback) {
    setTimeout(() => {
        const data = '지역';
        callback(data);
    }, 0)
}

fetchData1((data1) => {
    console.log(data1);
    fetchData2((data2) => {
        console.log(data2);
        fetchData3((data3) => {
            console.log(data3);
            fetchData4((data4) => {
                console.log(data4)
            })
        })
    })
})

 

result console

이름
나이
성별
지역

 

비동기 작업을 연속적으로 처리하기 위해서 콜백 함수들이 중첩이 된다. 중첩 레벨이 점점 깊어진다. 이런 패턴을 콜백 지옥이라고 부른다. 비동기 작업이 계단식으로 쌓이고, 시각적으로 코드를 파악하기가 어렵다. 따라서 이런 방식으로 작성된 코드는 유지보수가 어렵고, 더 복잡해지는 경우 수정 확장 부분을 찾기가 어렵다. 각 콜백의 역할과 서로간의 관계를 이해하는데 있어 시간이 필요하다.

 

콜백지옥

자바스크립트와 같은 비동기 프로그래밍에서 콜백 패턴을 사용했을 때 발생하는 문제이다. 많이 중첩된 콜백 함수들이 복잡하게 얽혀 코드의 가독성과 유지 보수성이 떨어진다. 하나의 콜백 함수 안에 다른 콜백 함수가 있고… xn 있다. 이러한 중첩이 깊어져서 코드의 가독성이 떨어진다. 디버깅과 오류 처리가 어렵다. 중첩된 콜백 구조에서 오류가 발생한다면, 어디에서 오류가 발생했는지 특정하고 여러 레벨의 콜백을 거슬러 올라가야 하기 때문에 혼란스러울 수 있다. 코드의 가독성 저하 때문에 중첩된 콜백 구조는 수정이나 확장이 어렵고 유지 보수 복잡성을 증가 시킨다.

 

Promise

 

소개

일종의 객체이다. 비동기 작업에 대한 미래의 완료, 실패와 그 결과 값을 나타낸다. Promise를 사용하게 되면 비동기 메서드에서 마치 동기 메서드 처럼 값을 반환할 수 있다. 다만, 최종 결과를 반환하는 것이 아니라 미래 어떤 시점에 결과를 약속하겠다는 일종의 약속이다.

미래의 결과에 대한 약속이다. Promise는 비동기 작업이 완료되었을 때 얻게 될 결과값에 대한 일종의 약속이다. 이 말은 작업이 바로 완료되지 않더라도 미래에 어떤 결과를 제공할 것이라는 약속을 의미한다. 이 Promise는 비동기 작업이 완료될 때까지 기다리겠다는 약속을 나타낸다. 작업이 성공적으로 완료되면 결과값을 , 실패하면 오류를 반환한다. 이 Promise는 세가지 상태 (대기, 성공, 실패)를 가진다. 한 번 상태가 결정되면, 그 상태는 변경되지 않는다. Promise가 fulfilled , reject 상태이면, 다른 상태로 전환될 수 없다. Promise를 사용하면 비동기 작업의 결과를 확실하게 처리한다. then은 성공 시에, catch는 실패 시에 오류를 처리한다. 이를 통해 작업의 결과를 명확하게 처리할 수 있다. Promise는 비동기 작업의 흐름을 더욱 명확하게 하고 예측 가능하게 만들어준다.

 

Promise의 사용법

resolve, reject, then, catch가 사용이 된다.

resolve는 Promise가 성공적으로 완료되었을 때 사용이 되고, reject는 실패했을 때 해당 정보를 인자로 받는다. resolve가 호출이 되면, Promise는 fulfilled 라고 하는 성공 상태가 된다. Promise는 성공적으로 완료가 되고 결과값으로 전달된 데이터를 가지고 Promise를 fulfilled상태로 전환을 한다. reject는 Promise에서 오류가 발생되었을 때 호출이 되고, 이 함수는 오류 객체나 오류에 대한 정보를 인자로 받아서 이 reject가 호출이 되면 Promise상태는 rejected상태가 된다. 실패 상태로 변경하고 오류 정보를 전달해서 rejected상태가 된다. then 메소드는 Promise가 성공적으로 완료되었을 때 실행되는 핸들러이고, 이 Promise가 성공했을 때, 즉 fulfilled 상태가 되었을 때, 콜백 함수를 등록한다. 이 메서드는 두 개의 인자를 받을 수 있다. 첫번째 인자를 Promise가 성공적으로 이행되었을 때 실행되는 함수이고, 두번째 인자는 Promise가 거부되었을 때 실행되는 함수이다. 일반적으로는 두번째 인자로 넣지 않고 catch 메서드에 작성한다. catch는 then 메서드와는 정반대로 promise에서 발생한 오류를 처리하기 위한 일종의 핸들러 메서드이다. 이 메서드는 Promise가 거부될 때 reject함수가 호출될 때 catch가 실행이 된다.

 

Promise의 세가지 상태 (동작들의 결과가 나타남)

pending 대기 - 비동기 작업 진행 중, 아직 완료되지 않은 상태

fulfilled 성공 - resolve 메서드와도 연관이 있다. resolve 메서드가 실행되면 Promise 는 fulfilled 상태가 되고, fulfilled 상태일 때, then 메서드를 통해 콜백함수가 실행된다.

rejected 실패 - Promise가 실패했을 때 상태이다. Promise의 reject 함수가 호출되었을 때, Promise의 상태가 rejected 상태가 되고, rejected 상태일 때, catch 메서드를 통해 catch에 등록된 콜백 함수가 실행된다.

비동기 프로그래밍에서 예측 가능하고 관리하기 쉬운 구조를 제공한다.

new Promise((resolve) => {
    resolve('data');
}).then(result => {
    console.log('Success: ', result);
});

 

result console

Success:  data

 

+reject 사용하여 강제로 오류발생, catch 메서드로 오류 잡아보기

 

catch 메서드는 reject 함수가 호출되었을 때 실행될 콜백함수를 등록할 수 있다. 이 메서드는 오류 처리 메커니즘을 구축하는 데 중요하다. 비동기 연산 중 발생할 수 있는 예외나 오류를 안정적으로 처리할 수 있는 방법을 제공한다.

reject함수를 사용하고 Promise 를 명시적으로 거부하고 catch로 오류를 처리하는 예시

new Promise((resolve, reject) => {
    reject('error code 1');
}).catch(error => {
    console.log('Caught an error: ', error);
})

 

result console

Caught an error:  error code 1

 

API 요청 예시

const XMLHttpRequest = require('xhr2');
const xhr = new XMLHttpRequest();
function fetchData() {
    return new Promise((resolve, reject) => {
        xhr.open("GET", "<https://jsonplaceholder.typicode.com/todos/1>");

        xhr.onload = function () {
            if (xhr.status >= 200 && xhr.status < 300) {
                resolve(JSON.parse(xhr.responseText));
            } else {
                reject(new Error("Failed to load: " + xhr.statusText));
            }
        }

        xhr.send();
    })
}

fetchData()
    .then(data => {
        console.log('Fetched data', data);
    })
    .catch(error => {
        console.error('Error 발생', error);
    })

result console

Fetched data { userId: 1, id: 1, title: 'delectus aut autem', completed: false }

 

Promise는 다양한 비동기 메서드를 지원한다. 각각의 메서드는 비동기 작업을 관리하는 다른 방식들을 제공한다.

  1. all

여러 개의 Promise를 병렬로 처리한다. 배열을 인자로 받고 결과로 배열을 반환한다. 하나라도 거부 (reject)되면 즉시 거부를 한다. 여러 비동기 작업을 동시에 시작하고, 모든 작업이 완료된 후 결과를 한 번에 처리 해야 할 때 유용하다.

Promise.all([
    new Promise(resolve => setTimeout(() => resolve('Promise1 완료'), 3000)),
    new Promise(resolve => setTimeout(() => resolve('Promise2 완료'), 2000)),
    new Promise(resolve => setTimeout(() => resolve('Promise3 완료'), 1000)),
]).then(console.log)

 

result console

[ 'Promise1 완료', 'Promise2 완료', 'Promise3 완료' ]

 

3초 뒤 콘솔이 출력된다. Promise.all은 차례대로 실행이 되는 것은 아니고 독립적으로 각각 실행이 된다. Promise.all에 의해 모든 Promise들이 완료될 때까지 기다린다. 모든 Promise가 이행되었을 때, 결과 값을 배열로 반환한다.

 

1. race

가장 먼저 이행되거나 거부되는 결과를 반환한다. 나머지 Promise의 결과는 무시된다. 여러 비동기 작업 중 최초로 완료되는 작업만 필요할 때 유용하다.

Promise.race([
    new Promise(resolve => setTimeout(() => resolve('Promise1 완료'), 3000)),
    new Promise(resolve => setTimeout(() => resolve('Promise2 완료'), 2000)),
    new Promise(resolve => setTimeout(() => resolve('Promise3 완료'), 1000)),
]).then(console.log)

 

result console

Promise3 완료

Promise3은 setTimeout의 1초로 설정이 되었기 때문에, Promise들 중 가장 먼저 완료된다. 가장 먼저 완료가 된 Promise3이 콘솔에 출력이 된다. Promise.race는 동시에 실행이 되지만 가장 빠르게 완료가 되는 것을 반환한다.

 

2. any

만약 성공 데이터 중에 가장 빨리 Promise를 보여주고 싶으면 any 메서드를 사용하면 된다. 실패 처리를 하기 위해 Promise3에 reject 추가

Promise.any([
    new Promise(resolve => setTimeout(() => resolve('Promise1 완료'), 3000)),
    new Promise(resolve => setTimeout(() => resolve('Promise2 완료'), 2000)),
    new Promise((resolve, reject) => setTimeout(() => reject('Promise3 완료'), 1000)),
]).then(console.log)

 

result console

Promise2 완료

 

성공하는 Promise 중에서 가장 빠르게 실행되는 Promise2가 출력이 된다. 실패하는 Promise는 무시가 된다.

실제 개발 환경에서는 직접 Promise객체를 생성하는 것보다 fetch API(웹에서 데이터를 가져오기 위해 사용되는 JS 내장 기능, 네트워크 요청을 보내고 응답을 받는 비동기 작업을 간단히 처리할 수 있게 한다.), async/await를 사용하는 경우가 많다. fetch API는 Promise를 반환하고 이를 통해 Promise의 API인 then, catch 메서드를 사용할 수 있다. fetch API는 요청이 성공적으로 완료되면, Promise의 이행 resolve를 처리하고 response객체가 이행값으로 이용이된다. fetch API는 resolve와 reject를 자동으로 처리한다. 이는 fetch API가 Promise를 반환하고 Promise의 상태가 fetch 요청의 성공과 실패에 따라 결정이 된다. fetch 요청이 성공적으로 완료될 때, Promise를 resolve 한다. 네트워크 오류 등으로 인해 실패하면 Promise를 reject 처리하고, catch 블록이 실행되어 오류를 처리할 수 있다.

 

fetch API 사용

function fetchData() {
    return fetch("<https://jsonplaceholder.typicode.com/todos/1>")
    .then((response) => {
        return response.json()
    })
}

fetchData()
    .then(data => {
        console.log('Fetched data', data);
    })
    .catch(error => {
        console.error('Error 발생', error);
    })

 

result console

Fetched data { userId: 1, id: 1, title: 'delectus aut autem', completed: false }

 

선언적이고 가독성 좋은 코드를 작성할 수 있다.

fetch API에는 Promise가 2번 사용된다. 네트워크 요청 완료를 위해, 응답 구문의 파싱을 위해 사용 된다.

 

Promise 체이닝

여러 비동기 작업을 순서대로 연결하여 실행할 수 있게 해준다. Promise 체이닝을 사용하면 비동기 작업을 깔끔하게 연속적으로 수행하고, 각 단계의 결과를 다음 단계로 전달 할 수 있다. Promise 체이닝의 작동방식은 체인의 시작점이 되는 Promise를 생성하거나 받고 then 메서드를 사용해서 Promise가 성공적으로 완료 되었을 때, 실행할 콜백함수를 지정한다. 콜백함수는 이전 Promise의 결과를 인자로 받고 새로운 값을 또 반환할 수 있다. 반환된 promise는 then의 콜백함수가 반환되는 값으로 resolve되거나, Promise에서 예외가 발생되면 reject된다. 이렇게 반환된 Promise는 또 다른 then을 호출해서 체이닝을 계속할 수 있다. 이 과정을 여러번 반복할 수 있다. 체인 중간에 발생하는 오류는 catch 메서드로 처리할 수 있다. catch 또한 Promise를 반환하므로 체이닝을 유지할 수 있다.

function fetchData(id) {
    return fetch(`https://jsonplaceholder.typicode.com/todos/${id}`)
        .then((response) => {
            return response.json()
        })
}

fetchData(1)
    .then(data => { console.log('Fetched data1', data); return fetchData(2); })
    .then(data => { console.log('Fetched data2', data); return fetchData(3); })
    .then(data => { console.log('Fetched data3', data); return fetchData(4); })
    .catch(error => { console.error('Error 발생', error); })

 

result console

Fetched data1 { userId: 1, id: 1, title: 'delectus aut autem', completed: false }
Fetched data2 {
  userId: 1,
  id: 2,
  title: 'quis ut nam facilis et officia qui',
  completed: false
}
Fetched data3 { userId: 1, id: 3, title: 'fugiat veniam minus', completed: false }

 

fetch API는 Promise를 리턴하여 긴 체인으로 실행을 할 수 있다. then 메서드를 새로운 Promise를 반환하기 때문에 계속해서 체이닝을 반복하여 진행할 수 있다.

 

 

Async, Await

Promise를 쉽게 사용할 수 있도록 ES6에 추가된 문법이다. 비동기 프로그래밍을 처리하는 가장 현대적인 방법이다. 비동기 코드를 동기 코드처럼 읽고 쓸 수 있게 한다. 코드의 가독성이 향상되고 복잡한 비동기 흐름을 잘 이해할 수 있다. try, catch로 에러 처리를 할 수 있다. 디버깅이 용이하다 Stack trace가 더 명확하고 전통적인 디버깅 기법을 적용할 수 있다. async await를 사용하면 복잡한 비동기 흐름 제어와 테스트 동기화를 쉽게할 수 있다. 여러 비동기 작업을 순차적으로, 병렬로 실행하는 것이 훨씬 간단하다. 비동기 작업의 의도를 명확하게 표현할 수 있다.

 

async는 비동기 함수를 정의할 때 사용된다. 함수를 선언할 때, 그 함수 이전에 async 키워드를 추가하면 비동기 함수로 정의가 된다. async 키워드를 추가한 함수는 항상 Promise를 반환한다. 이 함수에서 반환된 값은 자동으로 Promise를 감싸서 반환한다. 만약 함수가 Promise를 반환한다면 async 함수는 해당 Promise에 대한 결과값을 그대로 반환한다. async 함수 내에서 발생하는 예외는 Promise가 거부 됨을 의미한다. await 연산자는 async 함수 내에서만 사용할 수 있다. 가장 최근 문법인 ES2022 부터는 모듈의 최상위 레벨에서 await 사용이 가능한 top level await 기능을 제공한다. async 없이도 사용 가능한 문법이다. 하나의 거대한 async 함수처럼 동작 모듈 로딩과 사용이 더 유연하고 직관적으로 async함수가 필요 없어서 간결하다. await는 Promise 결과가 나올 때까지 함수 실행을 일시 중지하고 Promise의 결과값을 반환한다. 이 await는 Promise가 이행될 때까지 기다린 후에 결과 값을 반환한다. await가 일반 값에 사용된다면, 해당 값은 바로 반환한다.

const myName = async () => {
    return "myName";
}
myName();

 

result console

Promise {<fulfilled>: 'myName'}

 

async 키워드를 사용하면 함수는 자동으로 비동기 함수가 된다. 그리고 이 함수가 반환하는 값은 암시적으로 Promise 객체로 감싸져서 반환이 된다. 따라서 async 함수에서 단순한 문자열만을 반환하더라도 이 반환값은 자동으로 객체가 되고 Promise는 이행상태 fulfilled로 반환된다. async는 어떻게 자동으로 Promise를 반환을 할까? 이 코드는 Promise의 resolve를 통해서 이행상태의 Promise로 감싸지는 것과 유사한 효과를 가진다. async 키워드를 사용하는 함수에서 단순한 값을 반환하는 것은 명시적으로는 promise resolve를 호출해서 같은 값을 반환하는 것과 동일한 결과를 초래한다.

const myName2 = () => {
    return Promise.resolve("myName");
}
myName2();

 

result console

Promise {<fulfilled>: 'myName'}

 

결과가 동일하다. async함수는 반환값을 자동으로 Promise로 감싼다는 것을 이 코드를 통해 이해할 수 있다.

비동기 데이터 API 를 호출하는 코드를 작성해보자.

function getData() {
    return fetch(`https://jsonplaceholder.typicode.com/posts/1`)
}

const loadDataPromise = () => {
    getData()
    .then(res => res.json())
    .then(data => console.log(data))
}

loadDataPromise();

 

result console

{
  userId: 1,
  id: 1,
  title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit',
  body: 'quia et suscipit\\n' +
    'suscipit recusandae consequuntur expedita et cum\\n' +
    'reprehenderit molestiae ut ut quas totam\\n' +
    'nostrum rerum est autem sunt rem eveniet architecto'
}

 

별도의 Promise 메서드를 사용해야 하는 특성이 있다. then 메서드를 사용하지 않고 결과값을 변수에 담아서 동기적으로 코드를 작성하려면 async await를 사용하면 된다.

 

function getData() {
    return fetch(`https://jsonplaceholder.typicode.com/posts/1`)
}

const loadDataAwait = async () => {
    const result = await getData();
    const data = await result.json();
    console.log(data);
}

loadDataAwait();

 

result console

{
  userId: 1,
  id: 1,
  title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit',
  body: 'quia et suscipit\\n' +
    'suscipit recusandae consequuntur expedita et cum\\n' +
    'reprehenderit molestiae ut ut quas totam\\n' +
    'nostrum rerum est autem sunt rem eveniet architecto'
}

 

await 사용하면 함수의 실행 결과를 변수에 직접 할당할 수 가 있다. 이 함수 내에서 전체적인 흐름이 더욱 명확하게 표현될 수 있다. 프로그램의 구조를 이해하기가 수월하고 디버깅하기가 쉽다. await 키워드는 async 함수가 반환한 Promise의 결과를 기다렸다. Promise가 이행되면 그 결과값을 반환한다.

 

Await

비동기 작업의 디버깅과 데이터 조작을 더 수월하게 한다. 동기 코드처럼 순차적으로 작성할 수 있게 된다. await를 사용하면 동기 호출처럼 보이기 때문에 코드 흐름을 더 쉽게 이해하고 추적이 가능하다. 이 비동기 작업의 결과를 변수에 할당할 수 있어서, 중간 결과를 저장하거나, 디버깅할 때에 유용하다. 변수에 할당을 하면, 변수를 사용해서 데이터를 조작하거나 조건문, 반복문, 함수 호출 등과 같은 다른 작업을 호출해서 복잡한 데이터를 조작하기에 유리하다. 코드는 동기 코드와 유사하기 때문에 전통적인 방식대로 코드의 특정 지점에 break point를 설정하여 변수의 값을 검사하는 것이 더 간단해진다. try-catch 블록을 사용하여, 비동기 작업 중 발생하는 에러를 동기 코드에서 에러를 처리하는 것처럼 catch를 하고 처리할 수 있다. 코드의 어느 부분에서 에러가 발생했는지 파악하기가 쉽니다.

 

Promise 구문에서 예외 처리

function getData() {
    return fetch(`https://jsonplaceholder.typicode.com/post/1`)
}

const loadDataPromise = () => {
    getData()
        .then(res => {
            if (!res.ok) {
                throw new Error(`HTTP Error! status : ${res.status}`)
            }
            return res.json();
        })
        .then(data => console.log(data))
        .catch(error => {
            console.error(`Error fetching data: `, error)
        })
}

loadDataPromise();

 

result console

Error fetching data:  Error: HTTP Error! status : 404

 

async await에서 예외처리

function getData() {
    return fetch(`https://jsonplaceholder.typicode.com/post/1`)
}

const loadDataAwait = async () => {
    try {
        const result = await getData();
        if (!result.ok) {
            throw new Error(`HTTP Error! status : ${res.status}`)
        }

        const data = await result.json();
        console.log(data);
    } catch (error) {
        console.error(`Error fetching data: `, error.message);
    }

}

loadDataAwait();

 

result console

Error fetching data:  res is not defined

 

try catch를 사용하면 코드가 좀 더 동기 코드처럼 읽히고, 이해하기가 쉬워진다. 이 블록 내에서 비동기 코드를 실행하면서도 동시에 에러 처리를 할 수 있어서 코드 흐름을 따라가기가 수월하다. try catch를 사용하면 한 블록 내에서 여러 await 호출을 처리할 때 발생할 수 있는 모든 에러를 catch에서 잡을 수가 있다. 코드의 일관성을 유지하고, 에러처리 로직을 중앙 집중화해서 관리하기 쉽게 한다. 에러가 발생한 stacktrace가 명확해지는 장점이 있다. 디버깅 시에 에러의 원인을 파악하기가 수월하다. 조건부 로직을 사용하여 특정 조건에 따라 다른 에러 처리를 수행할 수 있다. 단순한 catch 메서드를 사용할 때보다 유연한 에러 처리가 가능하다.

 

중첩 비동기 호출을 Promise로 처리하는 코드

const getAuthorName = (id) => {
    return fetch(`https://jsonplaceholder.typicode.com/posts/${id}`)
        .then((response) => response.json())
        .then((post) => post.userId)
        .then((userId) => {
            return fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
                .then((response) => response.json())
                .then((user) => user.name)
        })
        .catch(error => console.log('error', error));
}

getAuthorName(1).then((name) => console.log("name: ", name));

 

위 코드를 async await로 작성

const getAuthorName = async (id) => {

    try {
        const postResponse = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
        const post = await postResponse.json();
        const userId = post.userId;

        const userResponse = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
        const user = await userResponse.json();
        const userName = user.name;

        console.log("name: ", userName);
    } catch (error) {
        console.log("error", error)
    }

}

getAuthorName(1);

 

외부에서 then을 사용할 필요없다.

Promise.All을 사용하여 여러 Promise들을 실행하고 그 중 하나라도 실패하는 경우에 에러를 핸들링하는 코드를 작성해보자.

function execute() {
    const promises = [
        new Promise((resolve, reject) => { console.log('Success Promise1'); resolve('sucess') }),
        new Promise((resolve, reject) => { console.log('Success Promise2'); resolve('sucess') }),
        new Promise((resolve, reject) => { console.log('Success Promise3'); resolve('sucess') }),
    ]
    return Promise.all(promises);
};

async function main() {
    try {
        await execute();
    } catch (error) {
        console.log('error: ', error)
    }
}

main(); 

 

 

[참고]

패스트캠퍼스 강의 (내 생에 마지막 JavaScript : 기초 문법부터 실무 웹 개발까지 한 번에 끝내기 초격차 패키지 Online) 를 기반으로 작성하였고, 추가 서치 내용을 정리하였습니다.

'개발공부 > Javascript' 카테고리의 다른 글

[JS]프로토타입  (1) 2024.04.28
[JS] 이벤트 루프  (1) 2024.04.18
[JS] 이벤트 버블링과 캡처링  (0) 2024.04.04
[JS] 호이스팅  (0) 2024.03.28
[JS] 기본형 데이터와 참조형 데이터  (0) 2024.03.24