모던 자바스크립트 Deep Dive 45장-프로미스

HustleKang·2022년 5월 23일

자바스크립트는 비동기 처리를 위해 콜백 패턴을 사용한다
하지만 이런 콜백 패턴 방식은 비동기 처리가 여러 개일 때 가독성이 매우 나쁘고, 에러 처리가 곤란하다
ES6부터 비동기 처리를 위한 패턴으로 프로미스를 도입

비동기 처리를 위한 콜백 패턴의 단점

콜백 헬

비동기 함수 내부의 비동기로 동작하는 코드는 비동기 함수가 종료된 이후에 완료되기 때문에
비동기 함수 내부의 비동기로 동작하는 코드에서 처리 결과를 외부로 반환하거나 상위 스코프의 변수에 할당해도 우리가 의도한 대로 동작하지 않음
따라서 비동기 함수의 처리 결과에 대한 후속 처리를 비동기 함수의 내부에서 수행해야 한다

let number = 0;
setTimeout(()=>{number = 100;},0);
console.log(number); // 100을 원했으나 0이 출력됨 
// number=100 할당이 console.log를 마친 뒤에 일어나기 때문에
const get = url => {
	const xhr = new XMLHttpRequest();
    xhr.open("GET",url);
    xhr.send();
    
    xhr.onload = () => {
    	if (xhr.response===200) return JSON.parse(xhr.response);
        console.error(xhr.status,xhr.statusText);
    }; 
};

const response = get('https://jsonplaceholder.typicode.com/posts/1');
console.log(response); // undefined 

get 함수는 onload 이벤트에 이벤트 핸들러를 할당하고 종료된다
반환값이 없으므로 undefined를 반환하게 되고
response에는 undefined가 할당된다
이후에 응답이 완료되면 할당한 이벤트 핸들러가 응답 값을 반환하지만 그 값을 이용할 방법이 없다

따라서 비동기 함수 내부에서 후처리를 해주면

const get = (url,callback) => {
	const xhr = new XMLHttpRequest();
    xhr.open("GET",url);
    xhr.send();
    
    xhr.onload = () => {
    	if (xhr.response===200) {
        return callback(JSON.parse(xhr.response));
        }
        else{
        	console.error(xhr.status,xhr.statusText);
        }
    }; 
};

get('https://jsonplaceholder.typicode.com/posts/1',console.log);

위에는 후처리가 1번이지만 후처리가 여러번 이라면 콜백 함수의 호출이 중첩되면서 매우 복잡해진다
이러한 현상을 콜백 헬 이라고 한다

  1. id가 1인 post의 정보를 받아오고
  2. 해당 포스트의 userId를 통해 다시 user의 정보를 받아온다

위 과정을 코드로 작성해보면

//get 함수는 위에 선언된 get과 동일

get('https://jsonplaceholder.typicode.com/posts/1',({userId})=>{
  get(`https://jsonplaceholder.typicode.com/users/${userId}`,(userData)=>{
    console.log(userData);
  });
});

// 후처리가 2번인데도 이렇게 복잡하다

에러 처리의 한계

try{
	setTimeout(()=>{throw new Error('에러!');},0);
} catch(e) {
	console.error(e);
}

try 코드 블록이 실행되고 블록 내부에서 에러가 발생하게 되면 catch문에 변수로 에러가 전달되고 catch 블록 내부가 실행된다

에러는 호출자 방향으로 전파된다 (콜 스택의 아래 방향)

하지만 위 예시에서 에러를 발생시키는 콜백 함수는 테스큐 큐에서 대기하다가 콜 스택이 비었을 때 실행되므로 catch가 불가능

이런 콜백 헬과 에러 처리 문제를 해결하기 위해 ES6에서 프로미스 도입

프로미스의 생성

  • ECMAScript 사양에 정의된 표준 빌트인 객체
  • 프로미스는 비동기 처리 상태와 처리 결과를 관리하는 객체
  • new 연산자와 함께 호출하여 프로미스 객체를 생성
  • 이 때 인수로 콜백 함수를 받는다
  • 인수로 받는 콜백 함수는 resolve 함수와 reject 함수를 인수로 받는다
  • 인수로 전달 받은 콜백 함수의 내부에서 비동기 처리가 수행된다
    - 비동기 처리 성공 시 resolve 함수 호출
    - 실패 시 reject 함수 호출
// 대충 쓰면 
const promise = new Promise((resolve,reject)=>{
  if (비동기 처리가 성공) resolve(값);
  else reject(값);
});

위에서 get 함수를 프로미스를 이용하여 다시 정의하면

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));
    }
  });
}

프로미스는 비동기 처리의 상태를 나타내는 상태 정보를 갖는다

  • pending : 비동기 처리가 아직 수행 되지 않은 상태
  • fulfilled : 비동기 처리가 수행된 상태 (성공)
    = resolve함수가 호출 된 이후
  • rejected : 비동기 처리가 수행된 상태 (실패)
    = reject 함수가 호출된 이후

fulfilled와 rejected 상태를 settled 상태 라고 함

프로미스는 비동기 처리 상태와 처리 결과를 관리하는 객체

프로미스의 후속 처리 메서드

  • then
  • catch
  • finally

프로미스의 비동기 처리 상태가 변화하면 (pending -> settled)
후속 처리 메서드에 인수로 전달한 콜백 함수가 호출되고
이 때 전달한 콜백 함수에는 프로미스의 처리 결과가 인수로 전달된다

모든 후속 처리 메서드는 프로미스를 반환하고 비동기로 동작

Promise.prototype.then

두 개의 콜백 함수를 인수로 전달받는다

  • 첫 번째 콜백 함수는 fulfilled 되면 호출됨
    = 비동기 처리 성공 시
  • 두 번째 콜백 함수는 rejected 되면 호출됨
    = 비동기 처리 실패 시
new Promise(resolve => resolve('비동기 성공!'))
  .then(result=>console.log(result), err => console.error(err));

new Promise((resolve,reject) => reject(new Error('비동기 실패!')))
  .then(result=>console.log(result), err => console.error(err));

then 에 인수로 전달한 콜백 함수가 프로미스를 반환하면 걔를 그대로 반환,
값을 반환하면 그 값을 암묵적으로 resolve 또는 reject하여 프로미스를 생성해 반환

Promise.prototype.catch

한 개의 콜백 함수를 전달받는다
프로미스가 rejected 상태가 되면 콜백 함수를 호출

new Promise((resolve,reject) => reject(new Error('비동기 실패!')))
  .catch(err => console.log(err));

Promise.prototype.finally

한 개의 콜백 함수를 전달받는다
fulfilled,rejected 상관 없이 무조건 한 번 인수로 넘긴 콜백 함수 호출

프로미스의 에러 처리

후속 처리 메서드인 then과 catch로 에러 처리 가능
then의 경우 두 번째 콜백 함수로 처리할 수 있지만 fulfilled 상태가 되어 첫 번째 콜백 함수를 실행하는 도중에 발생한 에러는 캐치가 불가능

-> catch 메서드는 비동기 처리에서 발생한 에러 + then 메서드 내부의 에러까지 캐치
에러 처리는 catch 메서드에서 하자

// 첫 번째 콜백 함수에서 발생한 에러 캐치 불가
promiseGet(url).then(
  res => console.정의되지않은메서드(res), 
  err => console.error(err));
  
 // then 메서드 내부 에러 캐치 가능
 promiseGet(url)
  .then(res => console.정의되지않은메서드(res))
  .catch(err => console.error(err));
  

프로미스 체이닝

후속 처리 메서드인 then,catch,finally 모두 프로미스를 반환하기 때문에 연속으로 호출 가능
= 프로미스 체이닝

프로미스는 콜백 헬이 발생하지는 않지만 결국 콜백 함수는 사용한다
async/await 을 사용하면 프로미스 후속 처리 메서드 없이 동기 방식으로 동작하는 것처럼 프로미스가 처리 결과를 반환하게 할 수 있음

const get = (url,callback) => {
	const xhr = new XMLHttpRequest();
    xhr.open("GET",url);
    xhr.send();
    
    xhr.onload = () => {
    	if (xhr.response===200) {
        return callback(JSON.parse(xhr.response));
        }
        else{
        	console.error(xhr.status,xhr.statusText);
        }
    }; 
};

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));
    }
  });
}

// 콜백 헬 예시
get('https://jsonplaceholder.typicode.com/posts/1',({userId})=>{
  get(`https://jsonplaceholder.typicode.com/users/${userId}`,(userData)=>{
    console.log(userData);
  });
});

// 프로미스를 이용하여 작성하면 
promiseGet('https://jsonplaceholder.typicode.com/posts/1')
	.then(({userId}) => promiseGet(`https://jsonplaceholder.typicode.com/users/${userId}`))
    .then(userData => console.log(userData))
    .catch(err => console.error(err));

프로미스의 정적 메서드

Promise.resolve / Promise.reject

이미 존재 하는 값을 래핑하여 프로미스를 생성

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

const rejectedPromise = Promise.reject(new Error('에러!'));
rejectedPromise.catch(console.log); // Error: 에러!

// 위에 코드, 아래 코드 똑같이 동작
const resolvedPromise = new Promise(resolve => resolve([1,2,3]));
resolvedPromise.then(console.log); // [1,2,3

const resolvedPromise = new Promise((resolve,reject) => reject(new Error('에러!')));
rejectedPromise.catch(console.log); // Error: 에러!


Promise.all

  • 여러 개의 비동기 처리를 모두 병렬로 처리할 때 사용
  • 프로미스를 요소로 갖는 이터러블을 인수로 받는다
  • 만약 인수 조건이 충족이 안되면 암묵적으로 Promise.resolve 메서드를 통해 프로미스로 래핑함
  • 전달 받은 프로미스가 모두 fulfilled 상태가 되면 모든 처리 결과를
    인수로 넘겨준 프로미스 순서대로 배열에 담아 반환 (완료 순이 아님)
  • 인수로 받은 프로미스 중 하나라도 rejected 상태가 되면 즉시 종료
const requestData1 = () => {
  return new Promise(resolve => setTimeout(()=>resolve(1),3000));
}

const requestData2 = () => {
  return new Promise(resolve => setTimeout(()=>resolve(2),2000));
}

const requestData3 = () =>{
  return new Promise(resolve => setTimeout(()=>resolve(3),1000));
}

const res = [];

// 6초 뒤에 [1,2,3] 출력
requestData1()
  .then((data)=>{
    res.push(data);
    return requestData2();
  })
  .then(data=>{
    res.push(data);
    return requestData3();
  })
  .then(data=>{
    res.push(data);
    console.log(res);
  })
  .catch(console.error);

각각의 requestData 는 서로에게 영향이 없기 때문에 순차적으로 실행할 필요가 없음
모두 병렬로 실행하려면

Promise.all([requestData1(),requestData2(),requestData3()])
    .then( arr => console.log(arr)); // 3초 컷

// Promise.resolve()를 통해 요소 각각을 프로미스로 래핑한 뒤 처리
Promise.all([1,2,3])
	.then( arr => console.log(arr)); // [1,2,3]

Promise.race

  • 인수로 프로미스를 갖는 이터러블을 전달 받고
  • 가장 먼저 fulfilled 상태가 되는 프로미스의 처리 결과를 resolve 하는 새로운 프로미스를 반환
  • 인수로 받은 프로미스 중 하나라도 rejected 상태가 되면 에러를 reject하는 새로운 프로미스 즉시 반환

Promise.allSettled

  • 인수로 프로미스를 갖는 이터러블 전달 받고
  • 모든 프로미스가 pendding 상태에서 fulfilled,rejected 로 바뀌면 처리 결과 배열에 담아서 반환

마이크로태스크 큐

프로미스의 후속 처리 메서드의 콜백 함수는 태스크 큐가 아니라 마이크로태스크 큐에 저장된다
처리 우선 순위 : 마이크로 태스크 큐 > 태스크 큐

setTimeout(()=>console.log(1),0);
Promise.resolve()
  .then(()=>console.log(2))
  .then(()=>console.log(3));
  
// 태스크 큐에는 : 1 
// 마이크로태스크 큐에는 : 2,3 
// 우선순위 대로 2,3,1 출력

fetch

  • HTTP 요청 전송 기능을 제공하는 Web API
  • 프로미스를 지원
  • 인터넷 익스플로러 제외 모든 브라우저 지원
  • HTTP 응답인 Response 객체를 래핑한 Promise 객체를 반환
  • 후속 처리 메서드 then을 통해 반한된 Response 객체 참조 가능
fetch(url,http 요청 메서드와 헤더와 페이로드 등을 설정한 객체);
fetch(url); // GET 요청 전송

Response.prototype에는 Response 객체에 있는 HTTP 응답 몸체를 참조할 수 있는 다양한 메서드가 있음

  • MIME 타입이 application/json인 HTTP 응답 몸체 취득
    = Response.prototype.json : HTTP 응답 몸체를 역직렬하여 반환
fetch('https://jsonplaceholder.typicode.com/albums/1')
    .then(res => res.json()) // Response객체의 json 메서드로 HTTP 응답 몸체를 역직렬화 
    .then(result => console.log(result)); // 그대로 출력 
// {userId: 1, id: 1, title: 'quidem molestiae enim'}
const request = {
  get(url){
    return fetch(url);
  },
  
  post(url,payload){
    return fetch(url,{
      method : 'POST',
      headers : {'content-Type' : 'application/json'},
      body : JSON.stringify(payload)
      })
  },

  patch(url,payload){
    return fetch(url,{
      method : 'PATCH',
      headers : {'content-Type' : 'application/json'},
      body : JSON.stringify(payload)
    });
  },

  delete(url){
    return fetch(url,{
      method : 'DELETE'
    })
  }
};

이웅모, 『모던 자바스크립트 Deep Dive』, 위키북스(2021)

profile
grindin'

0개의 댓글