📝

8. 비동기(콜백함수, 프로미스, await/async, fetch)

8-1. 비동기 처리

8-1-1. 비동기 이해

비동기적으로 실행된다는 것은 하나의 동작이 완료되지 않아도 다음 코드가 실행되는 것을 의미한다. 자바스크립트가 싱글 스레드를 가지면서도 비동기로 동작할 수 있는 원리는 바로 브라우저에 있다. 브라우저에 있는 Web API가 멀티스레드로 동작하기 때문이다.
💡
Thread 는 기본적으로 프로그램이 작업을 완료하는데 사용할 수 있는 단일 프로세스로, 각 스레드는 한 번에 하나의 작업만 수행할 수 있다.
알잘딱깔센 JavaScript알잘딱깔센 JavaScript
알잘딱깔센 JavaScript
자바스크립트 실행 환경을 살펴보면 자바스크립트 엔진의 콜 스택에 실행될 함수가 쌓이게 되는데, 비동기로 실행될 때는 Web API를 호출하게 된다. Web API에서는 콜백 함수를 콜백 큐(테스크 큐)에 추가하게 되고 이벤트 루프는 콜백 큐와 콜 스택을 보면서 콜 스택이 비면 콜백 큐의 함수를 꺼내 콜 스택에 넣는다. 이벤트 루프와 콜백 큐가 있기 때문에 콜 스택이 하나여도 비동기 동작이 가능하다.

8-1-2. 비동기 처리의 필요성

비동기 특성은 서버에 요청을 보내고 결과가 돌아오지 않아도 다음 코드를 실행한다. 비동기 방식이 필요한 이유는 웹에서 서버에 데이터를 요청했을 때 요청이 완료되기 전까지 아무것도 실행되지 않는다면 화면이 멈춘 것처럼 보일 뿐 아니라 하나의 프로그램을 실행하는 데 많은 시간이 소요되기 때문이다.
하지만 실행 순서가 중요할 때도 있다. 예를 들면 다른 코드가 서버에 요청한 데이터 값을 이용해야 할 때는 데이터를 받아 온 후에 코드가 실행되는게 필요하다. 이럴때 사용하는 것이 비동기 처리이다.
 
let response = fetch('mySnack.jpg'); let snackImg = response.snackImg(); //서버에서 이미지를 가져오면 이미지 크기, 네트워크 환경에 따라 실행 불가한 경우 발생, 비동기 처리 필요

8-1-3. 비동기 처리의 종류

비동기 처리의 종류에는 Promise, await/async, fetch 등이 있다. Promise는 ES6에 도입되었으며, 응답에 관한 정보를 갖고 있는 객체로 then, catch를 통해 결과 값을 처리한다. await/async는 ES2017에서 도입되었고 Promise를 기반으로 하지만 then, catch를 사용하지 않고 try-catch를 사용한다. 마지막으로 fetch는 접근하고자 하는 url과 매개변수로 네트워크 요청을 보낼 수 있다. 지금부터 비동기 처리의 종류에 대해서 더 자세히 알아보자.

8-2. 콜백함수

자바스크립트는 함수의 매개변수로 어떤 자료형이 들어올지 걸러주는 문법이 없다. 따라서 null과 undefined를 제외한 모든 자료형을 전달할 수 있다.

8-2-1. 콜백함수 이해하기

함수도 하나의 object이므로 매개변수로 전달될 수도 어떤 함수에 의해 return 될 수도 있다. 이렇게 다른 함수에 인자로 전달되는 함수, 그 함수 자체를 콜백함수(callback function) 라고 한다. 매개변수로 넘겨진 함수는 넘겨 받고 이를 실행할 수 있도록 나중에 다시 불러야 하는데, 이 불러달라는 것에 착안하여 Call Back이라는 이름이 붙었다.
 
즉 함수 A의 호출에서 매개변수로 함수 B가 전달되어, 특정 이벤트가 발생했거나 특정 시점이 되면 다시 호출되는 함수 B를 콜백 함수라고 말한다.
 
function dessert(count, eat, good) { count < 3 ? eatDessert() : goodDessert(); } function eatDessert() { console.log('오늘 먹어야 할 간식을 먹어주세요'); } function goodDessert() { console.log('오늘 먹어야할 간식을 모두 먹었습니다'); } dessert(4, eatDessert, goodDessert);
 
dessert, eatDessert, goodDessert 3가지 함수를 선언하고 dessert 함수를 호출할 때 매개변수로 count에 숫자 값을, eatgood에 각각 eatDessert 함수와 goodDessert 함수를 전달했다. 여기서 eatDessertgoodDessert 함수가 콜백함수인 것이다. dessert 함수가 먼저 호출되고, 매개변수로 들어온 count의 값에 따라 eatDessertgoodDessert 함수 둘 중 하나가 나중에 호출된다.
 
위 코드에서는 count가 4이므로 goodDessert가 실행된다.
notion imagenotion image
 

8-2-2. 동기 / 비동기 처리

 
function time() { const 시작 = Date.now(); for (let k=0; k < 100000000; k++) { } const 끝 = Date.now(); console.log(끝 - 시작 + 'ms'); //61ms } time(); console.log('다음 작업');
 
Data.now 는 1970년 1월 1일 0시 0분 0초부터 현재까지 경과된 밀리 초를 Number형으로 반환한다. 위에서 time() 함수가 100,000,000번 루프를 돌고 이 작업이 얼마나 걸렸는지 알려준다.
 
notion imagenotion image
 
결과물을 보니 time() 함수가 호출되면 for문이 돌아갈 때 다른 작업은 처리하지 않고 오직 for문만 실행된다. 이렇게 순차적으로 처리하는 것을 동기적이라고 한다.
여기서 for문을 처리할 동안 다른 작업도 하고 싶다면 함수를 비동기 형태로 전환해주면 된다. 그러기 위해 setTimeout 이라는 함수를 사용해준다. setTimeout은 일정 시간 후에 특정 코드, 함수를 의도적으로 지연한 뒤 실행하고 싶을 때 사용하는 함수이다.
 
function time() { setTimeout(() => { const 시작 = Date.now(); for (let k=0; k < 100000000; k++) { } const 끝 = Date.now(); console.log(끝 - 시작 + 'ms'); }, 0); } console.log('시작'); time(); console.log('다음 작업');
 
setTimeout함수는 첫번째 파라미터에 호출될 콜백함수를 넣고 두번째 파라미터에 지연시간 delay time을 받아 지연 시간 뒤에 콜백함수가 실행된다. 지연 시간은 밀리세컨드 단위로 설정해야하며 1000은 1초, 10000은 10초를 의미한다.
여기서는 0을 넣었으므로 이 함수는 바로 실행이 된다. 이렇게 setTimeout함수를 사용하면 우리가 해 줄 작업이 백그라운드에서 수행되므로 기존의 코드를 막지 않고 동시에 작업할 수 있다.
 
notion imagenotion image
 
결과물을 보니, 작업이 시작되고 for 루프가 돌아가는 동안 다음 작업이 실행 되고, for 루프가 끝나고 몇 ms이 걸렸는지 나타난다.
만약 time 함수가 끝난 뒤 어떤 작업을 처리하고 싶다면 콜백 함수를 파라미터로 전달해주면 된다.
 
function time(callback) { setTimeout(() => { const 시작 = Date.now(); for (let k = 0; k < 100000000; k++) { } const 끝 = Date.now(); console.log(끝 - 시작 + 'ms'); callback(); }, 0); } console.log('시작'); time(() => { console.log('작업이 끝났습니다.'); }); console.log('다음 작업');
 
notion imagenotion image

8-2-3. 콜백함수를 활용하는 메서드

8-2-3-1. 콜백함수를 활용하는 함수: forEach()

콜백함수를 활용하는 가장 기본적인 함수 forEach() 메서드이다. 배열이 갖고 있는 함수로 배열 내부의 요소를 활용해 콜백 함수를 호출한다.
 
let numberList = [181, 161, 25, 44]; numberList.forEach(function (value, index, array) { console.log(value, index, array); console.log(`${index}번째의 값은 ${value}`); });
 
notion imagenotion image
 
하지만 콜백함수는 이름이 없는 익명함수를 사용하기 때문에 아래의 코드처럼 바꿔줄 수 있다.
 
let numberList = [181, 161, 25, 44]; numberList.forEach(x => { console.log(x * 2); }); //출력결과 //362 //322 //50 //88

8-2-3-2. 콜백함수를 활용하는 함수: map()

map() 메서드도 배열이 갖고 있는 함수이다. map() 메서드는 콜백 함수에서 리턴한 값들을 기반으로 새로운 배열을 만든다. 밑의 코드에서는 콜백 함수 내부에서 value 값에 2를 곱해주므로 모든 배열의 요소에 2를 곱한 새로운 배열을 만든다.
 
// 배열 선언 let numberList = [181, 161, 25, 44]; // 배열의 모든 값에 2를 곱한다. numberList = numberList.map(function (value, index, array) { return value * 2; }) // 출력한다. // 매개변수로 console.log 메서드 자체를 넘겨주었다. numberList.forEach(console.log);
notion imagenotion image
 

8-2-3-3. 콜백함수를 활용하는 함수: filter()

 
const numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8]; let evenNumbers = numbers.filter(function (value) { return value % 2 === 0; }); let oddNumbers = numbers.filter(function (value) { return value % 2 === 1; }); console.log(`원래 배열: ${numbers}`); console.log(`홀수만 추출: ${oddNumbers}`); console.log(evenNumbers);
notion imagenotion image
 
filter() 메서드는 콜백함수에서 리턴하는 값이 true인 것들만 모아서 새로운 배열을 만드는 함수이다. function(value, index, array) {} 형태의 콜백함수를 사용하는 것이 기본이지만, 여기서는 value 만 활용하므로 매개변수로 value만 넣어주었다.
forEach(), map()에서 콜백함수의 매개변수를 value, index, array로 3개를 모두 입력하였지만 일반적으로는 value만 또는 valueindex만 사용하는 경우가 대다수이다. 콜백함수의 매개변수를 모두 다 입력할 필요 없이 사용하려는 것의 위치만 순서에 맞춰 입력하면 된다.

8-2-4. 콜백지옥

콜백지옥은 콜백함수를 익명 함수로 전달하는 과정에서 또 다시 콜백 안에 함수 호출이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상이다. 이런 현상을 콜백 헬, 멸망의 피라미드라고도 부른다.
주로 이벤트 처리나 서버 통신과 같은 비동기적인 작업을 수행하기 위해 등장하였다. 하지만 콜백 함수 내에서 또 다른 비동기 작업이 필요할 경우 위와 같은 중첩이 생기면서 콜백지옥이 탄생한다. 콜백지옥은 어디서 어떤 식으로 연결되었는지 한눈에 파악하기 힘들고 로직을 한눈에 이해하기 어려워 에러 해결이나 유지보수가 어렵다. 즉 가독성이 떨어지고 코드 수정이 어렵다.
 
setTimeout( (name) => { let snackList = name; console.log(snackList); setTimeout( (name) => { snackList += ', ' + name; console.log(snackList); setTimeout( (name) => { snackList += ', ' + name; console.log(snackList); setTimeout( (name) => { snackList += ', ' + name; console.log(snackList); }, 500, '다쿠아즈'); }, 500,'초코롤'); }, 500, '마카롱'); }, 500, '파운드');
 
위 코드는 0.5초마다 스낵 목록을 수집하고 출력한다.
 
> 출력값 파운드 (0.5초) 파운드, 마카롱 (1.0초) 파운드, 마카롱, 초코롤(1.5초) 파운드, 마카롱, 초코롤, 다쿠아즈(2.0초)
 

콜백 지옥 탈출

기명함수 사용
익명 함수의 사용을 포기하고, 함수를 나눠 버리면 깔끔하다. 위의 예를 다음과 같이 고칠 수 있다.
 
let coffeeList = ''; const addPound = (name) => { coffeeList = name; console.log(coffeeList); setTimeout(addMacaron, 500, '마카롱'); }; const addMacaron = (name) => { coffeeList += ', ' + name; console.log(coffeeList); setTimeout(addChocoroll, 500, '초코롤'); }; const addChocoroll = (name) => { coffeeList += ', ' + name; console.log(coffeeList); setTimeout(addDacquoise, 500, '다쿠아즈'); }; const addDacquoise = (name) => { coffeeList += ', ' + name; console.log(coffeeList); }; setTimeout(addPound, 500, '파운드');
 
함수의 선언과 호출을 구분할 수 있다면 위에서 아래로 순서대로 읽는데 어려움이 없고 들여쓰기가 깊어지지 않아 콜백지옥보다는 가독성이 높다. 하지만 일회성 함수를 전부 변수에 할당하여 변수명을 다 지어줘야할 뿐만 아니라 거꾸로 거슬러 코드명을 일일이 따라다녀야한다.
또한 전 단계에서 정의된 변수 등을 다음 함수에서 사용할 수 없다는 점과 완전히 바깥쪽에 변수를 선언해 사용해야 한다는 점이 코드를 작성하는 데 있어 헷갈리고 비효율적이다.
이 사태를 해결하기 위해 콜백지옥을 탈출하는 해결책으로 ES6에 도입 된 PromiseGenerator, ES2017에 도입된 async/await를 통해서 해결할 수 있다.
 

8-3. 프로미스(Promise)

프로미스는 자바스크립트의 오브젝트로 앞서 얘기한 콜백함수 같은 비동기적인것을 수행할 때 콜백함수를 대신하여 유용하게 사용할 수 있다.
제주도 여행을 와서 유명한 맛집을 찾아간다고 가정해보자. 맛집에 도착했더니 이미 많은 사람들이 줄지어 기다리고 있어 상당히 긴 웨이팅 시간이 예상된다. 식당은 손님들의 웨이팅 불편함을 효율적으로 관리하기 위해 알리미어플을 제공한다. 손님들에게 전화번호를 입력받고 해당 손님의 입장순서가 되면 카카오톡이나 문자로 알림이 가는 구조이다.
이제 손님들은 복잡하게 줄을 서서 오랜시간을 기다리지 않아도 된다. 기다리는 동안 주변의 경치와 각종 소품샵들을 둘러볼 수 있는 여유가 생긴다.
 
이제 위 예시를 코드로 비유해 보자.
  • 제작 코드(producing code)는 원격에서 스크립트를 불러오는 것 같은 시간이 걸리는 일을 한다. 위의 비유에서는 '식당(맛집)’이 제작 코드에 해당한다.
  • 소비 코드(consuming code)는 '제작 코드’의 결과를 기다렸다가 이를 소비한다. 이때 소비 주체(함수)는 여럿이 될 수 있다. 위 비유에서 소비 코드는 '손님’이다.
  • 프로미스(promise) 는 '제작 코드’와 '소비 코드’를 연결해 주는 특별한 자바스크립트 객체이다. 위 비유에서 프라미스는 '알리미어플’이다. 프로미스는 시간이 얼마나 걸리든 상관없이 약속한 결과를 만들어 내는 '제작 코드’가 준비되었을 때, 모든 소비 코드가 결과를 사용할 수 있도록 해준다.
프로미스는 더욱 많은 기능이 있고 구조가 훨씬 복잡하지만 이 비유를 이용하여 쉽게 프로미스를 알아보도록 하자.

8-3-1. 프로미스의 특징

프로미스 객체는 아래와 같은 문법으로 만들 수 있다.
 
const promise = new Promise((resolve, reject) => { console.log("1번손님 들어오세요");//executor(제작코드,실행함수) = 맛집 });
promise 기본문법
콘솔실행 결과콘솔실행 결과
콘솔실행 결과
new Promise 에 전달되는 함수를 executor 이라고 한다.
  • executor의 인수 resolve와 reject는 자바스크립트에서 자체 제공하는 콜백 함수이다. 따라서 따로 만들 필요는 없지만 둘중 한가지는 반드시 호출해야 한다.
  • resolve는 일이 성공적으로 끝난 경우 value와 함께 호출
  • reject는 에러 발생 시 에러 객체를 나타내는 error와 함께 호출
 
정리하면 executor 함수는 자동으로 실행이 된다. 처리 성공 여부에 따라 resolve나 reject를 호출한다.
 
또한 프로미스의 상태는 3가지의 상태(state)와 결과(result)를 가진다.
  • pending(대기): 처리가 완료되지 않은 상태
  • fullfilled(이행): 성공적으로 처리가 완료된 상태
  • rejected(거부): 처리가 실패로 끝난 상태
 
알잘딱깔센 JavaScript알잘딱깔센 JavaScript
알잘딱깔센 JavaScript
 
조금 더 자세히 알아보도록 하자. setTimeout 을 이용해서 식당에서 대기 손님에게 알림을 드리는 것 처럼 시간의 딜레이를 걸어 보겠다.
 
const promise = new Promise((resolve, reject) => { // promise가 만들어지면 executor함수는 자동으로 실행된다. setTimeout(()=>{ resolve('입장해주세요'); }, 3000); }); // 3초후 일이 성공적으로 끝났다는 신호와 함께 result는'입장해주세요'가 된다.
 
알잘딱깔센 JavaScript알잘딱깔센 JavaScript
알잘딱깔센 JavaScript
 
반대로 reject 의 경우이다. reject 의 경우에는 Error라는 오브젝트를 통해서 값을 전달한다.
 
const promise = new Promise((resolve, reject) => { // 3초 뒤에 에러와 함께 실행이 종료되었다는 신호를 보낸다. setTimeout(()=>{ reject(new Error('error'); }, 3000); });
 
3초 후 reject 가 호출되면 프로미스의 상태가 rejected로 변한다.
 
알잘딱깔센 JavaScript알잘딱깔센 JavaScript
알잘딱깔센 JavaScript
 
프로미스는 성공 또는 실패만 해야한다. executor은 resolvereject 중 하나를 반드시 호출해야 한다. 이미 변경된 상태는 더 이상 변하지 않는다. 다시말해 처리가 완료된 프로미스에 resolve와 reject를 호출하면 무시된다.
 
const promise = new Promise(function(resolve, reject) { resolve("1번손님 입장해주세요"); reject(new Error("~")); // 무시된다 setTimeout(() => resolve("~")); // 무시된다 });
 
이렇게 executor에 의해 처리가 끝난 일은 결과 혹은 에러만 가질 수 있다.

8-3-2. 소비함수 메서드(method): then, catch, finally

new Promise에 전달되는 함수는 executor(실행자, 실행 함수)라고 부른다. executor는 new Promise가 만들어질 때 자동으로 실행되는데, 결과를 최종적으로 만들어내는 제작 코드를 포함한다. 위 비유에서 '맛집’이 바로 executor이다.
그렇다면 만들어진 제작 코드의 결과를 기다렸다가 이를 소비하는 소비코드에 대해서 알아보자. 프로미스는 앞서 ‘손님(소비코드)’들과 ‘맛집(제작코드)’을 이어주는 ‘알리미어플’과 같은 이어주는 역할을 한다고 했다. 소비코드는 다음과 같은 메서드를 이용해 ‘알리미어플’에 번호를 등록할 수 있다.

8-3-2-1. then

then을 이용해서 다음과 같은 코드를 작성해 보았다.
 
//제작코드(producing code) const promise = new Promise((resolve, reject) => { setTimeout(() => { resolve('입장해주세요'); }, 3000); }); //소비코드(consuming code) promise.then(value => { console.log(value); });
3초후에 '입장해주세요'라는 값이 실행된다.3초후에 '입장해주세요'라는 값이 실행된다.
3초후에 '입장해주세요'라는 값이 실행된다.
then은 프로미스가 정상적으로 잘 수행이 되어서 마지막에 최종적으로 resolve라는 콜백 함수를 통해서 전달한 값이 value의 파라미터로 전달되어서 들어오는 걸 볼 수 있다.
반대로 reject 를 쓰게 되었을 때의 경우이다.
 
//제작코드(producing code) const promise = new Promise((resolve, reject) => { setTimeout(() => { reject(new Error('손님의 입장순서가 아닙니다')); }, 3000); }); //소비코드(consuming code) promise.then(value => { console.log(value); });
 
실행시켜보면 다음과 같은 에러 메세지가 출력된다. Uncaught라는 에러가 발생한다 이 말은 우리가 then이라는 것을 이용해서 성공적인 케이스만 다뤘기 때문에 잡히지 않은 에러가 발생한 것이다.
 
코드 실행 결과코드 실행 결과
코드 실행 결과
 

8-3-2-2. catch

방금 전과 같은 에러가 발생한 경우만 다루고 싶을 때는 catch 라는 메서드를 사용한다.
 
//제작코드(producing code) const promise = new Promise((resolve, reject) => { setTimeout(() => { reject(new Error('손님의 입장순서가 아닙니다')); }, 3000) }); //소비코드(consuming code) promise .then(value => { console.log(value); }) .catch(error => { console.log(error); });
 
이제는 더 이상 에러가 발생하지 않고 받아온 에러가 콘솔 창에 실행되는 것을 확인할 수 있다.
코드 실행 결과코드 실행 결과
코드 실행 결과
💡
프로미스에 then을 호출하게 되면 then은 결국 똑같은 프로미스를 리턴하기 때문에 그 리턴된 프로미스에 catch를 다시 호출할 수 있다. 이를 프로미스 체이닝(chaining)이라고 한다.
 

8-3-2-3. finally

finally 는 성공이든 실패이든 상관없이 무조건 마지막에 호출되는 메서드이다.
 
//제작코드(producing code) const promise = new Promise((resolve, reject) => { setTimeout(() => { reject(new Error('손님의 입장순서가 아닙니다')); }, 3000) }); //소비코드(consuming code) promise .then(value => { console.log(value); }) .catch(error => { console.log(error); }) .finally(() => { console.log('-제주맛집'); });
위와 같이 성공,실패 여부와 관계없이 어떤 기능을 마지막으로 수행하고 싶을 때 사용한다.
코드 실행 결과코드 실행 결과
코드 실행 결과
 

8-4. async function (async/await)

8-4-1. async function이란?

8-4-1-1. 등장배경

async가 왜 새롭게 등장했는지 그 배경을 안다면 우리는 async를 왜 사용하는지 알 수 있고 또 적절한 때에 사용할 수 있을 것이다. 위에서 살펴본 바와 같이 프로미스는 비동기 작업이 많아져도 콜백함수처럼 코드의 깊이가 길어지지 않는다는 장점이 있지만 여전히 아래와 같은 문제점이 있다.
 
  1. 특정 조건에 따라 분기를 나누기가 어렵다.
  1. 어떤 부분에서 에러가 발생했는지 파악하기가 어렵다.
  1. 프로미스 체이닝도 반복되면 가독성이 떨어져 .then 지옥이 발생할 가능성이 있다.
 
이러한 문제를 해결하기 위해 등장한 것이 바로 async/await 이다.

8-4-1-2. 특징

async/await은 프로미스를 기반으로 동작하고 프로미스의 후속 처리 메서드인 then 없이도 비동기를 동기처럼 보이도록 구현하는 방식으로 ECMAScript 2017(ES8)에 새롭게 추가된 키워드이다.
새롭게 만들어진 문법이 아닌 기존의 프로미스 위에 간편한 API를 제공한 비동기 처리 패턴으로 ‘syntatic sugar’ 라고 불린다. 보다 간결한 코드 작성을 위해 등장한 문법인 만큼 프로미스 보다 가독성이 좋고 동기 프로그래밍을 작성하는 방식과 유사하여 로직이 밑으로 흐르는듯 매우 직관적으로 보여진다. 그럼 본격적으로 async 함수의 작성 방법과 특징에 대해서 알아보자.

8-4-2. async 함수의 사용

8-4-2-1. async 함수 선언식으로의 사용

async는 함수 앞에 위치하고 해당 함수 앞에 async 키워드를 붙여서 사용하면 끝이다! 매우 간결하다.
 
async function 함수이름() { return 결과 값; }
 
async가 붙은 함수는 항상 반환값을 resolved로 하는 프로미스를 반환한다. 반환값에 프로미스가 명시되지 않더라도 이는 암묵적으로 프로미스로 감싸진다. 따라서 아래의 두 코드는 결과값이 '1'인 프로미스가 반환되는 동일한 코드이다.
 
async function snack() { return 1; } snack().then(alert); // '1' 반환 function snack() { return Promise.resolve(1); } snack().then(alert); // '1' 반환
 

8-4-2-2. async 함수 표현식으로의 사용

async를 함수와 함께 사용하면 아래와 같이 fullfilled 된 프로미스가 반환되는 것을 확인할 수 있다.
 
let snack = async function() { return "cake!"; }; // 또는 화살표 함수처럼 사용이 가능하다. let snack = async () => { return "cake!"; };
 
notion imagenotion image
 
.then() 블록을 사용하면 반환된 값을 바로 호출하여 사용할 수 있다.
 
snack().then((value) => console.log(value)) // cake! // 아래와 같이 간단하게 줄여서 작성할 수 있다. snack().then(console.log) // cake!

8-4-3. async & await 의 사용

8-4-3-1. async & await 사용 방법 및 특징

asyncawait과 함께 사용할 때 더욱 빛을 발하는데, 비동기 처리가 되어야하는 메서드 앞에 await 키워드를 넣어서 사용한다. 비동기 처리 메서드는 반드시 프로미스 객체를 반환해야 의도한 대로 동작하며 일반적으로 프로미스를 반환하는 API 호출 함수가 await 의 대상이 되는 코드이다.
 
async function 함수이름() { await 비동기 처리 메서드 이름(); } //또는 아래와 같이 사용한다. const 함수이름 = async () ⇒ { await 비동기 처리 메서드 이름(); }
 
await 키워드는 아래와 같은 특징을 잘 고려해서 사용해야 한다.
  • await keyword는 반드시 async 함수 내부에서만 사용해야 하고 프로미스 앞에서 사용해야 한다. 만약 async 외부에서 사용하게되면 SyntaxError가 발생한다.
 
  • 아래의 그림에서 보이는 것 처럼 await 키워드는 프로미스가 settled 상태(비동기 처리가 수행된 상태)가 될 때까지 대기하다가 settled 상태가 되면, 프로미스의 처리 결과를(resolved) 반환한다. 만약 프로미스 객체가 거부(rejected)된다면 이 때 ‘예외'가 발생하게 된다. 이 예외를 처리하는 방법 대해서는 9장 ‘예외처리’에서 살펴보도록 하자.
 
알잘딱깔센 JavaScript알잘딱깔센 JavaScript
알잘딱깔센 JavaScript
 
그런데, 여기서 ‘프로미스의 수행 결과를 기다렸다가 작업을 처리한다면 동기와 비동기의 차이가 없지 않나?’라는 의문이 생길 것이다. 이 의문에 대한 해답을 찾기 위해 await 키워드를 만날 때 일어나는 실행 흐름에 대해 알아보자.

8-4-3-2. async & await 제어흐름의 이해

 
const snack = () => Promise.resolve('와, 맛있겠다!') // 4번 async function mySnack() { console.log('치즈 케이크 주세요!') // 3번 const res = await snack() // 4번 console.log(res) // 6번 } console.log('주문하시겠어요?') // 1번 mySnack(); // 2번 console.log('주문하신 케이크 나왔습니다!') // 5번
 
위의 코드를 실행하면 출력값은 아래의 순서로 나온다.
 
'주문하시겠어요?' '치즈 케이크 주세요!' '주문하신 케이크 나왔습니다!' '와, 맛있겠다!'
 
엔진이 첫 번째로 만난 console.log('주문하시겠어요?') 를 출력한 다음부터의 흐름을 보자.
 
알잘딱깔센 JavaScript알잘딱깔센 JavaScript
알잘딱깔센 JavaScript
 
  1. mySnack() 이라는 비동기 함수를 호출하고 mySnack() 의 body가 동작한다. console.log 를 호출하면서 '치즈 케이크 주세요!' 의 문자열이 출력되면 콜스택을 빠져나온다.
  1. mySnack() 함수의 body 두 번째 라인인 snack()이 콜스택 안으로 들어가고 resolve 된 프로미스를 리턴한다. 프로미스가 resolve되고 snack()이 값을 리턴할 때, 엔진은 await 키워드를 마주친다!
  1. 이 때, body의 실행이 멈추고 mySnack() 함수가 마이크로테스크 큐에서 대기하면 엔진은 비동기 함수에서 나와 console.log('주문하신 케이크 나왔습니다!') 를 출력한다.
  1. 콜스택에 더이상 실행할 작업이 남아있지 않게 되면 이벤트 루프는 마이크로테스크에 남은 작업이 있는지 확인한다. 이 때, mySnack()snack() 을 resolve 한 뒤에 자기의 차례를 기다리고 있었다는 것을 발견한다. mySnack() 은 다시 콜스택으로 들어가고 res 변수는 snack()이 리턴한 값인 ‘와, 맛있겠다!’를 전달받는다. 콘솔에 '와, 맛있겠다!'가 출력되고 콜스텍에서 빠져나온다.
  1. 모든 작업이 끝난다.
 
💡
microtask는 Event Loop에 존재하는 2가지 queue 중 하나로 아래의 표와 같이 각 queue에 해당하는 메서드대로 해당 queue에 추가된다.[1]
(macro)task
setTimeout | setInterval | setImmediate |
microtask
process.nextTick | Promise callback | queueMicrotask |
 
프로미스와 async/await 의 제어흐름을 조금 더 자세히 알고 싶다면, 위 출처의 블로그를 참고하길 바란다. 비동기 처리의 흐름을 시각화하여 매우 직관적으로 설명하고 있어 보다 도움이 될 것이다.
위의 흐름을 이해했다면 이제 예제를 통해 async&await을 더 깊이 알아보자.

8-4-3-3. 실용예제

1) setTimeOut API 를 이용한 코드를 살펴보자.
 
function cook(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function process() { console.log('스낵을 만드는 중입니다!'); await cook(3000); console.log('스낵이 완성되었어요!'); } process().then(() => { console.log('맛있게 드세요!'); });
 
이제 어떤 출력값이 나올지 예상이 되는가? 엔진은 await 을 만나 cook(ms) 함수로 이동 한다. 여기서 setTimeout 함수를 만나는데, 이 함수는 즉시 실행되고 리턴되기 때문에 3초의 대기가 발생한다. 때문에 3초의 대기 후 await 바로 다음의 console.log 인 '스낵이 완성되었어요!' 가 출력된다.
 
'스낵을 만드는 중입니다!' //3초 쉬고 '스낵이 완성되었어요!' '맛있게 드세요!'
 
조금 더 다양한 시간이 할당된 코드를 살펴보자.
 
function cook(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } const myCake = async () => { await cook(1000); return '케이크'; }; const myCoffee = async () => { await cook(500); return '커피'; }; const myCookie = async () => { await cook(5000); return '쿠키'; }; async function asyncProcess() { const cake = await myCake(); console.log(cake); const coffee = await myCoffee(); console.log(coffee); const cookie = await myCookie(); console.log(cookie); } asyncProcess();
케이크 // 1초 커피 // 0.5초 쿠키 // 5초
 
위 코드를 실행시키면 하나의 작업이 종료된 후 다음 작업을 시작하므로 myCake (1초)→ myCoffee(0.5초) → myCookie(5초) 순으로 차례대로 출력된다. 이 함수들을 asyncProcess 함수에서 연달아 호출하게 되면서 asyncProcess 함수는 무려 6.5초의 시간동안 실행된다.
 
2) 병렬실행을 위해 promise.all 을 사용한 코드를 살펴보자.
async/await는 프로미스 위에 만들어져 있기 때문에 Promise의 모든 기능을 사용할 수 있다. 위의 코드를 보면 순차적 처리가 필요하지 않은 상황이므로 Promise APIs의 promise.all 함수를 추가해 병렬실행 하는 편이 낫다.
Promise.all() 앞에 async 키워드를 사용하여 동기식 코드처럼 작성할 수 있는데, 인자로는 배열을 전달받는다. 이 배열에 있는 모든 프로미스가 성공일 경우에만(fulfilled) 모든 처리 결과를 배열에 저장해 새로운 프로미스를 반환해준다. 단, 등록한 프로미스 중 하나라도 실패하면 전부 실패 한 것으로 간주된다.
 
function cook(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } const myCake = async () => { await cook(1000); return '케이크'; }; const myCoffee = async () => { await cook(500); return '커피'; }; const myCookie = async () => { await cook(5000); return '쿠키'; }; async function promiseProcess() { const results = await Promise.all([myCake(), myCoffee(), myCookie()]); console.log(results); } promiseProcess();
['케이크', '커피', '쿠키']
 
병렬처리는 또 언제 사용할까? 바로 모든 비동기 처리가 완료된 후 수행해야만 하는 작업에 사용된다.
예를 들면, id 를 입력하면 랜덤의 시간을 반환하는 fetchTime(id) 함수가 있다고 생각해보자. 그리고 이 id에 1번부터 5번 데이터를 넣어주고 그 값의 평균 시간을 구하려고 할 때의 코드를 살펴보자.
 
function setTimeoutPromise(ms) { return new Promise((resolve, reject) => { setTimeout(() => resolve(), ms); }); } async function fetchTime(id) { await setTimeoutPromise(500); console.log(`${id} 님의 데이터가 준비되었어요!`); return Math.floor(Math.random() * 20) + 1; } async function asyncProcess() { let hours = []; for (let i = 1; i < 6; i++) { hours.push(fetchTime(i)); } let result = await Promise.all(hours); console.log( `평균 공복시간은? : ${ result.reduce((prevalue, currentvalue) => prevalue + currentvalue, 0) / result.length } 시간 입니다.` ); } asyncProcess();
1 님의 데이터가 준비되었어요! 2 님의 데이터가 준비되었어요! 3 님의 데이터가 준비되었어요! 4 님의 데이터가 준비되었어요! 5 님의 데이터가 준비되었어요! 평균 공복시간은? : 8.2 시간 입니다.
데이터의 정보를 요청하는 것은 비동기로 처리하지만 평균을 내는 작업은 모든 비동기 작업이 완료되어야 가능하다. 따라서 1번부터 5번까지의 데이터를 다 받아오고 나서야 평균 시간이 출력된다.
 
3) fetch API를 받아와서 사용하는 코드를 살펴보자.
async & await 은 여러 개의 비동기 처리 코드를 다룰 때 빛을 발한다. 아래는 사용자의 정보와 할 일 목록을 받아올 수 있는 HTTP 통신 코드이다. fetch() 함수는 다음 장에서 자세히 다루니 어떤 상황에 사용되는지만 살펴보면 된다. 사용자 정보를 호출하고 사용자의 아이디가 ‘7’ 이라면 할 일 정보를 호출한 뒤, 받아온 할 일 정보의 제목을 콘솔에 출력하도록 만들어 보자.
 
function fetchUserId() { const userUrl = `https://jsonplaceholder.typicode.com/users/7` return fetch(userUrl).then(function(response) { return response.json(); }); } function fetchUserTodo() { const todoUrl = `https://jsonplaceholder.typicode.com/todos/7`; return fetch(todoUrl).then(function(response) { return response.json(); }); } async function getTodoTitle() { const user = await fetchUserId(); console.log(user) // '7'번 사용자의 정보를 받아옴 if (user.id === 7) { const todo = await fetchUserTodo(); console.log(todo.title); } } getTodoTitle(); // illo expedita consequatur quia in 출력
 
사용자의 id가 ‘7’이므로 아래와 같이 ‘7’사용자의 할일 목록을 받아오게 된다. 그리고 할일 목록의 제목(title)인 illo expedita consequatur quia in 가 출력된다.
notion imagenotion image
 
💡
JSONPlaceholder(https://jsonplaceholder.typicode.com) 는 무료로 제공되는 REST API이다. 해당 사이트의 data를 불러오는 연습을 해보면 감이 올 것이다.
 
4) 비동기로 loop를 도는for await..of 를 사용해 보자.
loop를 돌 때 for나 forEach문을 많이 사용한다. 위의 JSONPlaceholder의 API를 사용해서 async/await 내부에 forEach로 loop를 돌려보자.
 
const usernames = ['1', '2', '3', '4']; usernames.forEach(async (usernames) => { const result = await fetch(`https://jsonplaceholder.typicode.com/users/${usernames}`); console.log(result.json()); }); console.log('user의 목록을 불러왔습니다!'); // forEach 반복문이 끝나기도 전에 'user의 목록을 불러왔습니다!'가 먼저 실행된다.
 
forEach는 모든 비동기 작업이 끝날 때까지 기다리지 않기 때문에 console.log가 먼저 출력되어 버린다.
이러한 문제는 for await..of 를 사용하면 해결할 수 있다. 기본 구문은 for of 와 동일하며 await 키워드 앞에 for를 붙여서 사용한다.
 
for await (variable of iterable) { statement }
 
위의 코드를 for await..of 를 사용하여 바꿔보면 아래와 같다.
 
const usernames = ['1', '2', '3', '4']; for await (let user of usernames) { const result = await fetch(`https://jsonplaceholder.typicode.com/users/${user}`); console.log(result.json()); } console.log('user의 목록을 불러왔습니다!'); // for 문을 다 돌고난 후 'user의 목록을 불러왔습니다!'가 출력된다!
 
async/await이 도입되었다고 해서 프로미스나 콜백을 사용하지 않아야 하는 것은 아니다. 프로미스의 도입 이후에도 가벼운 코드를 작성할 때에는 콜백이 유용하기에 여전히 콜백함수를 사용한다. 프로미스는 .catch() 을 통해 에러 핸들링이 가능하지만, async/await은 에러 핸들링 기능이 없어 try-catch 문을 활용해야 한다. 비동기를 처리할 때에는 각각의 장단점을 비교하여 적절한 때에 사용하길 바란다.

8-5. Fetch

Fetch는 비동기 네트워크 통신을 구현하기 위해 사용하는 Web API이다. Fetch에 대해서 본격적으로 알아보기 전에 비동기 네트워크 통신에 대해 알아보도록 하자.

8-5-1. Ajax

자바스크립트를 이용하여 브라우저가 서버에게 비동기적으로 데이터를 요청하고, 응답 받은 데이터를 동적으로 페이지 렌더링 하는 방식을 Ajax(Asynchronous Javascript and XML)라고 한다.
 
notion imagenotion image
 
여기서 ‘비동기식 통신’이란 요청에 대한 응답을 기다리지 않고 응답을 받을 동안 다른 일들을 진행하는 일 처리 방식을 말한다. 현재 우리는 브라우저와 서버 간의 비동기 통신을 통해서 효율적으로 데이터를 렌더링 할 수 있다.
그렇다면, Ajax 방식이 등장하기 이전에는 어떻게 데이터를 주고 받았을까? 이전에는 완전한 HTML 파일을 응답 받아 렌더링 하였다. 브라우저와 서버 간의 데이터 통신은 동기적으로 작동했다. 동기식 통신이란, 서버가 요청에 대한 응답을 줄 때 까지 다른 일을 하지 못하고 기다리면서 순차적으로 작업하는 방식이다.
정리하자면, Ajax이전의 방식은 다음과 같다.
  1. 서버의 응답을 받기 전에는 다른 작업 요청들을 블로킹한다.
  1. 변경할 필요가 있는 부분만 전송 받아서 렌더링 하는 것이 아니라, 서버로부터 전체 HTML을 받아 전체 페이지를 다시 렌더링 한다.
이러한 동기 통신 방식은 속도 저하와 화면 깜빡임 현상 등의 문제가 발생해 매끄러운 작동과는 거리가 멀었다.
Ajax의 등장은 위의 문제들을 말끔히 해결해주었다.
  1. 서버의 응답이 완료될 때까지 기다리지 않고 다른 작업들을 진행한다.
  1. 변경 사항이 있는 부분만 렌더링한다.
Ajax 프로그래밍이라는 것은 자바스크립트를 이용한 비동기 네트워크 통신 방식이다. Ajax 프로그래밍은 Web API를 기반으로 동작한다. 대표적인 Web API로는 XMLHttpRequest 객체, JQuery, fetch 등이 있다.
XMLHttpRequest는 1999년 마이크로소프트 사에서 개발되었지만 당시에는 큰 주목을 받지 못했다. 2005년 구글 사가 구글 맵스를 통해 자바스크립트로 Ajax 방식을 구사한 이후부터 본격적인 존재감이 드러나기 시작했다. 이는 당시 혁신적인 화면 전환 효과를 보여주었다.
하지만 브라우저가 발전하면서 XMLHttpRequest 객체를 사용하는 것에서도 브라우저 간의 버전 일치가 되지 않아 불편함이 생겼다. 이에 따라 Web API 또한 발전했다. 그 중에서도 ES6에서 지원하는 가장 모던한 Web API인 Fetch가 우리가 앞으로 살펴볼 이야기의 주인공이다.

8-5-2. fetch 함수란

💡
fetch함수는 클라이언트 사이드 Web API로, HTTP요청 전송 기능을 제공한다. Ajax를 지원하는 여러가지 기술 중 비교적 최신 기술이며, 기존 방식에 비해 더 유연하고 분명하다. - 출처 : 생활 코딩

8-5-2-1. fetch 함수의 기본 구조

fetch()의 기본 문법 구조는 다음과 같다.
 
const promise = fetch(resource, [options])
 
fetch()는 2개의 매개변수를 가진다.
  • 첫 번째 매개변수 (resource) : 필수 사항, HTTP 요청을 보낼 URL
  • 두 번째 매개변수 (options) : 선택 사항, method, headers, body 등
 
첫 번째 매개변수는 필수 사항이며 요청을 보낼 URL을 받는다. 두 번째 매개변수는 선택 사항이다. 만약 두 번째 매개변수를 입력하지 않는다면, default 값으로 GET메서드 데이터 요청이 전송된다.

8-5-2-2. fetch 함수 동작 과정

fetch함수는 프로미스 객체를 반환한다. 프로미스 객체에는 HTTP응답에 관한 정보가 있는 Response 객체가 들어있다. 프로미스 객체 반환과 함께 응답 헤더가 반환 되며, 응답 몸체는 별도로 프로미스기반의 메서드를 호출해야 접근할 수 있다.
응답 헤더 에서 status 속성 값을 통해 HTTP상태 코드를 확인 할 수 있다. 대표적인 상태코드는 200, 404가 있으며 더 상세한 상태코드는 아래 wiki에서 확인 가능하다.
  • status : 200 - 서버가 데이터를 찾음
  • status : 404 - 서버가 데이터를 못 찾음 (not found)
 
응답 몸체에 담긴 데이터는 사용자가 원하는 형태로 파싱하여 접근 할 수 있다. 여기에는 다양한 메서드들이 있다.
  • reponse.json() - JSON 형태로 반환
  • response.text() - 텍스트 형태로 반환
  • response.formData() - FormData 객체 형태로 반환
  • response.blob() - 이진 데이터로 반환(주로 이미지, 오디오 등 멀티미디어 오브젝트를 다룰 때 사용)
  • response.arrayBuffer() - ArrayBuffer(바이트로 구성된 배열) 형태로 반환
fetch 함수의 동작은 응답 헤더 확인하고, 원하는 데이터를 추출하는 단계로 나눌 수 있다. 예시를 통해서 쉽게 이해 해보자.
 
fetch('https://jsonplaceholder.typicode.com/users/3')
 
id가 3인 user의 이메일 정보를 찾는다고 가정해보자. 여기서 url의 구조는 해당 페이지의 api 규율을 따른다. 예제에서 사용한 사이트(https://jsonplaceholder.typicode.com)의 api 규율에 따르면 id가 3인 user에 대한 정보는 url/users/id값 로 찾을 수 있다.
 
1) 응답 헤더 확인
여기서 반환 된 Response 객체 안에는 HTTP 응답 헤더가 들어있다. 응답 헤더가 들어있는 프로미스를 반환받아 어떻게 처리해야할지 작성해야 한다. Status 속성 값에 200이 있다면 서버가 성공적으로 데이터를 찾았음을 알 수 있다. 만약 찾지 못했다면 404 를 반환한다.
 
Response 응답 헤더 값Response 응답 헤더 값
Response 응답 헤더 값
응답 상태 값이 200이므로 서버가 id가 3인 user데이터 찾기에 성공했음을 알 수 있다.
 
2) 원하는 데이터 추출
3번 user의 데이터를 찾았다면, 이메일 주소 정보를 가져와야 할 타이밍이다. 우리가 찾고자 하는 정보는 응답 몸체에 있다. 응답 몸체에 담긴 정보는 그대로 가져올 수 없다. 사용자가 원하는 형태로 변환하여 가져와야 하는데, 여기서 response 메서드를 사용하여 가져온다. 다양한 메서드 중에서 json 메서드를 사용하여 정보를 받아보도록 하자.
 
fetch('https://jsonplaceholder.typicode.com/users/3') .then(response => response.json())
프로미스의 then 메서드를 사용해서 response의 응답이 오면 응답 몸체를 JSON형태로 파싱하도록 한다.
 
Response 응답 몸체 값Response 응답 몸체 값
Response 응답 몸체 값
객체로 역직렬화된 user 데이터가 반환 되는 것을 알 수 있다. 여기서 이메일 주소 데이터를 출력하려면 어떻게 해야 할까?
 
fetch('https://jsonplaceholder.typicode.com/users/3') .then(response => response.json()) .then(json => console.log(json.email)) //Nathan@yenseina.net
 
위의 코드는 프로미스만을 사용하여 작성한 코드이다. 이 코드는 async/await을 사용하여 작성할 수도 있다.
 
async function getUserEmail(id){ const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`); const user = await(response.json()); const email = user.email; console.log(email) } getUserEmail(3) //Nathan@yesenia.net
 
getUserEmail 함수는 매개변수로 id값을 받는다. 인자 값으로 3을 넘겨주었으므로 id가 3인 user의 데이터가 있는 url로 요청을 보낸다.
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
 
여기서 fetch는 프로미스를 반환하고 await은 피로미스가 resolve한 값인 Response객체를 반환한다. 즉, 변수 response에는 응답 데이터가 들어있는 Response객체가 담긴 것이다.
const user = await(response.json()); JSON형태로 파싱된 응답 데이터 값이 변수 user에 저장되고 user.email로 객체의 속성에 접근하여 이메일 값을 변수 email에 저장하였다.
JSON형태 외에도 Response 메서드를 사용하여 여러가지 형태의 데이터를 반환할 수 있다. 데이터를 텍스트 형태로 반환하고 싶다면 response.text() 를 사용할 수 있다.
 
const txt = fetch('https://reqres.in/api/products/3') .then(response => response.text()) .then(text => console.log(text))
 
이번에는 response.blob()을 사용해서 이미지를 받아오는 코드를 만들어 보자.
 
async function getImg(){ const response = await fetch(`https://picsum.photos/200`); const blobImg = await(response.blob()); console.log(blobImg); } getImg() //Promise //Blob {size: 19055, type: 'image/jpeg'}
 
그렇다면 위의 결과 값을 가지고 무얼 할 수 있을까? 이미지를 받아왔으니, 스크린에 출력하는 코드를 짜보도록 하자.
 
async function printImg(){ const response = await fetch(`https://picsum.photos/200`); const blobImg= await(response.blob()); //blob을 담을 img 태그를 만든다. const img = document.createElement('img'); //html body에 위에서 만든 img 태그를 삽입한다. document.body.append(img); //img 태그의 주소를 설정한다. img.src = URL.createObjectURL(blobImg); } printImg()
 
URL.createObjectURL 메소드는 Blob객체의 URL을 DOMString으로 변환해준다. Blob 객체로 받아온 데이터를 url로 활용하기 위해서는 URL.createObjectURL 을 다음과 같이 사용하여 url로 변환해주면 된다.
이와 같이 fetch 함수를 사용할 때 데이터를 다양한 형태로 반환하여 활용할 수 있다.

8-5-2-3. pollyfill

Fetch API는 가장 유연하고 잘 쓰이지만 최신에 추가된 Web API 이기 때문에 일부 구형 브라우저에서는 지원되지 않는다.
 
caniuse.com/fetch 2022-02-20. [2]caniuse.com/fetch 2022-02-20. [2]
 
위의 표에서 빨간 표시가 되어있는 IE, OPera Mini의 경우 fetch함수를 지원하지 않는다. 하지만 polyfill을 이용하면 해당 브라우저에서도 fetch를 활용할 수 있다.
 

8-5-3. fetch 함수를 통한 HTTP 요청

 
const promise = fetch(resource, [options])
 
fetch()의 두 번째 매개변수 인자 값에 요청 메서드를 전달하여 GET, POST, DELETE, PATCH, PUT등의 요청을 할 수 있다. options에 headers, methods, body 등의 정보를 객체 형태로 담아 전달해야 한다.
 
async function request() { const response = await fetch('url 기입', { method: "GET", //POST, DELETE, PATH, PUT headers: { "Content-type": "콘텐츠 형태", //application/json, text/plain 등 }, body: JSON.stringify( 서버에 전달할 데이터 ); }); const data = await response.json(); console.log(data); } request();
 
  1. GET 요청
    1. fetch()의 두 번째 매개변수를 생략하면 디폴트 값으로 GET 요청 메서드가 들어간다. GET 요청은 서버에서 데이터를 가져올 때 사용한다.
       
      // async/await 패턴 async function get() { const response = await fetch('url 기입', { method: "GET", }); const data = await response.json(); console.log(data); } get(); // Promise 패턴 function get() { fetch('url 기입',{ method: "GET", }) .then(response => response.json()) .then(json => console.log(json)); } get()
       
      GET 요청 방식을 이용하여 모든 user의 게시물들 목록을 출력하는 코드를 작성해보자.
       
      async function get() { const response = await fetch('https://jsonplaceholder.typicode.com/posts', { method: "GET", }); const data = await response.json(); console.log(data); } get(); /* [ { 'userId': 1, 'id': 1, 'title': ..., 'body': ... }, { 'userId': 1, 'id': 2, 'title': ..., 'body': ... }, ... { 'userId': 2, 'id': 1, 'title': ..., 'body': ... }, ... ] */
       
위 서비스를 통해 fetch get, post, delete, put 등을 테스트 할 수 있다.
get은 https://jsonplaceholder.typicode.com/posts 사이트에서 테스트가 되지만 다른 메서드는 지원하지 않는다. 앞으로 위 사이트(httpbin.org)를 테스트 사이트로 사용하도록 한다. 웹 브라우저의 콘솔창을 열고 아래코드를 입력해보자.
 
async function get() { const response = await fetch('https://httpbin.org/get?name=hojun', { method: 'GET', }); const data = await response.json(); console.log(data); } get();
 
notion imagenotion image
 
아래처럼 응답이 들어와 제대로 get 요청이 전송이 된 것을 확인할 수 있다. URL 구조는 WHATWG 문서를 참고 바란다.
 
  1. POST 요청
    1. POST 요청은 데이터 생성을 요청할 때 사용한다. 생성할 내용을 2번째 인자 값 중 body에 넣어서 전달해주면 된다.
      한 user당 하나의 게시글만 올릴 수 있는 서버가 있다고 하자. 이 서버의 3번 user의 게시물에 새로운 게시글을 추가하려고 한다. 새로운 게시글 콘텐츠는 이러하다. title은 ‘오늘의 간식’이고, 내용은 ‘고소미’이다.
       
      async function post() { const response = await fetch('https://jsonplaceholder.typicode.com/posts', { method: "POST", headers: { "Content-type": "application/json; charset=UTF-8", }, body: JSON.stringify({ //문자열로 변환하지 않으면 에러발생 "title": "오늘의 간식", "body": "고소미", "userId": 3 }) } ); const data = await response.json(); console.log(data); } post();
       
      JSON.stringify() 메서드는 배열이나 객체를 JSON 포맷의 문자열로 변환해준다.
       
      body: JSON.stringify({ "title": "오늘의 간식", "body": "고소미", "userId": 3 }); /* JSON.stringify 결과 { 'title' : '오늘의 간식', 'body' : '고소미', userId': 3 } */
       
      body에 담을 콘텐츠는 반드시JSON.stringify() 하여 JSON 문자열 형태로 변화해야 한다. 서버로 전송이 가능한 형태로 바꾸기 위해 문자열화 작업을 해줘야 하는데, 이를 직렬화라고 한다.
      💡
      객체(Object) → 문자열(string) : 직렬화(Serialization)
      문자열(string) → 객체(Object) : 역직렬화(Deserialization)
      GET을 사용하여 게시글 목록을 확인하면 POST요청을 보낸 새로운 콘텐츠가 생성되었음을 알 수 있다.
       
      // 결과 예시 [ { "userId": 2, "title": "이전 게시물", "body": "이전 게시물 입니다" }, { "userId": 3, "title": "오늘의 간식", "body": "고소미" } ]
       
      위의 결과 코드는 POST 요청의 결과를 보여주기 위해 만들어진 예시 코드이다. 예제에서 사용한 API는 free fake api로, 실제로 존재하는 서버가 아니기에 POST요청은 반영되지 않는다. 이번에는 직접 콘솔에서 테스트해볼 수 있는 코드를 살펴보도록 하자.
      게시판에 게시물을 작성한다고 생각하고 post로 데이터를 전달해보았다. 다음 결과값처럼 제대로 전송이 된 것을 확인할 수 있다.
       
      async function post() { const response = await fetch('https://httpbin.org/post', { method: "POST", headers: { "Content-type": "application/json; charset=UTF-8", }, body: JSON.stringify({ "title" : "게시물 제목", "body" : "게시물 내용", "userId" : "hojun", }) } ) const data = await response.json(); console.log(data) } post()
      notion imagenotion image
       
       
  1. DELETE 요청
    1. 리소스 삭제를 요청할 때 사용한다. 3번 user의 게시물을 모두 삭제하는 코드를 작성해보자.
       
      async function Delete() { const response = await fetch('https://jsonplaceholder.typicode.com/posts/3', { method: "DELETE"} ); const data = await response.json(); console.log(data); } Delete();
       
      실제 테스트 할 수 있는 코드는 아래와 같다. 게시물 번호가 있다면 3번 게시물을 삭제하라는 명령이다.
       
      async function deleteNotice() { const response = await fetch('https://httpbin.org/delete?noticeNumber=3', { method: "DELETE"} ); const data = await response.json(); console.log(data); } deleteNotice();
      notion imagenotion image
       
  1. PUT 요청
    1. 리소스 업데이트를 요청할 때 사용한다. 만약 업데이트할 리소스가 없을 시, 새로운 리소스가 생성된다.
      1번 user의 게시글 내용을 “고소미” → “마카다미아 쿠키”로 바꾸는 코드를 작성해보자. 게시글의 객체의 속성 값들을 모두 몸체에 기입하여 전송해야 한다. 만약 기입하지 않은 속성 값이 있다면 해당 value에는 null값이 들어간다.
       
      async function put() { const response = await fetch('https://jsonplaceholder.typicode.com/posts/1', { method: "PUT", headers: { "Content-type": "application/json; charset=UTF-8", }, body: JSON.stringify({ "title" : "오늘의 간식", "body" : "마카다미아 쿠키",//업데이트할 내용 "userId" : 1 //1번 user }) } ); const data = await response.json(); console.log(data); } put(); /* 결과 예시 { "userId": 1, "title": "오늘의 간식", "body": "마카다미아 쿠키" } */
       
      테스트 할 수 있는 코드는 아래와 같다.
       
      async function put() { const response = await fetch('https://httpbin.org/put?noticeNumber=3', { method: "PUT", headers: { "Content-type": "application/json; charset=UTF-8", }, body: JSON.stringify({ "title" : "오늘의 간식", "body" : "마카다미아 쿠키", //업데이트할 내용 "userId" : 1 //1번 user }) } ); const data = await response.json(); console.log(data); } put();
       
      notion imagenotion image
       
  1. PATCH요청
    1. PATCH는 PUT과 마찬가지로 리소스를 업데이트 할 때 요청하는 요청 메서드 이다.
      PUT은 전체 업데이트만 가능하여 속성 값들을 전부 body에 담아 요청을 보내야 한다. 이와 달리 PATCH는 부분 데이터만 업데이트가 가능하기 때문에 요청 몸체에 수정이 필요한 데이터만 담으면 된다.
      1번 user의 게시글 내용을 “고소미” → “마카다미아 쿠키”로 바꾸는 코드를 작성해보자.
       
      async function patch() { const response = await fetch('https://jsonplaceholder.typicode.com/posts/1', { method: "PATCH", headers: { "Content-type": "application/json; charset=UTF-8", }, body: JSON.stringify({ "body" : "마카다이마 쿠키", }); } ); const data = await response.json(); console.log(data); } patch(); /* 결과 예시 { "userId": 1, "title": "오늘의 간식", "body": "마카다미아 쿠키" } */
       
      테스트 할 수 있는 코드는 아래와 같다.
       
      async function patch() { const response = await fetch('https://httpbin.org/patch?noticeNumber=3', { method: "PATCH", headers: { "Content-type": "application/json; charset=UTF-8", }, body: JSON.stringify({ "body" : "마카다미아 쿠키", }) } ); const data = await response.json(); console.log(data); } patch();
notion imagenotion image
 
💡
fetch()는 웹 애플리케이션이 비동기적으로 작동하는 방식을 일컫는Ajax를 구현하기 위해 쓰이는 Web API이다. fetch()는 비교적 최신의 API이며 가장 유연하고 간단하다. fetch 함수에는 2개의 매개변수가 있는데, 첫 번째는 필수 매개변수로 데이터를 요청할 서버의 url을 기입해야 한다. 두 번째는 선택 매개변수이며, headers, mehod, body를 입력하여 다양한 서버 통신을 할 수 있다. fetch()는 HTTP 응답을 담고 있는 Response객체를 가진 프로미스 객체를 반환한다. 프로미스 반환 값의 Response 객체 안에는 응답 헤더가 있어서 서버가 데이터를 성공적으로 찾았는지 여부를 파악할 수 있다. 서버에서 가져온 데이터에 접근하기 위해서는 response 메서드를 이용해서 데이터 형태를 변환해야 한다.
 
 

Reference


  1. https://dev.to/lydiahallie/javascript-visualized-promises-async-await-5gke#asyncawait
  1. https://caniuse.com/fetch
  1. https://nodejs.org/api/url.html#url