자바스크립트 웹 개발 기본기 (2) (비동기 실행, Promise 객체)

LeeKyungwon·2024년 3월 30일
0

프론트엔드 

목록 보기
16/56
post-custom-banner

비동기 실행과 Promise 객체

fetch 함수와 비동기 실행

fetch함수에서 then 메소드는 콜백 함수를 등록하고 그 다음 줄 실행, 리스폰스가 도착한 후 콜백 함수 실행

console.log('Start!');

fetch('https://www.google.com')
  .then((response) => response.text())
  .then((result) => { console.log(result); });

console.log('End'); 

위 코드에는 두개의 콜백이 있다.
(1) (response) ⇒ response.text()
(2) (result) ⇒ { console.log(result); }
fetch 함수가 리퀘스트를 보내고 서버가 리스폰스를 받게 되면 콜백들이 순서대로 실행되는데 전체 실행 순서를 정리하자면

  1. console.log('Start');
  2. fetch 함수(리퀘스트 보내기 및 콜백 등록)
  3. console.log('End');
  4. 리스폰스가 오면 2. 에서 then 메소드로 등록해뒀던 콜백 실행

이렇게 된다.
이렇게 특정 작업을 시작(리퀘스트 보내기)하고 완벽하게 다 처리(리스폰스를 받아서 처리)하기 전에, 실행 흐름이 바로 다음 코드로 넘어가고, 나중에 콜백이 실행되는 것을 '비동기 실행'이라고 한다.

동기 실행은 한번 시작한 작업을 완료하기 전까지 코드의 실행 흐름이 다음 코드로 넘어가지 않는 것이다.

비동기 실행은 동기 실행에 비해 동일한 작업을 더 빠른 시간 내에 처리할 수 있다.

비동기 실행 함수들

1. setTimeout 함수

특정 함수의 실행을 원하는 시간만큼 뒤로 미루기 위해 사용하는 함수

console.log('a');
setTimeout(() => { console.log('b'); }, 2000);
console.log('c');

콜백의 실행을 2초 뒤로 미룬다.

a c b 순으로 출력

2. setInterval 함수

특정 콜백을 일정한 시간 간격으로 실행하도록 등록하는 함수

console.log('a');
setInterval(() => { console.log('b'); }, 2000);
console.log('c');

a c b 순으로 출력 후 2초 간격으로 b가 반복 출력

3. addEventListener 메소드

(1) onclick 속성

btn.onclick = function (e) { // 해당 이벤트 객체가 파라미터 e로 넘어옵니다.
  console.log('Hello Codeit!');
};

// 또는 arrow function 형식으로 이렇게 나타낼 수도 있습니다. 
btn.onclick = (e) => {
  console.log('Hello Codeit!');
};

(2) addEventListener 메소드

btn.addEventListener('click', function (e) { // 해당 이벤트 객체가 파라미터 e로 넘어옵니다.
  console.log('Hello Codeit!');
});

// 또는 arrow function 형식으로 이렇게 나타낼 수도 있습니다.
btn.addEventListener('click', (e) => {
  console.log('Hello Codeit!');
});

위 함수들은 함수의 아규먼트로 콜백을 넣는데, fetch 함수는 콜백을 파라미터로 전달받지 않고, fetch 함수가 리턴하는 어떤 객체의 then 메소드를 사용해서 콜백을 등록한다.
그 이유는 fetch 함수는 Promise 객체라는 것을 리턴하고, 이 Promise 객체는 비동기 실행을 지원하는 또 다른 종류의 문법에 해당하기 때문!

Promise 객체

fetch 함수는 promise 객체를 리턴한다.

Promise 객체 : 어떤 작업에 관한 상태 정보를 갖고 있는 객체
작업의 결과가 promise 객체에 저장된다.

상태
1. pending 진행 중
2. fulfilled 성공
3. rejected 실패

작업이 성공하면 promise 객체는 작업 성공 결과도 갖게된다.
아래 코드에서 response가 작업의 성공 결과에 해당
작업이 실패하면 작업 실패 정보를 갖게 됨

console.log('Start!');

fetch('https://www.google.com')
  .then((response) => response.text())
  .then((result) => { console.log(result); });

console.log('End'); 

then 메소드 : promise 객체의 메소드, pending->fulfilled 상태가 될 때 실행할 콜백 등록

서버로부터 리스폰스 -> 프로미스 객체 fulfilled 상태 -> then 메소드 콜백함수 실행

Promise Chaining

콜백이 순서대로 실행된다는 특징이 있다.

then 메소드 뒤에는 계속해서 then 메소드를 붙일 수 있다.
then 메소드가 새로운 promise 객체를 리턴한다.

then 메소드가 리턴한 promise 객체는 가장 처음에는 pending 상태이다.
콜백이 실행되고 콜백에서 어떤 값을 리턴하면 promise 객체가 영향을 받는다.

1. promise 객체를 리턴하는 경우

then 메소드에서 리턴한 promise 객체는 콜백이 리턴한 promise 객체와 동일한 상태와 결과를 갖게 된다. (동일한 상태, 작업 성공/실패 정보를 갖게 됨)

2. promise 객체가 아닌 값을 리턴하는 경우

콜백에서 숫자, 문자열, 일반 객체 같은 것을 리턴할 경우
then 메소드에서 리턴한 promise 객체는 fulfilled 상태가 되고,콜백의 리턴 값을 작업 성공 결과로 갖는다.

text, json 메소드

text, json 메소드도 Promise 객체를 리턴하는 메소드이다.

1. text 메소드

fetch 함수로 리스폰스를 잘 받으면, response 객체의 text 메소드는, fulfilled 상태이면서 리스폰스의 바디에 있는 내용을 string 타입으로 변환한 값을 '작업 성공 결과'로 가진 Promise 객체를 리턴한다.
작업 성공 결과는 string 타입인데, 그 값이 만약 JSON 데이터라면 이전에 배운 것처럼 JSON 객체의 parse 메소드로 Deserialize를 해줘야한다.(JSON.parse(result);)

2. json 메소드

fetch 함수로 리스폰스를 잘 받으면, response 객체의 json 메소드는, fulfilled 상태이면서, 리스폰스의 바디에 있는 JSON 데이터를 자바스크립트 객체로 Deserialize해서 생겨난 객체를 '작업 성공 결과'로 가진 Promise 객체를 리턴한다.
만약 리스폰스의 바디에 있는 내용이 JSON 타입이 아니라면 에러가 발생하고 Promise 객체는 rejected 상태가 되면서 그 '작업 실패 정보'를 갖게 된다.

Promise Chaining이 필요한 경우

비동기 작업을 순차적으로 수행해야 할 때 전체 코드를 깔끔하게 나타내기 위해 사용

rejected 상태가 되면 실행할 콜백

then 메소드의 두번째 파라미터로 원하는 콜백을 넣어주면 된다.

fetch('https://www.google.com')
  .then((response) => response.text(), (error) => {console.log(error);})
  .then((result) => { console.log(result); });

then 메소드

const successCallback = function () { };
const errorCallback = function () { };

fetch('https://jsonplaceholder.typicode.com/users') // Promise-A
  .then(successCallback, errorCallback); // Promise-B
  1. fetch 함수의 작업이 성공해서 Promise-A 객체가 fulfilled 상태가 된 경우 : then 메소드 안의 "첫 번째" 콜백인 successCallback이 실행
  2. fetch 함수의 작업이 실패해서 Promise-A 객체가 rejected 상태가 된 경우 : then 메소드 안의 "두 번째" 콜백인 errorCallback이 실행

여기서 중요한 점은 Promise-B는, 실행된 successCallback 또는 errorCallback에서 무엇을 리턴하느냐에 따라 상태와 결과가 결정된다는 점이다.

then 메소드가 리턴한 Promise 객체가, 콜백이 리턴하는 값에 따라 어떻게 달라지는지 경우를 나눠서 다뤄볼 것이다.

1. 실행된 콜백이 어떤 값을 리턴하는 경우

successCallback이 실행되든, errorCallback이 실행되든, 실행된 콜백에서 어떤 값을 리턴하는 경우

(1) Promise 객체를 리턴하는 경우

fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => response.json())
  .then((result) => { console.log(result) });

콜백에서 Promise 객체를 리턴하는 경우에는 그 콜백을 등록한 then 메소드가 리턴했던 Promise 객체가 콜백이 리턴한 Promise 객체의 상태와 결과를 똑같이 따라 갖게 된다.
=> 콜백에서 리턴하는 Promise 객체를 then 메소드가 그대로 리턴한다고 생각해도 됨. 그 다음부터는 콜백에서 리턴한 Promise 객체로부터 다시 Promise Chain이 쭉 이어져 나간다고 보면 된다.

(2) Promise 객체 이외의 값을 리턴하는 경우

then 메소드가 리턴했던 Promise 객체는 fulfilled 상태가 되고 작업 성공 결과로 그 값을 갖게 된다.

// Internet Disconnected

fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => response.json(), (error) => 'Try again!')
  .then((result) => { console.log(result) });

인터넷이 안 되는 상황에서 이 코드를 실행했을 때 두번째 콜백 (error) ⇒ 'Try again!이 실행되고 해당 콜백을 등록한 then 메소드가 리턴했던 Promise가 fulfilled 상태가 되고, 그 작업 성공 결과로 'Try again' 문자열을 갖게 된다.

2. 실행된 콜백이 아무 값도 리턴하지 않는 경우

// Internet Disconnected

fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => response.json(), (error) => { alert('Try again!'); })
  .then((result) => { console.log(result) });

위 코드 처럼 무언가를 리턴하는 게 아니라 단순히 alert 함수만 실행할 경우
1. (2) 규칙에 따라 then 메소드가 리턴했던 Promise 객체는 fulfilled 상태가 되고, 그 작업 성공 결과로 undefined를 갖게 된다.

3. 실행된 콜백 내부에서 에러가 발생했을 때

이 경우에는 Promise 객체가 rejected 상태가 되고, 작업 실패 정보로 해당 에러 객체를 갖게 된다.

4. 아무런 콜백도 실행되지 않을 때

// Internet Disconnected

fetch('https://www.google.com') // Promise-1
  .then((response) => response.text()) // Promise-2
  .then((result) => { console.log(result) }, (error) => { alert(error) }); 

Promise-1 객체는 rejected 상태가 되는데 두번째 콜백이 없기 때문에 아무런 콜백이 실행되지 않는다.
then 메소드가 리턴했던 Promise-2 객체는, 이전 Promise 객체와 동일한 상태와 결과를 갖게 된다.

catch 메소드

catch 메소드 : Promise 객체가 rejected 상태가 되면 실행할 메소드

fetch('https://www.google.com')
  .then((response) => response.text()) 
  .catch ((error) => { console.log(error);})
  .then((result) => { console.log(result) }

catch 메소드는 then 메소드로부터 나온 메소드이다.

  .catch ((error) => { console.log(error);})

이 코드는

  .then (undefined, (error) => { console.log(error);})

이 코드와 똑같은 의미를 가진다.

catch 메소드의 위치

에러가 catch 메소드 뒤에 나올 수도 있기 때문에 catch 메소드는 가장 마지막에 쓴다.

만약 중간에 에러가 발생해도 catch 메소드가 그 대안을 뒤로 넘겨줄 수 있으면 catch 메소드를 중간에 써도 된다.

fetch('https://friendbook.com/my/newsfeeds')
  .then((response) => response.json()) // -- A
  .then((result) => { // -- B
    const feeds = result;
    // 피드 데이터 가공...
    return processedFeeds; 
  })
  .catch((error) => { // -- C
    // 미리 저장해둔 일반 뉴스를 보여주기  
    const storedGeneralNews = getStoredGeneralNews();
    return storedGeneralNews;
  })
  .then((result) => { /* 화면에 표시 */ }) // -- D
  .catch((error) => { /* 에러 로깅 */ }); // -- E

비록 에러가 발생했다고 해도 만약 실패한 작업 대신 다른 방법을 통해서 작업을 정상적으로 끝마칠 수 있는 상황이라면 catch 메소드를 중간에 사용하기도한다.
getStoredGeneralNews 함수를 실행하는 것이 그 예

finally 메소드

어떤 작업이 성공하든 실패하든 상관없이 항상 실행하고 싶은 콜백이 있을 때

fetch('https://www.jsonplaceholder.typicode.com/users')
  .then((response) => response.text()) 
  .then((result) => { console.log(result); })
  .catch ((error) => { console.log(error);})
  .finally(() => { console.log('exit');});

Promise 객체의 등장 이유

Promise 객체가 없었다면 fetch 함수는

fetch('https://first.com', callback)

이렇게 직접 파라미터에 콜백을 전달하는 형식으로 정의되었을 것이다.
하지만 프로미스 객체를 사용한 이유는, 바로 함수에 콜백을 직접 넣는 형식은 콜백 헬(callback hell)이라고 하는 문제를 일으킬 수도 있기 때문이다.

fetch('https://first.com', (response) => {
  // Do Something
  fetch('https://second.com', (response) => {
    // Do Something
    fetch('https;//third.com', (response) => {
      // Do Something
      fetch('https;//fourth.com', (response) => {
        // Do Something
      });
    });
  });
});

여러 비동기 작업을 순차적으로 수행해야 할 때, 위 코드와 같이 가독성이 떨어진다.

fetch('https://first.com')
  .then((response) => {
    // Do Something 
    return fetch('https://second.com');
  })
  .then((response) => {
    // Do Something 
    return fetch('https://third.com');
  })
  .then((response) => { 
    // Do Something 
    return fetch('https://third.com');
  });

프로미스 객체를 리턴하는 방식은 이런 식으로 Promise Chaining을 해서 좀 더 깔끔한 코드로 여러 비동기 작업을 순차적으로 처리할 수 있다.
이렇게 Promise 객체를 사용하면 callback hell 문제를 해결할 수 있다.

직접 Promise 객체 생성하기

const p = new Promise((resolve, reject)=>{
  //setTimeout(()=>{resolve('success')); },2000);
  setTimeout(()=>{reject(new Error('fail')); },2000);
});

exector 함수

전통적인 형식의 비동기 실행 함수를 사용하는 코드를, Promise 기반의 코드로 변환하기 위해 Promise 객체를 직접 만드는 경우가 많다.

이미 상태가 결정된 Promise 객체 만들기

(1) fulfilled 상태의 Promise 객체 만들기

const p = Promise.resolve('success');

Promise의 resolve라는 메소드를 사용하면 바로 fulfilled 상태의 Promise 객체를 만들 수 있다. 위와 같이 쓰면 fulfilled 상태이면서, 작업 성공 결과로 문자열 'success'를 가진 Promise 객체를 만들 수 있다.

(2) rejected 상태의 Promise 객체 만들기

const p = Promise.reject(new Error('fail'));

Promise의 reject라는 메소드를 사용하면 바로 rejected 상태의 Promise 객체를 만들 수 있다. 위와 같이 쓰면 rejected 상태이면서, 작업 실패 정보로, fail이라는 메시지를 가진 Error 객체를 가진 Promise 객체를 만들 수 있다.

Promise 객체를 직접 생성하는 방법에는 이전에 배웠던 것처럼
new 생성자와 executor 함수를 사용하는 것 말고도 resolve 메소드나, reject 메소드를 사용하는 방법도 있다.
resolve 메소드나 reject 메소드로 생성한 Promise 객체도 이때까지 우리가 배운 것과 동일하게 작동한다.

const p = Promise.resolve('success');
p.then((result) => { console.log(result); }, (error) => { console.log(error); });

이 코드에서는 첫 번째 콜백이 실행되어서 작업 성공 결과인 문자열 success가 출력되고

const p = Promise.reject(new Error('fail'));
p.then((result) => { console.log(result); }, (error) => { console.log(error); });

이 코드에서는 두 번째 콜백이 실행되어서 작업 실패 정보인 Error 객체의 내용이 출력된다.

어떤 비동기 작업을 처리할 필요가 있다면, new 생성자와 executor 함수를 사용해서 Promise 객체를 만들어야 하지만, 그렇지 않고 바로 상태가 이미 결정된 Promise 객체를 만들고 싶을 때는 이 resolve 또는 reject 메소드를 사용한다.

함수 안에서 리턴하는 값이 여러 개인 경우 모든 리턴값을 Promise 객체로 통일하고 싶을 때, 종종 resolve 또는 reject 메소드를 쓴다.

function doSomething(a, b) {
    //~~
  if (problem) {
    throw new Error('Failed due to..'));
  } else {
    return fetch('https://~');
  }
}

위 코드는 문제가 발생하는 경우에 바로 Error 객체를 throw해 버리는데 문제가 존재하는 경우에도 Promise 객체를 리턴하고 싶다면 reject 메소드를 써서 작성할 수 있다.

function doSomething(a, b) {
  // ~~
  if (problem) {
    return Promise.reject(new Error('Failed due to..'));
  } else {
    return fetch('https://~');
  }
}

Promise 객체의 작업 성공 결과 또는 작업 실패 정보

Promise 객체의 상태가 fulfilled 또는 rejected 상태이기만 하면, 어느 시점이든, 몇 번이든 then 메소드를 붙여서 해당 결과를 가져올 수 있다.

const p = new Promise((resolve, reject) => {
  setTimeout(() => { resolve('success'); }, 2000); // 2초 후에 fulfilled 상태가 됨
});

p.then((result) => { console.log(result); }); // Promise 객체가 pending 상태일 때 콜백 등록
setTimeout(() => { p.then((result) => { console.log(result); }); }, 5000); // Promise 객체가 fulfilled 상태가 되고 나서 콜백 등록 

이 코드를 실행하면 Promise가 pending 상태일 때 등록한 콜백이든, fulfilled 상태가 된 후에 등록한 콜백이든 잘 실행된다.
어느 시점이든, 몇 번의 then 메소드를 붙이든 상관없이, pending 상태만 아니라면 항상 then 메소드로 Promise 객체의 결과를 추출할 수 있다.

여러 개의 Promise 객체를 다루는 방법

all 메소드

// 1번 직원 정보
const p1 = fetch('https://learn.codeit.kr/api/members/1').then((res) => res.json());
// 2번 직원 정보
const p2 = fetch('https://learn.codeit.kr/api/members/2').then((res) => res.json());
// 3번 직원 정보
const p3 = fetch('https://learn.codeit.kr/api/members/3').then((res) => res.json());

Promise
  .all([p1, p2, p3])
  .then((results) => {
    console.log(results); // Array : [1번 직원 정보, 2번 직원 정보, 3번 직원 정보]
  });

all 메소드는 이렇게 아규먼트로 들어온 배열 안에 있는 모든 Promise 객체가 pending 상태에서 fulfilled 상태가 될 때까지 기다린다.

all 메소드가 리턴한 Promise 객체는 각 개별 Promise 객체의 작업 성공 결과로 이루어진 배열을 자신의 작업 성공 결과로 갖는다.
이렇게 all 메소드는 여러 Promise 객체의 작업 성공 결과를 기다렸다가 모두 한 번에 취합하기 위해서 사용한다.

만약 하나라도 rejected 상태가 되면 all 메소드가 리턴한 Promise 객체는 rejected 상태가 되고 동일한 작업 실패 정보를 갖게 된다.

이러한 상황에 대비하려면 catch 메소드를 붙여주면 된다.

// 1번 직원 정보
const p1 = fetch('https://learn.codeit.kr/api/members/1').then((res) => res.json());
// 2번 직원 정보
const p2 = fetch('https://learn.codeit.kr/api/members/2').then((res) => res.json());
// 3번 직원 정보
const p3 = fetch('https://learnnnnnn.codeit.kr/api/members/3').then((res) => res.json());

Promise
  .all([p1, p2, p3])
  .then((results) => {
    console.log(results); // Array : [1번 직원 정보, 2번 직원 정보, 3번 직원 정보]
  })
  .catch((error) => {
    console.log(error);
  });

race 메소드

race 메소드도 all 메소드와 마찬가지로 여러 Promise 객체들이 있는 배열을 아규먼트로 받고 Promise 객체를 리턴하는데 그 적용 원리가 다르다.

race 메소드가 리턴한 Promise 객체는 아규먼트로 들어온 배열의 여러 Promise 객체들 중에서 가장 먼저 fulfilled 상태 또는 rejected 상태가 된 Promise 객체와 동일한 상태와 결과를 갖게된다.

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve('Success'), 1000);
});
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error('fail')), 2000);
});
const p3 = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error('fail2')), 4000);
});

Promise
  .race([p1, p2, p3])
  .then((result) => {
    console.log(result); // hello 출력
  })
  .catch((value) => {
    console.log(value);
  });

p1 객체는 1초 후에 fulfilled 상태가 되고, 그 작업 성공 결과로 문자열 Success를 가지게 되고 p2는 2초 후에, p3는 4초 후에 rejected 상태가 된다.

race 메소드가 리턴한 Promise 객체는 이 중에서 가장 빨리 상태 정보가 결정된 p1 객체와 동일한 상태와 결과를 가진다.

말그대로 race 메소드는 여러 Promise 객체들을 레이스(race, 경쟁)시켜서 가장 빨리 상태가 결정된 Promise 객체를 선택하는 메소드이다.

axios

fetch 함수 말고 Ajax 통신을 하는 방법이다. (외부 패키지)

axios
  .get('https://jsonplaceholder.typicode.com/users')
  .then((response) => {
    console.log(response);
  })
  .catch((error) => {
    console.log(error);
  });

axios 객체에서 리퀘스트를 보내는 많은 메소드들은 fetch 함수처럼 Promise 객체를 리턴한다.

fetch 함수와 비슷하지만, axios 객체에는 fetch 함수에는 없는 다음과 같은 몇 가지 기능 및 장점들이 있습니다.

  • 모든 리퀘스트, 리스폰스에 대한 공통 설정 및 공통된 전처리 함수 삽입 가능
  • serialization, deserialization을 자동으로 수행
  • 특정 리퀘스트에 대해 얼마나 오랫동안 리스폰스가 오지 않으면 리퀘스트를 취소할지 설정 가능(request timeout)
  • 업로드 시 진행 상태 정보를 얻을 수 있음
  • 리퀘스트 취소 기능 지원

하지만 별도의 패키지를 다운로드해야 하기 때문에 axios에서 제공하는 추가 기능이 필요한 경우에는 axios를 쓰고, 그런 기능이 필요하지 않고 별도의 패키지 다운로드를 원하지 않는 경우에는 fetch 함수를 사용한다.

post-custom-banner

0개의 댓글