동기식 (Synchronous)
먼저 시작된 하나의 작업이 끝날 때까지 다른 작업을 시작하지 않고 기다렸다가 다 끝나면 새로운 작업을 시작하는 방식. 즉, 한 번에 여러 작업을 처리하지 않고 하나만 처리함.
비동기식 (Asynchronous)
먼저 실행된 작업이 끝날 때까지 기다리지 않고 다음 작업을 수행하는 방식. 즉, 한 번에 여러 작업을 처리함.
Asynchronous JavaScript And XML의 약자로, 서버와 비동기적으로 통신할 때 사용하는 API이다.
AJAX를 사용하면 백그라운드 영역에서 비동기적으로 서버와 통신하여, 그 결과를 웹 페이지의 일부분에만 표시한다.
즉, AJAX를 사용함으로써 웹 페이지 전체를 다시 로딩하지 않고도, 웹 페이지의 일부분만을 갱신할 수 있다.
AJAX에 대한 자세한 내용은 아래 글을 참조하도록 한다.
🔗 AJAX 기초 개념 정리
🔗 AJAX란 무엇인가?
JavaScript Object Notation의 약자로, 데이터를 저장하거나 전송할 때 많이 사용되는 경량의 DATA 교환 형식이다.
JSON은 사람과 기계 모두 이해하기 쉬우며 용량이 작아서, 최근에는 JSON이 XML을 대체해서 데이터 전송 등에 많이 사용된다. JSON 문서 형식은 자바스크립트 객체의 형식을 기반으로 만들어져서 자바스크립트 객체 표기법과 아주 유사하다. 특정 언어에 종속되지 않기 때문에 다른 프로그래밍 언어를 이용해서도 쉽게 만들 수 있다.
JSON에 대한 자세한 내용은 아래 글을 참조하도록 한다.
🔗 JSON이란 무엇인가?
자바스크립트는 싱글스레드(하나의 메인스레드) 런타임을 가진 동기식 언어이다.
분기문, 반복문, 함수 호출 등이 동기적으로 실행되며, 이 때 코드의 처리는 코드의 흐름과 동일하다. 싱글 스레드 환경에서 메인 스레드를 긴 시간 점유하면, 프로그램이 멈출 수도 있다.
하지만, 자바스크립트는 브라우저에서 별도의 API를 사용하여 비동기적으로 작업을 처리할 수 있다.
자바스크립트에서 비동기 코드를 처리하는 모듈은 이벤트 루프(event loop), 태스크 큐(task queue), 잡 큐(job queue) 등으로 구성된다.
API 모듈은 비동기 요청을 처리 후 태스크 큐에 콜백 함수를 넣는다. 자바스크립트 엔진은 콜 스택이 비워지면, 태스크 큐의 콜백 함수를 실행한다.
자바스크립트는 비동기 내장함수를 제공하는 데, 그것은 바로 setTimeout
, XMLHttpRequest
, fetch()
이다.
XMLHttpRequest는 현재는 잘 사용하지 않는 것으로 현재는 fetch를 많이 사용한다. fetch는 Promise 기반이기 때문에 Promise 이후에 살펴보겠다. 따라서 여기선 setTimeout에 대해서만 간단하게 보도록 하자.
• setTimeout() 함수는 특정 코드를 바로 실행하지 않고 일정 시간동안 지연시킨 후 실행한다.
setTimeout(function() { 코드 or 콜백함수 }, 지연시간);
setTimeout(() => console.log("2초 후에 실행됨"), 2000);
// 지연 시간은 밀리초 단위로 기입. 1초 -> 1000
// 2초 후에 콘솔에 "2초 후에 실행됨" 출력
• clearTimeout() 함수는 setTimeout()을 취소, 중지시킨다.
clearTimeout( [식별자] );
아래는 clearTimeout을 이용한 디바운싱 예제이다.
* 디바운싱(debouncing) : 연이어 호출되는 함수들 중 마지막 함수(또는 제일 처음)만 호출하도록 하는 것var timer; document.querySelector('#input').addEventListener('input', function(e) { if (timer) { // input 될 때 마다 이벤트가 발생하지 않도록 n-1 번째 이벤트 취소 clearTimeout(timer); } // n번째 이벤트만 남겨서 마지막 이벤트만 실행되게 timer = setTimeout(function() { console.log('여기에 ajax 요청', e.target.value); }, 200); });
➰ 비동기 함수인 setTimeout을 예시로 자바스크립트에서 어떻게 비동기 처리를 하는지 간단하게 살펴보겠다.
const foo = () => console.log("First");
const bar = () => setTimeout(() => console.log("Second"), 500);
const baz = () => console.log("Third");
bar();
foo();
baz();
① bar가 호출되면 setTimeout을 반환하고 콜스택에 추가 된다.
setTimeout 함수는 비동기 함수이기 때문에 타이머가 완료 될 때까지 콜스택에 하염없이 머물지 않고, 미래에 실행될 것을 약속한 후에 setTimeout의 콜백 함수가 Web API에 추가되어 비동기적으로 실행 된다.
② Web API에서 콜백함수의 타이머가 실행되는 동안 foo가 호출되어 콜스택에 추가된다. 실행 완료 후 콜스택을 빠져나가며 "First"를 기록한다.
③ baz가 호출되어 콜스택에 추가된다. 실행 완료 후 콜스택을 빠져나가며 "Second"를 기록한다.
④ foo와 baz가 실행되는 동안 콜백함수의 타이머가 완료되면 콜백함수는 태스크 큐에 들어가서 콜스택이 비워질 때까지 기다린다. 이 때, 이벤트 루프가 콜스택과 태스크 큐의 상태를 확인하며 콜스택이 비워지면 콜백 함수를 콜스택으로 이동시킨다. 이렇게 해서 콜백함수의 실행이 완료되면 콜스택을 빠져나간다.
이벤트 루프 이해에 도움을 얻은 영상 📽️ Event Loop
① Callback Function
② Promise
③ async/await
콜백 함수
는 파라미터로 함수를 전달 받아 함수의 내부에서 실행하는 함수이다.
그런데 콜백 함수는 자칫하면 '콜백 지옥 (callback hell)'이 발생할 수 있다.
콜백 지옥이란, 함수의 매개변수로 넘겨지는 콜백 함수가 반복되어 코드의 들여쓰기 수준이 감당하기 힘들어질 정도로 깊어지는 현상이다.
이러한 콜백 지옥을 해결하기 위해 새로운 비동기 처리 방법으로 Promise가 탄생한 것이다. Promise에 대해 알아보도록 하자.
Promise
는 자바스크립트에서 제공하는 비동기를 간편하게 처리할 수 있도록 도와주는 객체이다.
Promise의 상태는 프로세스가 기능을 수행하고 있는 중인지, 기능 수행이 완료되어 성공했는지 실패했는지에 대한 상태를 말하며, 다음 3가지의 상태를 갖는다.
① 대기(pending) : 진행 상태, Promise 객체가 생성되어 사용될 준비가 된 상태
promise의 객체는 new Promise()
로 생성할 수 있으며, 콜백 함수를 선언할 수 있고, 콜백 함수의 인자는 resolve, reject이다.
new Promise(function(resolve, reject) {})
② 이행(Fulfilled) : 성공 상태, 비동기 처리에 의해 원하는 올바른 결과를 얻어와 그 결과를 정상적으로 처리하고자 resolve
가 호출된 상태
③ 거부(Rejected) : 실패 상태, 무언가 잘못되어 예외로 처리하고자 reject
가 호출된 상태
const promise = new Promise((resolve, reject) => { // 대기 상태
getData(
response => resolve(response.data), // 이행 상태
error => reject(error.message) // 거부 상태
)
})
* 체이닝(chaining) : 동일한 객체에 메서드를 연결할 수 있는 것
• then()
- 성공(resolve) 시에는 then 메서드에 실행할 콜백 함수를 인자로 넘긴다.
• catch()
- 실패(reject) 시에는 catch 메서드에 실행할 콜백 함수를 인자로 넘긴다.
• finally()
- 성공/실패 여부와 상관없이 모두 실행 시에는 finally 메서드에 실행할 콜백 함수를 인자로 넘긴다.
promise
.then(data => console.log(data))
.catch(err => console.error(err))
.finally(() => console.log("always run")
위와 같이 메서드를 체이닝하면 함수를 호출한 주체가 실행을 완료한 뒤 자기 자신을 리턴한다.
① new Promise() 메서드를 호출하면 대기(Pending) 상태가 된다.
이 때, 콜백 함수를 선언할 수 있고 인자는 resolve, reject이다.
new Promise(); // new Promise() 메서드 호출, 대기 상태
new Promise(function(resolve, reject) { // 콜백 함수 선언
// ...
});
② 콜백 함수의 인자 resolve를 실행하면 이행(Fulfilled) 상태가 된다.
new Promise(function(resolve, reject) {
resolve();
});
또한, 이행 상태가 되면 then()을 이용하여 처리 결과 값을 받을 수 있다.
function getData() {
return new Promise(function(resolve, reject) {
var data = "resolve";
resolve(data);
});
}
getData().then(function(resolvedData) {
console.log(resolvedData); // "resolve"
});
③ 콜백 함수의 인자 reject를 실행하면 거부(Rejected) 상태가 된다.
new Promise(function(resolve, reject) {
reject();
});
실패 상태가 되면 실패한 이유(실패 처리의 결과 값)를 catch()로 받을 수 있다.
function getData() {
return new Promise(function(resolve, reject) {
reject(new Error("Request is failed"));
});
}
getData().then().catch(function(err) {
console.log(err); // Error: Request is failed
});
종합적인 예제
function getData() { return new Promise(function(resolve, reject) { $.get('url 주소/products/1', function(response) { if (response) { resolve(response); } reject(new Error("Request is failed")); }); }); } // 위 $.get() 호출 결과에 따라 'response' 또는 'Error' 출력 getData().then(function(data) { console.log(data); // response 값 출력 }).catch(function(err) { console.error(err); // Error 출력 });
Promise.all
은 여러 작업을 동시에 처리하고, 작업이 모두 다 실행 완료되면 모두 다 반환한다.
Promise.all([timer(1000), timer(2000), timer(3000)])
.then((value) => { console.log(value); });
// [1000, 2000, 3000] → 3000까지 모두 다 실행이 끝난 후에 다 반환
Promise.race
은 실행이 가장 빠른 결과를 반환한다.
Promise.race([timer(1000), timer(2000), timer(3000)])
.then((value) => { console.log(value); });
// 1000 → 가장 빨리 실행된 1000 반환
Fetch 함수는 자바스크립트에서 서버로 네트워크 요청을 보내고 응답을 받을 수 있도록 해준다. Promise 기반으로 구성되어 있어 비동기 처리에 편리하다. 따라서 Promise의 후속 처리 메서드인 then이나 catch와 같은 체이닝으로 작성할 수 있다.
// fetch 기본 구조
fetch(url, options) // url, options
.then(response => response.json())
// 첫 번째 then에서는 데이터 타입을 결정한다.
.then(data => { console.log(data) })
// 두 번째 then에서는 데이터를 전달 받는다.
.catch(error => { console.log("error") });
// catch에서 에러 요청이 발생했을 때, 에러를 받는다.
보이는 것과 같이 Promise 객체와 기본적인 구조와 동작은 비슷하다.
첫번째 인자로 URL, 두번째 인자로 옵션 객체를 받고 Promise 타입의 객체를 반환한다. 반환된 객체는, API 호출이 성공했을 경우에는 응답(response) 객체를 resolve하고, 실패했을 경우에는 예외(error) 객체를 reject한다.
코드를 좀 더 자세히 뜯어보자면,
fetch의 첫 번째 인자에 요청할 url이 들어가고, 두 번째 인자에는 옵션이 들어간다. 기본적으로 http 메소드 중 GET
으로 동작하며, fetch를 통해 ajax를 호출 시 해당 주소에 요청을 보낸 다음, 응답을 받게 된다.
응답을 받은 후에, 첫 번째 then
에서는 response.json()
메서드로 JSON 형태로 파싱하여 리턴한다. (이 때 response.text()
로 작성하면 텍스트 형태로 리턴한다.)
그 다음 두 번째 then에서 리턴 받은 json 값을 받고, 원하는 처리 한다.
마지막으로 catch에서는 에러 처리를 한다.
- Http Method(CRUD)
• Create ->POST
(해당 URI를 요청하면 리소스를 생성)
• Read ->GET
(해당 리소스를 조회)
• Update ->PUT
(해당 리소스를 수정)
• Delete ->DELETE
(리소스를 삭제)
GET 메서드는 데이터를 가져올 때 쓰이며, default 동작이기 때문에 option을 작성하지 않아도 된다.
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then((response) => response.json())
.then((data) => console.log(data))
json 형태로 파싱했기 때문에 json 형태의 데이터를 얻을 수 있다.
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit↵suscipit recusandae consequuntur …strum rerum est autem sunt rem eveniet architecto"
}
POST 메서드는 데이터를 보낼 때 사용한다. method 옵션을 POST로 지정해주고, headers 옵션으로 JSON 포맷 사용한다고 알려줘야 하며, body 옵션에는 요청 데이터를 JSON 포맷으로 넣어준다.
fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "Test",
body: "I am testing!",
userId: 1,
}),
})
.then((response) => response.json())
.then((data) => console.log(data))
JSON.stringify()
메서드는 JavaScript 값이나 객체를 JSON 문자열로 변환한다.
{title: "Test", body: "I am testing!", userId: 1, id: 101}
PUT 메서드는 API에서 관리하는 데이터의 수정을 할 때 사용한다. method 옵션만 PUT으로 설정한다는 점 외에는 POST 방식과 비슷하다.
fetch("https://jsonplaceholder.typicode.com/posts", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "Test",
body: "I am testing!",
userId: 1,
}),
})
.then((response) => response.json())
.then((data) => console.log(data))
DELETE 메서드는 데이터를 삭제할 때 사용하며, 보내는 데이터가 없기 때문에 headers, body 옵션이 필요 없다.
fetch("https://jsonplaceholder.typicode.com/posts/1", {
method: "DELETE",
})
.then((response) => response.json())
.then((data) => console.log(data))
then()을 연쇄적으로 체이닝하다보면 콜백지옥마냥 혼란에 빠질 수 있다. 이러한 문제를 해결하기 위해 탄생한 것이 async/await이다.
즉, async/await를 사용하면 기존의 Promise를 보다 간결하게 작성할 수 있다.
따라서 async/await는 비동기 코드를 동기적인 코드인 것처럼 직관적으로 바꿔주는 역할을 한다. → 비동기 코드에 순서를 부여
• await는 반드시 async 함수 안에서 실행되며, async는 항상 Promise 객체를 반환한다.
// 기본 구조
async function() {
await
}
• then이 하던 작업을 await가 대신한다. 따라서 await 키워드는, then을 체이닝한 것처럼 순서대로 동작한다.
async function asyncFunc() {
let response = await fetch('#');
let data = await response.json();
return data;
}
• try-catch 구문을 사용하여 에러 처리를 할 수 있다는 것도 장점이다.
async function asyncFunc() {
try { // try 안에 실행될 코드를 넣어주면 됨
let response = await fetch('#');
let data = await response.json();
return data;
} catch (e) { // catch에는 에러시 실행될 코드
console.log("error : ", e)
}
}
참고 자료
🔗 자바스크립트 Promise 쉽게 이해하기
🔗 서버 요청 및 응답 (자바스크립트 fetch API)
🔗 자바스크립트 async와 await
🔗 📚 비동기처리 (async / await) 개념 & 문법 💯 정리
🔗 💡 async / await 문법은 이해하겠는데 도대체 정확히 뭐하는 용도인지