JS 웹 개발 (3)

깨진알·2023년 12월 19일

JavaScript

목록 보기
19/21

비동기 실행과 Promise 객체(1)

1. fetch 함수와 비동기 실행

console.log("Start"); // 첫 번째로 동작

fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => response.text())
  .then((result) => { console.log(result); }); // 세 번째로 동작(fetch 함수에 비동기 실행이 되는 부분이 있기 때문)

console.log("End"); // 두 번째로 동작

then 메소드는 콜백을 단지 등록만 하는 기능을 한다. 이 후에 Server에 response가 도착하면 콜백들이 순서대로 동작하면서 마지막에 result가 나타나게 되는 것이다.


2. 동기 실행과 비동기 실행

console.log('Start!');

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

console.log('End'); 

위 코드에는 2개의 콜백이 있다.
(1) (response) => response.text()
(2) (result) => { console.log(result); }

fetch 함수가 request를 보내고, Server의 response를 받게 되면, 이 콜백들이 순서대로 실행된다. 전체 코드의 실행 순서는 다음과 같다.

  1. console.log("Start");
  2. fetch 함수 (request 보내기 및 콜백 등록)
  3. console.log("End");
  4. response가 오면 2번에서 then 메소드로 등록해뒀던 콜백 실행

이렇게 특정 작업을 시작(request 보내기)하고 완벽하게 다 처리(response를 받아서 처리)하기 전에, 실행 흐름이 바로 다음 코드로 넘어가고, 나중에 콜백이 실행되는 것을 '비동기 실행'이라고 한다. 이에 반해 한번 시작한 작업은 다 처리하고 나서야, 다음 코드로 넘어가는, 약간 더 익숙한 방식의 실행은 '동기 실행'이라고 한다. 만약 동기 실행되는 함수였으면 다음과 같이 진행되었을 것이다.

  1. console.log("Start");
  2. fetch 함수 (request 보내기)
  3. response가 올 때까지 코드 실행이 잠시 '정지'되고, response가 오면 필요한 처리 수행
  4. console.log("End");

'비동기 실행'이 존재하는 이유는 보통 '비동기 실행'이 '동기 실행'에 비해, 동일한 작업을 더 빠른 시간 내에 처리할 수 있기 때문이다. 방금 전과 같이 fetch 함수가 실행되고 response가 올 때까지 기다린다는 것은 의미가 없다. response가 올 때까지는 아무런 작업을 하지 않게 되므로 시간을 낭비하게 된다.

하지만 비동기 실행이라면 일단 request 보내기, 콜백 등록까지만 해두고, 바로 다음 작업을 시작함으로써 시간을 절약할 수 있다.

2-1. 알아야하는 비동기 실행 함수들

(1) setTimeout 함수

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

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

setTimeout 함수는 첫 번째 파라미터에 있는 () => { console.log('b'); } 이 콜백의 실행을, 두 번째 파라미터에 적힌 2000 밀리세컨즈(=2초) 뒤로 미룬다.

fetch 함수에서는 콜백이 실행되는 조건이, 'response가 도착했을 때'라면, setTimeout에서 콜백이 실행되는 조건은, '설정한 밀리세컨즈만큼의 시간이 경과했을 때'이다. 둘 다 콜백의 실행을 나중으로 미룬다는 점에서 비슷하다.

(2) setInterval 함수

특정 콜백을 일정한 시간 간격으로 실행하도록 등록하는 함수이다. Interval은 '간격'이라는 뜻을 갖고 있다.

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

콜백이 2초 간격으로 계속 실행된다. (숫자 5는 다섯 번 출력되었다는 뜻이다.)

(3) addEventListener 메소드

addEventListener 메소드는 DOM 객체의 메소드이다. 만약 사용자가 웹 페이지에서 어떤 버튼 등을 클릭했을 때, 실행하고 싶은 함수가 있다고 가정하자.
1. 해당 DOM 객체의 onclick 속성에 그 함수를 설정할 수 있다.
2. 해당 DOM 객체의 addEventListener 메소드의 파라미터로 전달하면 된다.

(1) onclick 속성

...

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

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

...

(2)

...

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

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

... 

이렇게 클릭과 같은 특정 이벤트가 발생했을 때 실행할 콜백을 등록하는 addEventListener 메소드도 비동기 실행과 관련이 있다. 파라미터로 전달된 콜백이 당장 실행되는 것이 아니라, 나중에 특정 조건(클릭 이벤트 발생)이 만족될 때(마다) 실행되기 때문이다.

fetch함수와 위의 함수들의 차이제 대해 알아보자.

setTimeout(콜백, 시간) 
setInterval(콜백, 시간)
addEventListener(이벤트 이름, 콜백)
fetch('https://www.google.com')
  .then((response) => response.text()) // fetch 함수가 리턴하는 객체의 then 메소드를 사용해서 콜백을 등록
  .then((result) => { console.log(result); });

fetch 함수는 콜백을 파라미터로 바로 전달받는게 아니라, fetch 함수가 리턴하는 어떤 객체의 then 메소드를 사용해서 콜백을 등록한다. fetch 함수의 사용하는 형식이 다른 이유는 좀 더 새로운 방식으로 비동기 실행을 지원하는 자바스크립트 문법과 연관이 있기 때문이다. fetch 함수는 Promise 객체라는 것을 리턴하고, 이 Promise 객체는 비동기 실행을 지원하는 또 다른 종류의 문법에 해당하기 때문이다.


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

Promise 객체는 어떤 작업에 관한 '상태 정보'를 의미한다.

console.log('Start!');

// 1. pending 상태(진행중)
// 2. response를 잘 받으면 fulfilled 상태(성공)
// 3. 만약 request를 보내지 못하거나 response를 받지 못하는 상태가 되면 rejected 상태가 된다.(실패)
fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => response.text())
  .then((result) => { console.log(result); });

console.log('End');

만약 pending 상태에서 fulfilled 상태가 되었을 때는 Promise 객체가 '작업 성공 결과'(첫 번째 콜백 파라미터=response)도 갖게 된다. 반대로 rejected 상태에서는 Promise 객체가 '작업 실패 정보'를 갖게 된다.

3-1. fetch 함수를 사용한 코드, 다시 해석하기

console.log('Start!');

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

console.log('End');

then 메소드는 사실 Promise 객체의 메소드이다. Promise 객체가 pending 상태에서 fulfilled 상태가 될 때 실행할 콜백을 등록하는 메소드이다.


4. Promise Chaining

console.log('Start!');

fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => response.text())
  .then((result) => {
    const users = JSON.parse(result);
  	return users[0];
  })
  .then((user) => {
    console.log(user);
    const { address } = user;
    return address;
  })
  .then((address) => {
    console.log(address);
    const { geo } = address;
    return geo;
  })
  .then((geo) => {
    console.log(geo);
    const { lat } = geo;
    return lat;
  })
  .then((lat) => {
    console.log(lat);
  });
  
console.log('End');

Promise 객체에 여러 then 메소드를 붙이는 것을 Promise Chanining이라고 한다. Promise Chaining을 사용하는 이유는 then 메소드가 새로운 Promise 객체를 리턴하기 때문이다.



4-1. text, json 메소드도 Promise 객체를 리턴한다.

(1) text 메소드

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

(2) json 메소드

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


5. Promise Chaining이 필요한 경우

console.log('Start');

// 위의 코드 간략하게 작성
fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => response.text())
  .then((result) => {
    const users = JSON.parse(result);
    const user = users[0];
    console.log(user);
    const { address } = user;
    console.log(address);
    const { geo } = address;
    console.log(geo);
    const { lat } = geo;
    console.log(lat);
  });

console.log('End');

Promise Chaining은 비동기 작업을 순차적으로 실행할 때 전체 코드를 깔끔하게 작성하기 위해 사용된다.

fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => response.text())
  .then((result) => {
    const users = JSON.parse(result);
    const { id } = users[0];
    return fetch('https://jsonplaceholder.typicode.com/posts?userId=${id}');
  })
  .then((response) => response.text())
  .then((posts) => {
    console.log(posts);
  });

6. rejected 상태가 되면 실행할 콜백

fetch('https://jsonplaceholder.typicode.com/users')
  // .then((fulfilled), (rejected))
  .then((response) => response.text(), (error) => { console.log(error); })
  .then((result) => { console.log(result); });

7. then 메소드 완벽하게 이해하기

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

fetch('https://jsonplaceholder.typicode.com/users') // Promise-A
  .then(successCallback, errorCallback); // Promise-B

(1) fetch 메소드가 리턴하는 Promise 객체를 Promise-A 객체라고 하고,
(2) then 메소드가 리턴하는 Promise 객체를 Promise-B 객체라고 해보자.

  1. fetch 함수의 작업이 성공해서 Promise-A 객체가 fulfilled 상태가 된 경우 : then 메소드 안의 "첫 번째" 콜백인 successCallback이 실행된다.
  2. fetch 함수의 작업이 실패해서 Promise-A 객체가 rejected 상태가 된 경우 : then 메소드 안의 "두 번째" 콜백인 errorCallback이 실행된다.

Promise-B는, 실행된 successCallback 또는 errorCallback에서 무엇을 리턴하는냐에 따라서

  • 그 상태(fullfilled 또는 rejected)와
  • 결과(작업 성공 결과 or 작업 실패 정보)가 결정된다.

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

successCallback이 실행되든, errorCallback이 실행되든, 실행된 콜백에서 어떤 값을 리턴하는 경우이다. 크게 2가지 경우로 나눌 수 있다.

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

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

(response) => response.json() 콜백은 Promise 객체를 리턴하는 코드이다. 이렇게 콜백에서 Promise 객체를 리턴하는 경우에는 그 콜백을 등록한 then 메소드가 리턴했던 Promise 객체가 콜백이 리턴한 Promise 객체의 상태와 결과를 똑같이 따라 갖게 된다. 즉, 위 코드의 첫 번째 then 메소드가 리턴했던 Promise 객체는, response 객체의 json 메소드가 리턴한 Promsie 객체가 추후에 갖게 되는 상태와 결과를 그대로 따라서 갖게 된다는 뜻이다.

쉽게 기억하기 위해서 그냥 콜백에서 리턴하는 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) });

인터넷이 안 되는 상황에서 위의 코드를 실행해보자. fetch 함수의 작업이 실패해서 두 번째 콜백인 (error) => 'Try again!'이 실행된다. 두 번째 콜백은 '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 함수만 실행하고 끝난다고 가정해보자. 그럼 결과적으로 이 콜백은 아무런 값도 리턴하지 않은 것과 같다. 자바스크립트에서는 함수가 아무것도 리턴하지 않으면 undefined를 리턴한 것으로 간주한다. 따라서 then 메소드가 리턴했던 Promise 객체는 fulfilled 상태가 되고, 그 작업 성공 결과로 undefined를 갖게 된다.

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

// 정의하지 않은 함수 사용
fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => { 
        ...
        add(1, 2); // ReferenceError 발생
        ... 
  });

// 인위적으로 throw문 사용
fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => { 
        ...
        throw new Error('failed'); 
        ... 
  });

이러한 경우에는 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) }); 

then 메소드의 아무런 콜백도 실행되지 않는 경우가 있다. 인터넷을 끊고 위의 코드를 실행한다고 가정해보자. fetch 함수가 리턴한 Promise-1 객체는 rejected 상태가 되기 때문에, 첫 번째 then 메소드의 두 번째 콜백이 실행되어야 한다. 하지만 보면 두 번째 콜백이 없다. 이런 경우에 then 메소드가 리턴했던 Promise-2 객체는, 이전 Promise 객체와 동일한 상태와 결과를 갖게 된다. 즉, Promise-2 객체는 Promise-1 객체처럼 rejected 상태가 되고, 똑같은 작업 실패 정보를 갖게 된다.

그럼 rejected 상태가 된 Promise-2의 then 메소드에는 이제 두 번째 콜백이 존재하기 때문에 그 두 번째 콜백이 실행된다. 이렇게 아무런 콜백도 실행되지 않은 경우에는 그 이전 Promise 객체의 상태와 결과가 그대로 이어진다.

profile
프론트엔드 지식으로 가득찰 때까지

0개의 댓글