1. 인트로... (Intro)
JavaScript(JS)의 동기적 / 비동기적 처리를 먼저 아래 그림으로 확인해 보자!
1.1 동기 / 비동기란? 🤔🤔
동기 (Synchronous)
먼저 시작된 하나의 작업이 끝날 때 까지 다른 작업을 하지 않고 기다렸다가 먼저 시작된 작업이 끝나면 다음 새로운 작업을 시작함
즉, 여러 작업을 한 번에 처리하지 않고 하나씩 처리!!
비동기 (Asynchronous)
먼저 시작된 작업이 끝날 때 까지 기다리지 않고 바로 다음 작업을 수행함
즉, 여러 작업을 한 번에 처리!!
JavaScript는 싱글 스레드 언어이기 때문에 한 번에 하나의 작업만 수행 가능하다.
프로그래밍을 하면서 일반적으로 각 함수와 코드들이 위에서 아래로 동작하는데 이러한 코드 순차 실행을 "동기" 라고 말한다.
동기 방식은 간단하고 직관적이지만 작업이 오래 걸리거나 응답이 늦어지는 경우 성능에 영향을 줄 수 있다.
예를 들어 서버에 데이터를 요청하고 응답을 받아야 하는데 이 응답이 올 때까지 다른 작업을 하지 못하고 대기해야 하는데 이렇게 되면 프로그램의 흐름이 멈추거나 지연이 된다.. OMG...🥲😥
이러한 부분을 방지하고자 JavaScrip로 여러 작업을 동시에 처리하기 위해 "비동기" 라는 개념 등장!!
비동기 작업을 통하여 특정 작업의 완료를 기다리지 않고 다른 작업을 동시에 수행할 수 있다.
프로젝트를 진행하다 보면 (저 쪽 웹 담당 하시는 분들 회의하는 걸 들었을 때..)
비동기 처리로 진행해야 된다는 말을 들을 수 있었는데 이 의미는 메인 스레드가 작업을 다른 곳에 인가하여 처리되게 하고, 그 작업이 완료되면 콜백 함수를 받아 실행하는 방식으로..... (내가 쓰면서도 뭔 말인지...)
쉽게 말하자면 작업을 백그라운드에 요청하여 처리 하고 멀티로 작업을 동시에 처리한다고 생각하면 된다.
병렬로 작업을 동시 처리하여 프로그램의 흐름이 멈추거나 지연되지 않게되어 총 코드 실행 시간이 줄어 빠른 처리가 가능해 진다.
1.2 AJAX란? 🤔🤔
Asynchronous JavaScript And XML의 약자.. (난 영어 약자..😥😣)
→ JavaScript와 XML을 이용한 비동기적 정보 교환 통신 기법
서버와 비동기적으로 통신할 때 사용하는 API라 할 수 있다.
AJAX를 사용하면 백그라운드 영역에서 비동기적으로 서버와 통신하고 그 결과를 웹 페이지의 일부분에 표시한다.
쉽게 말하자면 AJAX라는 놈을 사용해서 웹 페이지 전체를 다시 로딩하지 않고 웹 페이지의 일부분만을 갱신 할 수 있다!!!
1.3 JSON이란? 🤔🤔
JavaScript Object Notation의 약자이며 데이터를 저장하거나 전송할 때 많이 사용되는 경량의 데이터 교환 형식이다.
{
"name":"Jack",
"age":30,
"contactNumbers":[
{
"type":"Home",
"number":"123 123-123"
},
{
"type":"Office",
"number":"321 321-321"
}
],
"spouse":null,
"favoriteSports":[
"Football",
"Cricket"
]
}
최근에는 JSON이 XML을 대체해서 데이터 전송에 많이 사용되고 있으며 특정 언어에 종속되지 않기 때문에 다른 프로그래밍 언어를 이용해서도 쉽게 생성이 가능하다.
2. JavaScript의 비동기 처리
2.1 JavaScript 비동기 병렬 처리 원리
비동기 함수의 Callback 함수가 Event Loop에 의해서 Callback Queue에 담기고 다시 싱글 스레드인 Call Stack에 담겨 Callback 함수가 실행되는 동작 원리이다.
글과 말로는 어려우니 밑에 그림을 보자...
setTimeout() 함수를 예를 들어 설명 하자면 이렇다.
먼저 setTimeout() 함수는 특정 코드를 바로 실행하지 않고 일정 시간동안 지연시킨 후 실행해 준다.
setTimeout(() => console.log("2초 후 실행"), 2000);
비동기 함수인 setTimeout() 함수를 활용하여 비동기 처리가 진행되는지 간단한게 살펴 보자면..
const foo = () => console.log("First");
const bar = () => setTimeout(() => console.log("Second"), 2000);
const baz = () => console.log("Third");
bar();
foo();
baz();
1. bar()가 호출되어 setTimeout()을 반환하고 Call Stack에 추가 된다.
setTimeout()은 비동기 함수이기 때문에 타이머가 완료 될 따까지 Call Stack에 머물지 않고 Web API에 추가되어 비동기적으로 실행된다.
2. Web API에서 Callback 함수 타이머가 실행되는 동안 foo()가 호출되어 Call Stack에 추가된다.
실행 완료 후 Call Stack을 빠져나가며 Output에 "First"를 기록한다.
3. 그 다음 baz()가 호출되어 Call Stack에 추가되어 실행 완료 후 Call Stack을 빠져나가며 "Third"를 Output에 기록 한다.
4. foo()와 baz()가 실행되는 동안 Callback 함수의 타이머가 완료되면 Callback Queue로 들어가서 Call Stack이 비워질 때 까지 대기한 후 Event Loop가 Call Stack과 Callback Queue의 상태를 확인하며 Call Stack이 비워지는 것을 확인하면 Callback 함수를 Call Stack으로 이동 시킨다.
Output에 "Second"를 기록하며 Call Stack을 빠져 나간다.
그렇다면 JavaScript는 싱글 스레드 언어인데 어떻게 작업(task)들을 동시에 처리가 가능할까??
위에서 살짝 언급 한 Web API 때문이다.
설명하자면 (아래 그림 참조) JavaScript를 실행하는 Call Stack은 싱글 스레드지만 서버에게 리소스를 요청하거나 파일 입출력 혹은 타이머 대기 작업을 실행하는 Web API들은 멀티 스레드이기 때문에 동시 작업 처리가 가능하다.
멀티 스레드
쉽게 말하자면 멀티 스레드는 백그라운드에서 동시에 처리된다고 이해하면 된다.
Web API
Web API를 조금 더 설명하자면 타이머, 네트워크 요청, 파일 입출력, 이벤트 처리 등 브라우저에서 제공하는 다양한 API를 포괄하는 API 총칭이다. 브라우저마다 다르겠지만 우리가 자주 사용하는 크롬 브라우저 같은 경우 Web API는 멀티 스레드로 구현되어 있다.
정리하자면 브라우저는 멀티스레드로 이루어져 있고 비동기 함수는 이러한 동시적 처리 작업 원리 덕분에 성능 향상을 할 수 있었다.
2.2 비동기 처리 기법
2.2.1 Callback 함수
Callback 함수는 파라미터로 함수를 전달 받아 함수의 내부에서 실행하는 함수이다.
비동기 방식은 요청과 응답의 순서를 보장하지 않기 때문에 응답의 처리 결과에 의존하는 경우 Callback 함수를 이용하여 작업 순서를 간접적으로 맞출 수 있다.
function getDB(callback) {
// 데이터베이스로부터 3초 후에 데이터 값을 받아온 후, 콜백 함수 호출
setTimeout(() => {
const value = 100;
callback(value);
}, 3000);
}
function main() {
// 호출할 작업에 콜백 함수를 넘긴다
getDB(function(value) {
let data = value * 2;
console.log('data의 값 : ', data);
});
}
main();
data의 값 : 200
위 코드는 Callback 함수 내에서 data 변수 값을 받아 출력이 되므로 비동기 작업이 완료된 후 출력된다.
즉, Callback 함수는 비동기 함수에서 작업 결과를 전달받아 처리하는데 사용되어 작업 순서를 맞출 수 있다.
따라서 비동기 함수와 Callback 함수는 서로 밀접한 관계를 가지고 있다.
그런데 Callback 함수 방식은 복잡하게 얽힌 비동기 처리를 하게 되면 개발자가 어플리케이션의 흐름을 읽기 어려워지는 문제가 발생하여 Callback 지옥(Hell)에 빠질 수 있다.
즉, 함수의 매개변수로 넘겨지는 Callback 함수가 반복되어 코드의 들여쓰기 수준이 감당하기 힘들어질 정도로 깊어지는 현상이다.
이러한 Callback 지옥을 해결하기 위해 새로운 비동기 처리 방법으로 Promise가 탄생하였다.
2.2.2 Promise 객체
앞서 설명한 Callback 함수는 비동기를 순차적으로 처리하기 위한 일종의 편법이며 정식으로 지원하는 비동기 전용 함수가 아니다.
위에서 말했듯이 이러한 한계점을 극복하기 위해 비동기 처리를 위한 전용 객체인 Promise가 탄생하였다.
Promise는 비동기 작업의 성공 또는 실패와 그 결과값을 나타내는 객체이며 Promise를 사용하면 비동기 작업을 쉽고 깔끔하게 연결할 수 있다.
Promise는 다음 3가지의 상태를 갖는다.
- 대기 (Pending) : 진행 상태, Promise 객체가 생성되어 사용될 준비가 된 상태
Promise 객체는 new Promise()로 생성 가능하며 Callback 함수를 선언할 수 있고 Callback 함수 인자는 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) // 거부 상태
)
})
function getDB() {
return new Promise((resolve) => {
setTimeout(() => {
const value = 100;
resolve(value);
}, 3000);
});
}
function main() {
getDB()
.then((value) => {
let data = value * 2;
console.log('data의 값 : ', data);
})
.catch((error) => {
console.error(error);
});
}
main();
Promise 처리 흐름을 보자면
1. new Promise() 메서드를 호출하면 대기 (Pending) 상태가 된다.
이 때 Callback 함수를 선언할 수 있고 인자는 resolve, reject이다.
new Promise(); // new Promise() 메서드 호출, 대기 상태 new Promise(function(resolve, reject) { // 콜백 함수 선언 // ... });
2. Callback 함수의 인자 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" });
3. Callback 함수의 인자 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 출력 });
2.2.3 async / await (에이싱크 / 어웨잇)
Promise 객체도 완벽한 해결책은 아니라고 한다...
Callback 지옥이 있듯이 지나친 then 핸들러 함수의 남용으로 인한 Promise 지옥이 존재한다.
즉, Promise가 여러 개 연결되면 코드가 길어지고 복잡해 질 수 있다.
그래서 JavaScript에는 async / await이라는 문법이 또한 추가 되었다... (그만 좀...ㅠㅠ 😭😭😭😭)
async / await는 Promise를 기반으로 하지만 마치 동기 코드처럼 작성할 수 있게 해주며 비동기 작업을 쉽게 읽고 이해할 수 있게 해주기 때문에 비동기 작업을 처리할 일이 있다면 이 방식을 쓰는 것이 보통이다.
정리하자면 async / await를 사용하면 기존의 Promise를 보다 간결하게 작성할 수 있다.
따라서 async / await는 비동기 코드를 동기적인 코드인 것처럼 직관적으로 바꿔주는 역할을 한다.
→ 비동기 코드에 순서를 부여
잠깐! 💡
Promise나 async / await과 같은 문법을 사용하는 이유는 비동기 처리의 흐름을 명확하게 인지하고자 하는 노력이다.
기본 구조는 아래와 같다.
async function() {
await
}
then이 하던 작업을 await이 대신 한다.
async function asyncFunc() {
let response = await fetch('#');
let data = await response.json();
return data;
}
try-catch 구문을 사용하여 Error 처리도 가능하다.
async function asyncFunc() {
try { // try 안에 실행될 코드를 넣어주면 됨
let response = await fetch('#');
let data = await response.json();
return data;
} catch (e) { // catch에는 에러시 실행될 코드
console.log("error : ", e)
}
}
function getDB() {
return new Promise((resolve, reject) => {
// 데이터베이스에서 값을 가져오는 3초 걸린다고 가정 (비동기 처리)
setTimeout(() => {
const value = 100;
resolve(value); // Promise 객체 반환
}, 3000);
});
}
async function main() {
let data = await getDB(); // await 키워드로 Promise가 완료될 때까지 기다린다
data *= 2;
console.log('data의 값 : ', data);
}
main(); // 메인 스레드 실행
결국 async / await이 비동기를 처리함에 있어 Callback 함수와 Promise 방식보다 훨씬 좋아 보이지만 사용 방법과 환경에 따라 코드가 복잡해질 수 있으므로 비동기 처리에 대한 3가지 방식은 용도에 맞춰 적절히 사용해야 한다.
Callback 함수 방식도 복잡한 상황이 아닐 때 사용하면 오히려 가독성이 좋을 수 있는데 예를 들자면 Node.js의 Express 프레임워크는 서버 라우팅을 Callback 함수로 처리하는 방식을 제공한다.
const express = require("express"); // Express 모듈 불러오기
const app = express(); // Express 앱 객체 생성
// /home url 경로에 GET 요청이 들어오면 이에 대한 라우팅 정의
app.get("/home", function (req, res) {
// 응답 보내기
res.send("Hello, Express!");
});
정리하자면 결국 작업과 환경에 따라 사용하되 Callback 함수는 복잡하지 않고 심플한 비동기 작업을 처리해야 할 때 사용하면 오히려 Promise 방식보다 좋을 수 있으며 비교적 복잡한 비동기 작업을 처리할 때는 Promise 객체와 async / await을 적절하게 사용하면 코드를 보다 간결하게 작성할 수 있다.
※ 참조 url :
하기 url에서 보고 해보고 느끼고 한 것들을 정리하였습니다.
- https://developer.mozilla.org/ko/docs/Learn/JavaScript/Asynchronous
비동기 JavaScript - Web 개발 학습하기 | MDN
이 과정에서 우리는 asynchronous JavaScript와 이것이 중요한 이유, 서버에서 리소스를 가져오는 것과 같은 잠재적 블로킹 연산을 어떻게 효과적으로 다룰 수 있을지에 대해 살펴봅니다.
developer.mozilla.org
- https://velog.io/@kim_unknown_/JavaScript-Asynchronous
[JavaScript] 자바스크립트 비동기식 처리
동기식 (Synchronous) 먼저 시작된 하나의 작업이 끝날 때까지 다른 작업을 시작하지 않고 기다렸다가 다 끝나면 새로운 작업을 시작하는 방식. 즉, 한 번에 여러 작업을 처리하지 않고 하나만 처리
velog.io
- https://inpa.tistory.com/entry/%F0%9F%8C%90-js-async
🌐 자바스크립트의 핵심 '비동기' 완벽 이해 ❗
자바스크립트의 동기와 비동기 자바스크립트는 싱글 스레드 언어이기 때문에 한 번에 하나의 작업만 수행할 수 있다. 즉, 이전 작업이 완료되어야 다음 작업을 수행할 수 있게 된다. 우리가 프
inpa.tistory.com
'스터디 (정리) > Frontend' 카테고리의 다른 글
[Front-end] CSS 개념 (Feat. HTML) (3) | 2023.05.27 |
---|---|
[Front-end] JavaScript 발톱부터 담가보자...😂🤣 (6) | 2023.05.23 |