[JS] 비동기, Promise, Fetch API, async/await

JongHoon Son·2022년 10월 10일
0

JS

목록 보기
9/9
post-thumbnail

자바스크립트 엔진은 싱글스레드 기반으로 동작한다.

싱글스레드, 직역하면 스레드가 한 개라는 것이다. 그렇다면 특정 언어가 싱글스레드를 기반으로 동작한다는 것은 무엇을 의미할까? 싱글스레드로 동작하는 언어는 코드를 해석&동작하는 과정에서 단 한 명의 사람이 관여한다고 생각할 수 있다. 즉 코드에 작성된 여러가지 일(작업)을 한 명의 사람(싱글스레드)이 한 번에 한 개의 작업을 순서대로 처리하는 것을 의미한다. 이러한 싱글스레드 기반의 언어의 제어 흐름을 '동기적 제어 흐름'이라고 한다.


동기적 제어 흐름

위에서 언급한대로 자바스크립트 엔진은 싱글스레드를 기반으로 동작하며, 싱글스레드이기 때문에 동기적으로 동작한다. 동기적으로 동작한다는 것은 위에서 언급했다시피 한 번에 단 한 개의 작업을 순서대로 처리하는 것을 의미한다.

따라서 동기적 제어 흐름의 특징은 다음과 같다.

  • 동시에 여러 작업을 수행할 수 없다. 따라서 현재 실행중인 동기 작업이 끝날 때까지 대기한다.
  • 코드가 작성된 순서대로 한 줄씩 처리하므로, 자바스크립트 엔진 관점에서 수행 흐름을 예측하기가 쉽다.

비동기적 제어 흐름

우리는 프로그램을 개발하는 과정에서 가끔 서버와 통신하여 데이터를 주고 받는 등의 기능을 구현해야할 때가 있다. 서버와 통신한다는 것은, 서버에 무언가를 요청(Request) 하고, 서버의 응답(Response) 를 받을 때까지는 요청에 대한 작업이 진행중(Pending) 상태라는 것이다. 만약 이러한 과정을 동기적으로 처리하게되면 서버와 통신을 시도하는 와중에는 다른 작업을 처리할 수 없기 때문에 서버에 작업을 요청하고, 응답을 도착할 때까지는 코드 실행이 멈추게 된다. 만약 시간이 오래걸리는 작업이라서 서버로부터 작업의 결과를 늦게 받게된다면, 기다리는 데에만 매우 많은 시간이 소비되기에 매우 비효율적이라고 볼 수 있다. 따라서 기본적으로 서버와의 통신처럼 시간이 많이 걸리는 작업은 동기적으로 처리하지 않고, 비동기적 제어 흐름으로 처리하는 것이 더 효율적이다. 따라서 자바스크립트는 동기적으로 동작하는 자바스크립트 엔진의 메인스레드가 코드를 한 줄씩 실행하다가 비동기적으로 처리해야하는 코드를 만나면, 자바스크립트 엔진이 외부 작업장에 비동기 작업의 처리를 위임하고, 바로 다음 코드부터 이어서 처리한다. 결국 이런 식으로 서버와의 응답 등이 필요한 오래 걸리는 작업을 비동기적으로 처리하게 되면 메인스레드의 코드 처리를 방해하지 않을 수 있다.

이처럼 오래걸리는 작업을 외부에서 따로 작업을 처리한다는 것이 주된 목적인 비동기적 제어 흐름의 특징은 다음과 같다.

  • 동시에 여러 작업을 수행할 수 있다.
  • 자바스크립트 엔진 관점에서 흐름을 예측하기 어렵다. (비동기적으로 수행중인 작업이 언제 끝나는지 확신할 수 없다.)

Promise 개념

자바스크립트에서는 각 비동기 작업의 단위를 관리하기 위해 Promise 객체를 사용한다. Promise 객체는 비동기적 작업을 외부에 위임하고, 외부로부터 해당 작업이 끝나는 대로 결과를 돌려받는다. Promise, 직역하면 '약속'이다. 즉, 자바스크립트의Promise 객체는 비동기 작업을 수행해주는 곳(서버 등)과 다음과 같은 약속을 한다. '서버야 이 작업이 완료되면 나에게 알려줘!', 이처럼 자바스크립트의 Promise 객체는 외부에 비동기 작업을 위임하고, 그 결과를 기다리고, 결과가 나오면 특정 작업을 수행하기 때문에 자바스크립트 엔진 입장에서 Promise자바스크립트 엔진이 고용한 비동기 작업을 처리하는 직원 이라고도 생각할 수 있다.


Promise 객체

const promise1 = new Promise((resolve, reject) => {
  // 비동기 작업
});

비동기 작업의 요청 및 응답의 후처리를 담당하는 Promise 객체는 new Promise로 생성할 수있다.
이때 인수로 전달하는 resolvereject는 각각 비동기 작업의 응답 결과에 따라 호출되는 콜백함수이다. Promise는 외부로부터 비동기 작업의 결과를 받아서 성공했다고 판단할 시에는 resolve(param)를, 실패했다고 판단할 시에는 reject(param)를 호출한다. 이때 성공/실패의 판단 기준은 각 Promise가 직접 정의한다.

예를 들어, 비동기로 가져온 숫자 데이터 'Num' 에 대해

3 이상이면 성공 => resolve
3 미만이면 실패 => reject

할 수도 있으며, 추가적으로 가져온 데이터 'Num'이 숫자 형식이 아닐 경우 reject를 수행하는 등
갖가지 성공/실패의 경우를 모두 Promise가 정의, 결정한다.


Promise 상태

Pending : 비동기 작업 진행 중인 상태
Fulfilled, Resolved : 비동기 작업 성공
Rejected : 비동기 작업 실패
Settled : 비동기 작업 종료


Promise의 then, catch, finally 메서드

promise1
	.then(data => {
  		// Promise가 resolve 메서드를 호출하면 동작하는 메서드, resolve의 인자를 받음
    	console.log("성공 : ", data);
    })
    .catch(e => {
  		// Promise가 reject 메서드를 호출하면 동작하는 메서드, reject의 인자를 받음
    	console.log("실패 : ", e);
    })
    .finally(() +> {
  		// Promise가 resolve 또는 reject 메서드를 호출하면 동작하는 메서드
    	console.log("promise 종료");
    })
  1. then 메서드는 Promise가 성공했을 때(resolve를 호출했을 때) 수행할 콜백함수를 갖고 있으며, Promiseresolve의 인자에 넣은 값을 넘겨 받는다.
  1. catch 메서드는 Promise가 실패했을 때(reject를 호출했을 때) 수행할 콜백함수를 갖고 있으며, Promisereject의 인자에 넣은 값을 넘겨 받는다.
  1. finally 메서드는 Promise가 성공 또는 실패했을 때 수행할 콜백함수를 갖고 있다.

Promise 체이닝

하나의 Promise에 여러 개의 then 메서드를 연속해서 사용하는 것을 Promise 체이닝이라고 한다. Promise로부터 받아온 resolve의 인자 값을 then으로 처리하고, 또 처리 결과를 다음 then으로 넘겨 처리하고, 처리 결과를 다음 then으로 넘겨 처리하는 과정을 반복한다고 보면 된다. 이때 주의할 것은, 각 과정에서 then 메서드가 return 하는 값은 Promise 객체여야 한다. 이전 then 메서드가 returnPromise 객체의 resolve에 쓰인 인자 값을 다음 then 메서드가 받아서 사용한다. 만약 return하는 값 A가 Promise 타입이 아닐 경우, A를 암묵적으로 Promise.resolve()에 감싸서 리턴하기 때문에, 다음 then 메서드에서 A를 받아서 사용할 수 있다.

new Promise((resolve, reject) => {
  resolve("Some datas");
})
  // => result1 : Promise가 resolve의 인자 값으로 사용한 값
  .then((result1) => {
    return result1 + " I've got"; 
  })
  // => result2 : 이전 then 메서드가 반환한 값 (이전 then 메서드에서 일반 문자열을 return 하였지만, Promise.resolve()에 감싸져서 왔기 때문에 읽을 수 있음)
  .then((result2) => {
    return new Promise((resolve, reject) => {
      resolve(result2 + "!!");
    });
  })
  // => result3 : 이전 then 메서드가 반환한 Promise가 resolve의 인자 값으로 사용한 값
  .then((result3) => console.log(result3));

// 출력 : Some datas I've got!!

Fetch API

Web API 중 하나인 Fetch APIHTTP를 통해 서버에 무언가를 요청할 때 사용하는 API로, 비동기적으로 동작한다. fetch를 이용해 서버에 POST 등의 HTTP Request를 보내면, 서버는 그 결과로 Promise 객체를 돌려준다. 서버가 보낸 Promise 에는 resolve의 인수로 Response 객체가 들어있는데, 이 Response 객체에는 서버가 클라이언트의 요청에 대한 응답 정보가 들어있다. 따라서 fetch를 수행한 클라이언트는 이 Response 객체의 값을 이용해 본인이 원하는 작업을 수행한다.

const fetchedPromise = fetch(serverURL);  // fetch는 Promise를 리턴한다.

fetchedPromise
	.then(response => {
  		// fetch의 결과로 반환되는 Promise에는 resolve의 인자에 Response 객체가 들어있다.
  		if(response.ok) {        
          	// Response 객체는 json() 메서드를 사용하면 Promise 객체로 변환된다.
          	// 해당 Promise 객체의 resolve의 인자에는 서버가 보낸 데이터가 들어 있다.
  			const promise = response.json();
          	return promise;
        }
	}
    .then(data => { /*...*/ });
    .catch(error => {
    		// 요청 실패
        }
    }
    
// 여기서 fetchedPromise는 fetch API가 서버로부터 리턴 받은 Promise 객체이다.
           
// 서버가 Promise 객체를 준다는 것은, 해당 Promise 안에 특정 조건 하에서는 resolve 메서드,
// 특정 조건 하에서는 reject 메서드를 호출하겠다는 로직이 있다는 것이다.
           
// 따라서 서버가 준 Promise 객체에 then 메서드와 catch 메서드를 붙혀서 사용한다.
           
// 서버가 resolve 메서드를 호출할 시 매개변수로 전달하는 값은 Response 객체이므로
// json 메서드를 이용해 Promise로 변경하여 사용한다.
           
// 서버가 reject 메서드를 호출할 시 매개변수로 전달하는 값은 Error 객체이다.
    
    
// 즉 정리하면, fetch를 수행할 경우

// fetch 사용
// 			Promise 획득		=> (fetch의 리턴 값)
// then 사용
// 			Response 획득		=> (1번 Promise의 resolve의 인자에 들어가 있는 값)
// json 사용		
// 			Promise 획득		=> (2번 Response를 json()으로 변환하여 얻은 값)
// then 사용
// 			Object(data) 획득	=> (3번 Promise의 resolve의 인자에 들어가 있는 값)

// 순서로 결과를 정제해서 서버가 제공한 데이터를 취득할 수 있다.

async

보통 Promise는 다음과 같은 방식으로 많이 쓰인다.

const fetchUsers = () => {
  	// fetch로 부터 받은 Promise 객체를 리턴
	return fetch(serverURL)
    	.then(response => response.json())
        .then(data => data.results);
}

fetchUsers 함수는 serverURL로부터 유저 정보를 가져오는 함수로,
fetch를 통해 서버로부터 받아온 Promise 객체에 then 메서드를 붙혀서 리턴한다.
따라서 fetchUsers 함수의 역할을 한 줄로 정리하면, 'Promise를 리턴하는 함수'이다.

이렇게 단순히 Promise를 리턴하는 함수를 간단하게 정의하는 문법으로 async가 있다.
특정 함수를 정의 할 때 async를 붙이면 해당 함수는 async 함수가 되고, 해당 async 함수return 하는 것은 항상 Promise 객체이다. 만약 return하는 값 A가 Promise 타입이 아닐 경우, A를 암묵적으로 Promise.resolve()에 감싸서 리턴하기 때문에, 다음 then 메서드에서 A를 받아서 사용할 수 있다. (then 메서드와 동일)


async function fetchUsers() {
 	const promise = fetch(severURL)
    	.then(response => response.json());
        .then(data => data.results);
  
  	return promise;
}

// async 함수가 return 하는 값은 Promise 객체여야 한다.

await

await 문법은 async 함수 내에서 사용되는 것으로 async 함수 내에서 Promise를 사용할 경우 해당 Promise 앞에 await을 붙히면 Promise에서 처리하는 비동기 작업이 끝나고 결과가 리턴될 때까지 메인스레드는 모든 동기 작업을 멈추고 대기한다. 즉, await은 원래는 비동기로 처리되는 작업(fetch 등)을 동기적으로 처리하고자 할 때 사용되며, await을 앞에 붙인 Promise는 해당 Promise에서 마지막으로 호출된 resolve의 인자 값을 반환한다.

사실 async 함수 자체만으로는 크게 의미 있는 문법은 아니며, 비동기 작업을 동기적으로 처리하는 await 기능 때문에 async/await 이 하나로 묶여서 사용되는 편이다. async/await은 에러 처리로 try-catch 문을 사용한다.

const fetchUsers = () => {
	return fetch(serverURL)
    	.then(response => response.json())
        .then(data => data.results);
}

const fetchAnimals = () => {
	return fetch(serverURL)
    	.then(response => response.json())
        .then(data => data.results);
}


// async 함수
async function getDatas() {
  	try {
      	// fetch를 수행하는 함수의 모든 비동기 작업이 끝날 때까지 멈춤
      	// (비동기 작업을 동기적으로 처리함)
   		const users = await fetchUsers();		// fetchUsers 수행
      	const animals = await fetchAnimals();	// fetchUsers 수행이 종료되면 fetchAnimals 수행
      
      	// 최종적으로 users와 animals가 반환받는 값은
      	// 각 fetch 함수의 마지막 then 메서드가 return하는 값인 data.results 임        
    	return {users, animals};
    } catch (e) {
     	throw e; 
    }
}

getDatas()
	.then(data => {
		// 받아온 data로 수행하는 작업
	})
	.catch(console.log)

// async 함수인 getDatas 함수는 Promise를 반환하는
// fetchUsers 함수와 fetchAnimals를 호출한다.

// await을 이용해 fetchUsers와 fetchAnimals 함수 안에서 사용되는
// fetch의 비동기 작업이 모두 종료될 때까지 기다린다.
profile
FE 공부

0개의 댓글