SPACE RUMI

Hi, I am rumi. Let's Splattack!

[STUDY] 스터디/Deep Dive JS

js 프로미스, async await, 에러 핸들링

백루미 2023. 2. 10. 20:04
반응형

프로미스 Promise

자바스크립트는 비동기처리를 위한 하나의 패턴으로 콜백함수를 사용하는데, 이는 콜백 지옥으로 인해 가독성이 나쁘고, 비동기 처리도중에 발생한 에러처리가 곤란하며, 비동기 처리를 한번에 하는데도 한계가 있다. 콜백지옥, 에러처리 등을 해결하기위해 나온것이 프로미스다.

프로미스는 비동기 처리상태와 처리 결과를 관리하는 객체다. 
호출을 하면, 결과값을 주는 일종의 약속. 결과값은 then에 저장되어 원할때 쓸 수 있다.

비동기 함수에서의 콜백함수 호출은 비동기로 동작하기때문에 콜백함수 내부에서 처리 결과를 반환하거나, 상위 스코프의 변수에 할당하면 기대한대로 동작하지 않는다 (setTimeout 함수의 경우, 고유한 타이머 id를 반환하므로 콜백함수에서 값을 반환하는것은 무의미하다.)

let test = 0;

setTimeout(()=> {test = 'test'}, 0);
console.log(test); // 0

HTTP 요청 메서드인 GET 요청을 전송하고 서버의 응답을 전달받는 get 함수도 비동기 함수다. (get 함수 내부의 onload 이벤트 핸들러가 비동기로 동작한다)

즉, 비동기 함수는 비동기 처리 결과를 외부에 반환할수없고, 상위 스코프 변수에 할당할 수도 없다. 따라서 비동기 함수의 처리 결과에 대한 후속처리는 비동기 함수 내부에서 수행해야 한다. 이를 위해서 콜백함수를 통해 후속처리를 수행하는 과정을 반복하다 보면.. 콜백 함수 호출이 중첩되고 복잡도가 높아지면서 콜백 헬(콜백지옥)이 열린다.

try ... catch ... finally문에서 에러를 처리하려고해도, 비동기 콜백 함수에 대한 에러를 catch문에서 잡지 못한다. 그 이유는...
비동기 함수의 콜백함수가 실행될때 이미 비동기 함수는 콜스택에서 제거된 상태이다.
(이는 콜백함수를 호출한것이 해당 비동기 함수가 아니라는것) 에러는 호출자 방향으로 전파되기 때문이다.

비동기함수 호출 => 콜스택에 들어감 => 비동기라 콜백호출 대기하지않고 함수 즉시 종료(팝) => (타이머일경우, 타이머가 만료되면) 콜백함수를 태스트큐에 푸시 => 이벤트루프가 콜스택 비었는지 계속 감지 => 콜스택이 비는순간 콜백함수를 콜스택으로 이동시켜줌 => 콜백함수 실행

 

Promise 생성자 함수를 new 연산자와 함께 호출하면 Promise 객체를 생성한다. Promise 생성자 함수는 비동기처리를 수행할 콜백함수를 인수로 전달받는데, 이 콜백함수는 resolve와 reject함수를 인수로 전달받는다.

const promiseGet = (url) => {
return new Promise((resolve, reject)=>{
    const xhr = new XMLHttpRequest();
    xhr.open('GET',url);
    xhr.send();
    
    xhr.onload = () => {
        if(xhr.status === 200){
            resolve(JSON.parse(xhr.response));
        }else {
            reject(new Error(xhr.status));
        }
    }
})
}

promiseGet('https://jsonplaceholder.typicode.com/posts/1');

프로미스는 비동기 처리가 어떻게 진행되고있는지를 나타내는 3가지 상태 정보를 가진다.
pending : 비동기 처리가 아직 수행되지 않은 상태 
fulfilled : 비동기 처리가 수행된 상태(성공)
rejected : 비동기 처리가 수행된 상태(실패)

프로미스는의 비동기 처리 상태가 변화할때 사용할수있는 then, catch, fainally 후속 메서드를 지원한다.
후속 처리 메서드에 인수로 전달한 콜백함수가 선택적으로 호출된다. 이때 후속처리 메서드의 콜백함수에 프로미스의 처리 결과가 인수로 전달된다. 모든 후속 처리 메서드는 언제나 프로미스를 반환하며 비동기로 동작한다.

Promise.prototype.then : 두개의 콜백 함수를 인수로 전달받는데, 첫번째 콜백함수는 성공(fulfilled)일때, 두번째 콜백함수는 실패(rejected)일때 호출된다. 언제나 프로미스를 반환한다. (에러처리는 catch에서 하는것을 권장)

Promise.prototype.catch : 한개의 콜백함수를 인수로 전달받는데, 프로미스가 실패(rejected) 상태인 경우에만 호출된다. 언제나 프로미스를 반환한다.

Promise.prototype.finally : 프로미스의 성공 또는 실패와 상관없이 무조건 한번 호출된다. 언제나 프로미스를 반환한다.
= ex 로딩바

후속처리 메서드는 언제나 프로미스를 반환하므로 연속적으로 처리할 수 있는데, .then().then().then().catch() 
이를 프로미스 체이닝이라 한다. 프로미스도 콜백 패턴을 사용하므로 콜백함수를 사용하지 않는것은 아니다.

Promise는 주로 생성자 함수로 사용되지만, 함수도 객체이므로 메서드를 가질 수 있다. Promise는 5가지 정적메서드를 제공한다.
Promise.resolve 는 인수로 전달받은 값을 resolve하는 프로미스를 생성한다.
Promise.reject 는 인수로 전달받은 값을 reject하는 프로미스를 생성한다. 

const resolvedPromise1 = Promise.resolve([1,2,3]);
resolvedPromise1.then(console.log) ; // [1,2,3]

// 둘 다 동일하게 동작한다.
const resolvedPromise2 = new Promise(resolve => resolve([1,2,3]))
resolvedPromise2.then(console.log) ; // [1,2,3]

 

Promise.all 은 여러개의 비동기 처리를 모두 병렬처리할때 사용한다.
프로미스를 요소로 갖는 배열 등의 이터러블을 인수로 전달받는다. 전달받은 모든 프로미스가 모두 fulfilled(성공) 상태가 되면 모든 처리결과를 배열에 저장해 새로운 프로미스를 반환한다.

하나라도 rejected 상태가 되면 나머지 프로미스가 fulfilled 상태가 되는것을 기다리지 않고 즉시 종료한다.

const requestData1 = () => new Promise(resolve => setTimeout(()=> resolve(1),3000))
const requestData2 = () => new Promise(resolve => setTimeout(()=> resolve(2),2000))
const requestData3 = () => new Promise(resolve => setTimeout(()=> resolve(3),1000))

Promise.all([requestData1(),requestData2(),requestData3()]).then(console.log).catch(console.error)
// pending... 후 3초 뒤 [1,2,3] 리턴

인수로 전달받은 이터러블의 요소가 프로미스가 아닌경우 Promise.resolve를 사용해 프로미스로 래핑한다.

Promise.all([
    1, // Promise.resolve(1)
    2, // Promise.resolve(2)
    3, // Promise.resolve(3)
])

 

Promise.race는 가장 먼저 fulfilled(성공) 상태가 된 프로미스의 처리결과를 resolve하는 새로운 프로미스를 반환한다.

Promise.race([
    new Promise((resolve) => setTimeout(()=> resolve(1),3000)),
    new Promise((resolve) => setTimeout(()=> resolve(2),2000)),
    new Promise((resolve) => setTimeout(()=> resolve(3),1000)),
]).then(console.log).catch(console.log) // pending... 후 3 리턴

Promise.race([
    new Promise((_,reject) => setTimeout(()=> reject(new Error('Error 1')),3000)),
    new Promise((_,reject) => setTimeout(()=> reject(new Error('Error 2')),2000)),
    new Promise((_,reject) => setTimeout(()=> reject(new Error('Error 3')),1000)),
]).then(console.log).catch(console.log) // pending... 후 Error : Error 3 리턴

 

Promise.allSettled는 프로미스를 요소로 갖는 배열 등의 이터러블을 인수로 전달받는다. 그리고 전달받은 프로미스가 모두 settled상태 (비동기 처리가 수행된 상태)가 되면 처리결과를 배열로 반환한다.

Promise.allSettled([
    new Promise(resolve => setTimeout(()=>resolve(1),2000)),
    new Promise((_, reject) => setTimeout(()=>reject(new Error('Error!')),1000),
]).then(console.log)
// [{status:"fulfilled", value:1}, {status:"rejected", reason:Error: Error! at <anonymous>}]

 

마이크로태스크 큐

프로미스의 후속 처리 메서드의 콜백함수는 태스크큐가 아니라 마이크로태스크 큐에 일시저장된다. 이 둘은 별도의 큐다. 그 외의 비동기 함수 콜백함수나 이벤트 핸들러는 태스크큐에 일시 저장된다. 마이크로 태스크 큐는 태스크큐보다 우선순위가 높다.
콜스택이 비면, 마이크로 태스크큐를 가져와 실행하고, 마이크로태스크큐가 비면 태스크큐에서 대기하고있는 함수를 가져와 실행한다.

 

fetch

HTTP 요청 전송 기능을 제공하는 Web API 이다.
XMLHttpRequest보다 사용법이 간단하고 프로미스를 지원하며, 대부분의 모던 브라우저에서 지원한다.

fetch함수는 HTTP응답을 나타내는 Response 객체를 래핑한 Promise 객체를 반환한다.

fetch('http://url...').then(response => console.log(response))

 

문제점)

404 not found나 500 internet server error같은 HTTP에러가 발생해도 reject하지 않고, 불리언 타입의 ok 상태를 false로 설정한 Response 객체를 resolve 한다.

오프라인 등의 네트워크 장애나 cors 에러에 의해 요청이 완료되지 못한경우에만 프로미스를 reject한다.

= 따라서 fetch 함수를 쓸때는 resolve한 불리언타입의 ok 상태를 확인해 명시적으로 에러처리를 해야한다.

 

제너레이터 generator

코드 블록의 실행을 중지했다가 필요한 시점에 재개할수있는 특수한 함수. 함수 실행의 제어권을 호출자에게 양도할 수 있으며, 함수 호출자와 함수의 상태를 주고받을 수 있다. 제너레이트 함수를 호출하면 제너레이터 객체를 반환한다.(이터러블이면서 이터레이터인 제너레이터 객체를 반환)

제너레이터 함수는 function* 키워드로 선언한다.

 

async / await

async await를 사용하면, 마치 동기 처리처럼 프로미스를 사용할 수 있다. 프로미스의 후속 처리 메서드 없이 프로미스가 처리 결과를 반환하도록 구현 할 수 있다.

const fetch = require('node-fetch');

async function fetchTodo(){
	const url = "http://jsonplaceholder.typicode.com/todos/1";
    
    const response = await fetch(url);
    const todo = await response.json();
    console.log(todo); // {...}
}

await 키워드는 반드시 async 함수 내부에서 사용해야한다. 언제나 프로미스를 반환한다.
await 키워드는 프로미스가 settled 상태 (비동기 처리가 수행된 상태)가 될 때까지 대기하다가, 프로미스가 resolve한 처리 결과를 반환한다. await 키워드는 반드시 프로미스 앞에서 사용해야한다.

async await 에서 에러처리는 try catch 문을 사용할 수 있다. 콜백 함수를 인수로 전달받는 비동기 함수와 달리, 프로미스를 반환하는 비동기 함수는 명시적으로 호출할 수 있기 때문에 호출자가 명확하다.

const fetch = require('node-fetch');

const foo = async () => {
 try{
    const wrongUrl = 'http://wrong.url';

    const response = await fetch(wrongUrl);
    const data = await response.json();
    return data;
    
    }catch(err){
		console.error(err); // Type Error : failed to fetch
    }
}

 

async 함수 내에서 catch 문을 사용해서 에러처리를 하지 않으면 async 함수는 발생한 에러를 reject하는 프로미스를 반환한다. => async 함수를 호출하고, Promise.prototype.catch 후속 처리 메서드를 사용해 에러를 캐치할수도 있다.

const fetch = require('node-fetch');

const foo = async () => {
    const wrongUrl = 'http://wrong.url';

    const response = await fetch(wrongUrl);
    const data = await response.json();
    return data;
}

foo().then(console.log).catch(console.error); // TypeError : failed to fetch

 

예외적인 상황에 에러 처리를 하지않으면 프로그램은 강제 종료된다. 따라서 항상 예외케이스가 발생할 수 있다는것을 염두에두고 코드를 작성해야한다. 

 

throw

try...catch...finally의 try 코드블록에서 throw로 에러 객체를 던질 수 있다. 에러를 던지면 catch문의 에러변수가 생성되고, 던져진 에러 객체가 할당된다. 에러는 호출자 방향으로 전파되며, 던져진 throw에러객체를 catch하지 않으면 프로그램은 강제 종료된다.

* 비동기함수인 setTimeout이나 프로미스 후속처리메서드의 콜백함수는 호출자가 없으므로 (태스크큐나 마이크로태스크큐에 일시저장됐다가 콜스택이 비면 이동되는데, 이때 콜스택 맨밑에 존재하게 되므로 에러를 전파할 호출자가 존재하지 않음) 주의가 필요하다.

 

module

모듈은 독자적인 모듈 스코프를 갖는다. 따라서 모듈 내에서 선언한 식별자는 모듈 외부에서 참조할 수 없다. 

<script type="module" src="foo.mjs"></script>
<script type="module" src="bar.mjs"></script>

 

모듈 내부에서 선언한 식별자를 외부에 공개하여 다른 모듈이 재사용 할 수 있게 하려면 export 키워드를 사용한다. 변수, 함수, 클래스 등 모든 식별자를 export 할 수 있다. 다른 모듈에서 export 한 식별자를 자신의 모듈 스코프 내부로 로드하려면 import 키워드를 사용한다.

export const test = 'test';
const test2 = 'test2';
const test3 = 'test3';

export {test2, test3}; // 객체로 구성하여 한번에 내보낼 수 있다.

...

import test from '../testFile.tsx';

 

모듈이 export한 식별자 이름을 일일이 지정하지않고, 하나의 이름으로 한번에 가져올 수 있다.
이때 import되는 식별자는 as뒤에 지정한 이름의 객체에 프로퍼티로 할당된다.

import * as react from 'react.js';

 

export한 식별자 이름을 변경하여 import 할 수 있다.

import {data1 as ProductData, data2 as OrganizationData} from './project.tsx';

 

하나만 내보내려면 default 키워드를 사용한다. 단, var, let, const 키워드는 사용할 수 없다.

export default const test = 'test'; // SyntaxError : Unexpected token 'const'

 

Babel & Webpack

Babel : ES6 + ES.NEXT 사양의 소스를 ES5 사양의 소스코드로 변환 해주는 트렌스파일러. 최신 ECMAscript 지원안하는 브라우저에서도 잘 되게끔 해준다.

Webpack : 리소스를 하나의 파일로 만들어주는 모듈 번들러

반응형

'[STUDY] 스터디 > Deep Dive JS' 카테고리의 다른 글

Deep Dive JS 스터디 회고  (2) 2023.02.13
js 비동기 프로그래밍, Ajax와 HTTP  (0) 2023.02.06
js 타이머  (0) 2023.01.28
js 이벤트  (1) 2023.01.28
DOM  (0) 2023.01.26