Skip to content

week06 yoochul1

YOOCHULKIM edited this page Mar 26, 2023 · 2 revisions

프로미스

  • Promise 등장배경
    • 콜백함수 문제점
      • 콜백 헬
      • Error catch 불가

문제점1: 콜백 패턴

  • 예상 햇던 것과 다르게 동작, (아래 예시 response 가 undefined)
  • setTimeout의 콜백이 비동기로 동작하는것도 비슷
  • get 함수의 onload 이벤트 핸들러는 비동기로 동작
// 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}`);
  };
};

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

// 예상값
/*
{
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere ...",
  "body": "quia et suscipit ..."
}
*/
let g = 0;

// 비동기 함수인 setTimeout 함수는 콜백 함수의 처리 결과를 외부로 반환하거나
// 상위 스코프의 변수에 할당하지 못한다.
setTimeout(() => { g = 100; }, 0);
console.log(g); // 0

이를 해결하기 위해서는 onload 내부에서 콜백으로 후속 처리를 해야된다

그러나...

콜백 헬

get('/step1', a => {
  get(`/step2/${a}`, b => {
    get(`/step3/${b}`, c => {
      get(`/step4/${c}`, d => {
        console.log(d);
      });
    });
  });
});
// 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: error 처리

아래 예시는 에러를 캐치하지 못한다. 이유는, setTimeout은 비동기함수를 콜백함수가 호출되기를 기다리지 않고 바로 콜스택에서 제거되고, 콜백 함수만 남았기 때문이다. 쉽게 말해, catch는 setTimeout과 함께 간다. 따라서, setTimeout의 콜백함수가 던진 에러는 catch되지 않는다.

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

Promise 등장

  • ES6에 표준 빌트인 객체로 등장
  • 콜백함수의 2개 인수
    • resolve
    • reject
  • 비동기 처리 status
    • pending / fulfilled / rejected
  • 프로미스는 비동기 처리 상태와 결과를 관리하는 객체로 볼 수 있다.
  • 상태가 변할때, 후속처리 메서드에 인수로 전달된 콜백 함수가 선택적으로 호출된다.
    • 모든 후속처리 메서드는 promise를 반환

resolve

fulfilled된 프로미스

image-20230324093859862

reject

image-20230324093916691

앞선 만든 get을 다시 구현한다면,

// 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 함수 호출
        resolve(JSON.parse(xhr.response));
      } else {
        // 에러 시 reject 함수 호출
        reject(new Error(xhr.status));
      }
    };
  });
};

const url = 'https://jsonplaceholder.typicode.com';
promiseGet(`${url}/posts/1`)
  .then(res => console.log(res))
  .catch(err => console.error(err))
  .finally(() => console.log('Bye!'));

프로미스 여러번 체이닝으로 사용시

// id가 1인 post의 userId를 취득
// then 은 promise 를 계속 반환 한다.
// 콜밸 헬 제거!!
promiseGet(`${url}/posts/1`)
  .then(({ userId }) => promiseGet(`${url}/users/${userId}`))
  .then(userInfo => console.log(userInfo))
  .catch(err => console.error(err));

async / await

다만, 프로미스도 콜백함수를 사용해야 될 수 있다. 콜백 패턴은 가독성이 좋지 않으므로, ES8 에서 나온 async / await 을 사용하자. 그러면, 프로미스의 후속처리 메서드 없이 동기 처럼 처리 결과를 반환하도록 구현할 수 있다.

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

(async () => {
  // id가 1인 post의 userId를 취득
  const { userId } = await promiseGet(`${url}/posts/1`);

  // 취득한 post의 userId로 user 정보를 취득
  const userInfo = await promiseGet(`${url}/users/${userId}`);

  console.log(userInfo);
})();

프로미스의 5가지 정적 메서드

  • Promise.resolve
  • Promise.reject
  • Promise.all
  • Promise.race
  • Promise.allSettled

resolve / reject

// 배열을 resolve하는 프로미스를 생성
const resolvedPromise = Promise.resolve([1, 2, 3]);
resolvedPromise.then(console.log); // [1, 2, 3]
// ------------------ 생성자 함수 처리시 ------------------
const resolvedPromise = new Promise(resolve => resolve([1, 2, 3]));
resolvedPromise.then(console.log); // [1, 2, 3]
// 에러 객체를 reject하는 프로미스를 생성
const rejectedPromise = Promise.reject(new Error('Error!'));
rejectedPromise.catch(console.log); // Error: Error!
// ------------------ 생성자 함수 처리시 ------------------
const rejectedPromise = new Promise((_, reject) => reject(new Error('Error!')));
rejectedPromise.catch(console.log); // Error: Error!

all

  • 병렬로 처리
  • 모든 프로미스 상태가 fullfilled 될때 처리
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);
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);
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

참고

프로미스가 아닌 경우 Promise.resolve 로 래핑

Promise.all([
1, // => Promise.resolve(1)
2, // => Promise.resolve(2)
3  // => Promise.resolve(3)
])
.then(console.log) // [1, 2, 3]
.catch(console.log);

깃헙 사용자 이름 취득 이거 왜 있음?? repo 가져오는걸로 못하나? 아님 위 json 사이트에서 가져와야할듯

// 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 함수를 호출한다.
        resolve(JSON.parse(xhr.response));
      } else {
        // 에러 처리를 위해 reject 함수를 호출한다.
        reject(new Error(xhr.status));
      }
    };
  });
};

const githubIds = ['jeresig', 'ahejlsberg', 'ungmo2'];

Promise.all(githubIds.map(id => promiseGet(`https://api.github.com/users/${id}`)))
  // [Promise, Promise, Promise] => Promise [userInfo, userInfo, userInfo]
  .then(users => users.map(user => user.name))
  // [userInfo, userInfo, userInfo] => Promise ['John Resig', 'Anders Hejlsberg', 'Ungmo Lee']
  .then(console.log)
  .catch(console.error);

race

  • 가장 먼저 처리된 프로미스를 반환한다. (즉, 먼저 fulfilled 된다 )
  • 제일 먼저 에러가 발생할 경우 rejected 된 프로미스를 먼저 반환 (catch 사용한다면~)
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);
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); // Error: Error 3

allSettled

  • ES11 에서 도입
  • 모든 settled 된 즉, fulfilled 나 rejected 상태가 되면 다음 실행
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}
]
*/

Microtask Queue

  • 프로미스의 후속 처리 메서드는 Task Queue 가 아니라 Microtask Queue 에 저장된다.
    • 프로미스 이외의 비동기 함수의 콜백함수나 이벤트 핸들러는 Task Queue 저장
  • 우선순위
    • Microtask Queue > Task Queue

따라서 아래는 2 -> 3 -> 1 순으로 출력

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

Promise.resolve()
  .then(() => console.log(2))
  .then(() => console.log(3));

fetch

  • 클라이언트 사이드 Web API
  • XMLHttpRequest 보다 간단하고 프로미스 지원
fetch('https://jsonplaceholder.typicode.com/todos/1')
  .then(response => console.log(response));
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}

주의

아래는 catch 될 것처럼 보이지만, OK 가 출력된다.

fetch 는 404, 500 에러에도 reject 하지 않고, ok 상태를 false 로 설정한 응답 객체를 resolve 한다. (단, 네트워크 자애나 cors 에러의 경우 promise로 reject 한다.)

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

// 부적절한 URL이 지정되었기 때문에 404 Not Found 에러가 발생한다.
fetch(wrongUrl)
  .then(() => console.log('ok'))
  .catch(() => console.log('error'));

올바르게 고친다면,

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

get post patch delete

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

get

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}

post

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}

patch

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}

delete

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

Clone this wiki locally