본문 바로가기
JS

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

by 학식러 2024. 2. 16.

 

45장 프로미스

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

1 ) 콜백 헬

45-01

// GET 요청을 위한 비동기 함수
const get = url => {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', url);
  xhr.send();

  xhr.onload = () => {
    if (xhr.status === 200) {
      // 서버의 응답을 콘솔에 출력한다.
      console.log(JSON.parse(xhr.response));
    } else {
      console.error(`${xhr.status} ${xhr.statusText}`);
    }
  };
};

// id가 1인 post를 취득
get('<https://jsonplaceholder.typicode.com/posts/1>');
/*
{
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere ...",
  "body": "quia et suscipit ..."
}
*/
  • 비동기 함수를 호출하면 함수 내부의 비동기로 동작하는 코드가 완료되지 않았다 해도 기다리지 않고 즉시 종료됨.
  • 즉 비동기 함수 내부의 비동기로 동작하는 코드는 비동기 함수가 종료된 이후에 완료됨.
  • 따라서 비동기 함수 내부의 비동기로 동작하는 코드에서 처리 결과를 외부로 반환하거나 상위 스코프의 변수에 할당하면 기대한 대로 동작하지 않음.

ex

  • setTimeout 함수를 호출하면 콜백함수를 호출 스케줄링 한 다음, 타이머 id를 반환하고 즉시 종료됨.
  • 즉 비동기 함수인 setTimeout 함수의 콜백함수는 setTimeout 함수가 종료된 이후에 호출됨.

45-02

let g = 0;

// 비동기 함수인 setTimeout 함수는 콜백 함수의 처리 결과를 외부로 반환하거나
// 상위 스코프의 변수에 할당하지 못한다.
setTimeout(() => { g = 100; }, 0);
console.log(g); // 0
  • get함수 내부의 onload 이벤트 핸들러가 비동기로 동작함.
  • get 함수를 호출하면 GET 요청을 전송하고 onload 이벤트 핸들러를 등록한 다음 undefined를 반환하고 즉시 종료됨.
  • 즉 비동기 함수인 get 함수 내부의 onload 이벤트 핸들러는 get 함수가 종료된 이후에 실행됨.
  • 따라서 get 함수의 onload 이벤트 핸들러에서 서버의 응답 결과를 반환하거나 상위 스코프의 변수에 할당하면 기대한 대로 동작하지 않음.
  • 수정

45-03

// GET 요청을 위한 비동기 함수
const get = url => {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', url);
  xhr.send();

  xhr.onload = () => {
    if (xhr.status === 200) {
      // ① 서버의 응답을 반환한다.
      return JSON.parse(xhr.response);
    }
    console.error(`${xhr.status} ${xhr.statusText}`);
  };
};

// ② id가 1인 post를 취득
const response = get('<https://jsonplaceholder.typicode.com/posts/1>');
console.log(response); // undefined
  • 1번은 get함수의 반환문이 아님.
  • 비동기 함수는 비동기 처리 결과를 외부에 반환할 수 없고 상위 스코프의 변수에 할당할 수도 없음.
  • 따라서 비동기 함수의 처리 결과에 대한 후속 처리는 비동기 함수 내부에서 수행해야함.
  • 이때 비동기 함수를 범용적으로 사용하기 위해 비동기 함수에 비동기 처리 결과에 대한 후속 처리를 수행하는 콜백함수를 전달하는 것이 일반적.
  • 필요에 따라 비동기 처리가 성공하면 호출될 콜백 함수와 비동기 처리가 실패하면 호출될 콜백 함수를 전달할 수 있음.

45-06

// GET 요청을 위한 비동기 함수
const get = (url, successCallback, failureCallback) => {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', url);
  xhr.send();

  xhr.onload = () => {
    if (xhr.status === 200) {
      // 서버의 응답을 콜백 함수에 인수로 전달하면서 호출하여 응답에 대한 후속 처리를 한다.
      successCallback(JSON.parse(xhr.response));
    } else {
      // 에러 정보를 콜백 함수에 인수로 전달하면서 호출하여 에러 처리를 한다.
      failureCallback(xhr.status);
    }
  };
};

// id가 1인 post를 취득
// 서버의 응답에 대한 후속 처리를 위한 콜백 함수를 비동기 함수인 get에 전달해야 한다.
get('<https://jsonplaceholder.typicode.com/posts/1>', console.log, console.error);
/*
{
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere ...",
  "body": "quia et suscipit ..."
}
*/
  • 콜백 함수를 통해 비동기 처리 결과에 대한 후속 처리를 수행하는 비동기 함수가 비동기 처리 결과를 가지고 또다시 비동기 함수를 호출 : 콜백 헬

45-07

// GET 요청을 위한 비동기 함수
const get = (url, callback) => {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', url);
  xhr.send();

  xhr.onload = () => {
    if (xhr.status === 200) {
      // 서버의 응답을 콜백 함수에 전달하면서 호출하여 응답에 대한 후속 처리를 한다.
      callback(JSON.parse(xhr.response));
    } else {
      console.error(`${xhr.status} ${xhr.statusText}`);
    }
  };
};

const url = '<https://jsonplaceholder.typicode.com>';

// id가 1인 post의 userId를 취득
get(`${url}/posts/1`, ({ userId }) => {
  console.log(userId); // 1
  // post의 userId를 사용하여 user 정보를 취득
  get(`${url}/users/${userId}`, userInfo => {
    console.log(userInfo); // {id: 1, name: "Leanne Graham", username: "Bret",...}
  });
});

2 ) 에러 처리의 한계

  • 비동기 처리를 위한 콜백 패턴의 문제점 중에서 가장 심각한 것은 에러 처리가 곤란하다는 것.

45-09

try {
  setTimeout(() => { throw new Error('Error!'); }, 1000);
} catch (e) {
  // 에러를 캐치하지 못한다
  console.error('캐치한 에러', e);
}
  • try … catch … finallly 문
try catch finally 문은 에러 처리를 구현하는 방법임. 먼저 try 코드 블록이 실행됨.
이때 try 코드 블록에 포함된 문 중에서 에러가 발생하면 해단 에러는 catch 문의 err 변수에 전달되고
catch 코드 블록이 실행됨. finally 코드 블록은 에러 발생과 상관없이 반드시 한 번 실행됨.
  • try에서 호출한 setTimeout 함수는 1초 후에 콜백 함수가 실행되도록 타이머를 설정하고, 이후 콜백 함수는 에러를 발생시킴.
  • 하지만 이 에러는 catch 코드 블록에서 캐치되지 않음. 그 이유를 알아보자.
  • setTimeout 함수의 콜백함수가 실행될 때 setTImeout 함수는 이미 콜 스택에서 제거된 상태임.
  • 이것은 setTimeout 함수의 콜백함수를 호출한 것이 setTimeout 함수가 아니라는 것을 의미.
  • setTimeout 함수의 콜백 함수의 호출자(caller)가 setTImeout 함수라면 콜 스택의 현재 실행중인 실행 컨텍스트가 콜백 함수의 실행컨텍스트일 때 실행중인 실행 컨텍스트의 하위 실행 컨텍스트가 setTImeout 함수여야 함.
  • 에러는 호출자(caller) 방향으로 전파됨.
  • 즉, 콜 스택의 아래 방향으로 전파됨.

프로미스의 생성

  • Promise 생성자 함수는 비동기 처리를 수행할 콜백함수를 인수로 전달받는데 이 콜백함수는 resolve와 reject 함수를 인수로 전달받음.

45-10

// 프로미스 생성
const promise = new Promise((resolve, reject) => {
  // Promise 함수의 콜백 함수 내부에서 비동기 처리를 수행한다.
  if (/* 비동기 처리 성공 */) {
    resolve('result');
  } else { /* 비동기 처리 실패 */
    reject('failure reason');
  }
});
  • 프로미스는 다음과 같이 현재 비동기 처리가 어떻게 진행되고 있는지를 나타내는 상태 정보를 가짐.

  • 생성된 직후의 프로미스는 기본적으로 pending 상태. 이후 비동기 처리가 수행되면 비동기 처리 결과에 따라 다음과 같이 프로미스의 상태가 변경됨.
    • 비동기 처리 성공 : resolve 함수를 호출해 프로미스를 fulfilled 상태로 변경
    • 비동기 처리 실패 : reject 함수를 호출해 프로미스를 rejected 상태로 변경
  • 이처럼 프로미스의 상태는 resolve 또는 reject 함수를 호출하는 것으로 결정됨.
  • fulfilled 또는 rejected 상태를 settled 상태라고 함.
  • 일단 settled 상태가 되면 더는 다른 상태로 변화할 수 없음.
  • 프로미스는 비동기 처리 결과도 상태로 가짐.
  • 프로미스는 비동기 처리 상태와 처리 결과를 관리하는 객체.

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

  • 프로미스의 비동기 처리 상태가 변화하면 이에 따른 후속 처리를 해야함.
  • 프로미스가 fulfilled 상태가 되면 처리 결과를 가지고 무언가를 해야하고, rejected 상태가 되면 에러 처리를 해야함.
  • 이를 위해 프로미스는 후속 메서드 then, catch, finally를 제공함.
  • 프로미스의 비동기 처리 상태가 변화하면 후속 처리 메서드에 인수로 전달한 콜백 함수가 선택적으로 호출됨.

1 ) then

  • 첫번째 콜백함수 : 비동기 처리가 성공했을 때 호출되는 성공 처리 콜백 함수
  • 두번째 콜백 함수 : 비동기 처리가 실패했을 때 호출되는 실패 처리 콜백 함수

2 ) catch

  • 프로미스가 rejected상태인 경우만 호출됨

3 ) finally

  • 프로미스의 성공 또는 실패와 상관없이 무조건 한 번 호출됨.
  • try, catch ,finally 메서드는 언제나 프로미스를 반환함

프로미스의 에러 처리

  • 모든 then메서드 호출 이후에 catch 호출하면 가독성 좋고 명확함.
  • 따라서 에러 처리는 then에서 하지 말고 catch에서 하는 것을 권장함.

프로미스 체이닝

  • then → catch → then 이런식으로 연속해서 쓸 수 있음.

45-24

const url = '<https://jsonplaceholder.typicode.com>';

// id가 1인 post의 userId를 취득
promiseGet(`${url}/posts/1`)
  // 취득한 post의 userId로 user 정보를 취득
  .then(({ userId }) => promiseGet(`${url}/users/${userId}`))
  .then(userInfo => console.log(userInfo))
  .catch(err => console.error(err));
  • 콜백 패턴은 가독성이 안좋음.
  • async/await 사용해서 동기처럼 처리.

프로미스의 정적 메서드

1 ) Promise.resolve / Promise.reject

  • 인수로 전달받은 값을 resolve / reject 하는 프로미스를 생성함.

45-26

// 배열을 resolve하는 프로미스를 생성
const resolvedPromise = Promise.resolve([1, 2, 3]);
resolvedPromise.then(console.log); // [1, 2, 3]

2 ) Promise.all

  • 여러 개의 비동기 처리를 모두 병렬 처리할 때 사용

45-30

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

// 세 개의 비동기 처리를 순차적으로 처리
const res = [];
requestData1()
  .then(data => {
    res.push(data);
    return requestData2();
  })
  .then(data => {
    res.push(data);
    return requestData3();
  })
  .then(data => {
    res.push(data);
    console.log(res); // [1, 2, 3] ⇒ 약 6초 소요
  })
  .catch(console.error);
  • 세 개의 비동기 처리를 순차적으로 처리함.
  • 앞의 비동기 처리가 완료하면 다음 비동기 처리를 수행함.
  • 하지만 위 예제의 경우에는 앞의 비동기 처리 결과를 다음 비동기 처리가 사용하지 않아서 순차적으로 처리할 필요가 없음.

45-31

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) // [ 1, 2, 3 ] ⇒ 약 3초 소요
  .catch(console.error);
  • 모든 프로미스가 fulfilled 상태가 되면 모든 처리 결과를 배열에 저장해 새로운 프로미스를 반환함.
  • 하나라도 rejected 상태가 되면 나머지 프로미스가 fulfilled 상태가 되는 것을 기다리지 않고 즉시 종료함.

45-32

Promise.all([
  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); // Error: Error 3

3 ) Promise.race

  • 가장 먼저 fulfilled 상태가 된 프로미스의 처리 결과를 resolve 하는 새로운 프로미스를 반환함.

45-35

Promise.race([
  new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
  new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
  new Promise(resolve => setTimeout(() => resolve(3), 1000)) // 3
])
  .then(console.log) // 3
  .catch(console.log);
  • 하나라도 rejected 상태가 되면 에러를 reject 하는 새로운 프로미스를 즉시 반환함.

4 ) Promise.allSettled

  • 전달받은 프로미스가 모두 settled 상태가 되면 처리 결과를 배열로 반환함.

45-37

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>:3:54}
]
*/

마이크로태스크 큐

  • 순서 예측

45-39

setTimeout(() => console.log(1), 0);

Promise.resolve()
  .then(() => console.log(2))
  .then(() => console.log(3));
  • 1 → 2 → 3 (X)
  • 2 → 3 → 1 (O)
  • 프로미스의 후속 처리 메서드의 콜백함수는 태스크 큐가 아니라 마이크로태스크 큐에 저장됨.
  • 마이크로태스크 큐는 태스크 큐보다 우선순위가 높음

fetch

  • HTTP 요청을 전송할 URL, HTTP 요청 메서드, HTTP 요청 헤더, 페이로드 등을 설정한 객체를 전달.
  • HTTP 응답을 나타내는 Response 객체를 래핑한 Promise 객체를 반환함.

45-40 GET 요청

fetch('<https://jsonplaceholder.typicode.com/todos/1>')
  .then(response => console.log(response));
  • Response에는 HTTP 응답 몸체를 위한 다양한 메서드를 제공함.
  • ex MIME 타입이 application/json인 HTTP 응답 몸체를 얻으려면 json 메서드를 사용함.
  • json 메서드는 Response 객체에서 HTTP 응답 몸체(response body)를 취득하여 역직렬화 함.

45-41

fetch('<https://jsonplaceholder.typicode.com/todos/1>')
  // response는 HTTP 응답을 나타내는 Response 객체이다.
  // json 메서드를 사용하여 Response 객체에서 HTTP 응답 몸체를 취득하여 역직렬화한다.
  .then(response => response.json())
  // json은 역직렬화된 HTTP 응답 몸체이다.
  .then(json => console.log(json));
  // {userId: 1, id: 1, title: "delectus aut autem", completed: false}
  • 에러 처리에 주의

45-42

const wrongUrl = '<https://jsonplaceholder.typicode.com/XXX/1>';

// 부적절한 URL이 지정되었기 때문에 404 Not Found 에러가 발생한다.
fetch(wrongUrl)
  .then(() => console.log('ok'))
  .catch(() => console.log('error'));
  • fetch 함수가 반환하는 프로미스는 기본적으로 404 Not Found나 500 Internal Server Error와 같은 HTTP 에러가 발생해도 에러를 reject하지 않고 불리언 타입의 ok 상태를 false로 설정한 Response 객체를 resolve 함.
  • 오프라인 등의 네트워크 장애나 CORS 에러에 의해 요청이 완료되지 못한 경우에만 프로미스를 reject 함.
  • 따라서 반환한 프로미스가 resolve한 불리언 타입의 ok상태를 확인해서 명시적으로 에러를 처리할 필요가 있음.

45-43

const wrongUrl = '<https://jsonplaceholder.typicode.com/XXX/1>';

// 부적절한 URL이 지정되었기 때문에 404 Not Found 에러가 발생한다.
fetch(wrongUrl)
  // response는 HTTP 응답을 나타내는 Response 객체다.
  .then(response => {
    if (!response.ok) throw new Error(response.statusText);
    return response.json();
  })
  .then(todo => console.log(todo))
  .catch(err => console.error(err));
  • 참고로 axios는 모든 HTTP 에러를 reject 하는 프로미스를 반환함.
  • fetch 함수를 통해 HTTP 요청을 전송해보자.
  • URL, HTTP 요청 메서드, HTTP 요청 헤더, 페이로드 등을 설정한 객체를 전달

45-44

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

1 ) GET 요청

45-45

request.get('<https://jsonplaceholder.typicode.com/todos/1>')
  .then(response => {
    if (!response.ok) throw new Error(response.statusText);
    return response.json();
  })
  .then(todos => console.log(todos))
  .catch(err => console.error(err));
// {userId: 1, id: 1, title: "delectus aut autem", completed: false}

2 ) POST 요청

45-46

request.post('<https://jsonplaceholder.typicode.com/todos>', {
  userId: 1,
  title: 'JavaScript',
  completed: false
}).then(response => {
    if (!response.ok) throw new Error(response.statusText);
    return response.json();
  })
  .then(todos => console.log(todos))
  .catch(err => console.error(err));
// {userId: 1, title: "JavaScript", completed: false, id: 201}

3 ) PATCH 요청

45-47

request.patch('<https://jsonplaceholder.typicode.com/todos/1>', {
  completed: true
}).then(response => {
    if (!response.ok) throw new Error(response.statusText);
    return response.json();
  })
  .then(todos => console.log(todos))
  .catch(err => console.error(err));
// {userId: 1, id: 1, title: "delectus aut autem", completed: true}

4 ) DELETE 요청

45-48

request.delete('<https://jsonplaceholder.typicode.com/todos/1>')
  .then(response => {
    if (!response.ok) throw new Error(response.statusText);
    return response.json();
  })
  .then(todos => console.log(todos))
  .catch(err => console.error(err));
// {}

댓글