📖 [JS] 동기와 비동기
자바스크립트에서 "동기"와 "비동기"는 코드 실행 방식의 차이를 설명하는 중요한 개념입니다.
이 두 개념은 특히 비동기 프로그래밍이 필수적인 현대 웹 개발에서 매우 중요합니다.
동기(Synchronous)
동기적 코드 실행은 코드가 순차적으로 한 줄씩 실행됨을 의미합니다.
즉, 한 작업이 완료된 후에야 다음 작업이 시작됩니다. 이 방식은 이해하기 쉽고 예측 가능하지만, 시간이 많이 걸리는 작업
(예: 네트워크 요청, 파일 읽기 등)이 있을 경우, 브라우저가 응답하지 않게 될 위험이 있습니다.
자바스크립트와 스레드
자바스크립트는 싱글 스레드(Single-Threaded) 언어로 설계되었습니다.
이는 자바스크립트 엔진이 한 번에 하나의 작업만 실행할 수 있음을 의미합니다.
하지만 자바스크립트는 비동기 프로그래밍 모델을 사용하여 동시성을 제공하며,
이를 통해 마치 여러 작업이 동시에 실행되는 것처럼 보이게 합니다.
자바스크립트의 비동기 모델은 이벤트 루프(Event Loop)라는 메커니즘을 통해 구현됩니다. 이벤트 루프는 콜백 함수, 프로미스, async/await와 같은 비동기 작업을 관리하며, 이러한 작업이 완료되면 콜백 큐에서 호출 스택으로 작업을 전달합니다.
비동기(Asynchronous)
비동기적 코드 실행은 특정 작업이 완료될 때까지 기다리지 않고 다음 작업을 바로 시작합니다.
비동기 작업이 완료되면, 관련된 콜백 함수나 프로미스가 실행됩니다.
비동기 방식은 네트워크 요청, 타이머, 파일 입출력 등 시간이 걸릴 수 있는 작업을 처리할 때 매우 유용합니다.
이를 통해 브라우저가 응답하지 않게 되는 것을 방지할 수 있습니다.
setTiemout()은
첫 번째 함수로 콜백 함수를 넣고, 두 번째 인수로 숫자값에 해당하는 ms 지나면
첫 번째 인수로 전달한 콜백함수를 실행하게 된다.
console.log(1);
setTimeout(() => {
console.log(2);
}, 3000); // 원하는 코드를 특정 시간 이후의 비동기적 처리
console.log(3);
>>> 1
3
2
위의 자바 스크립트의 코드는 여러 개 작업을 동시에 작업하는 것처럼 보인다.
하지만 자바스크립트는 싱글스레드 즉, 스레드를 하나 밖에 가지고 있지 못한다.
그렇기 때문에 이렇게 여러 개의 스레드를 활용하지 못하여서 동시에 여러 가지 일을 처리하기가 힘듭니다.
하지만 어떻게 여러 개의 작업을 동시에 처리할 수 있는 것일까요?
그 이유는 setTiemout 함수 같은 비동기 작업들은 사실 자바스크립트 엔진에 있는 스레드가 실행한 것이 아니라
Web APIs라는 브라우저가 직접 관리하는 별도의 공간에서 따로 실행된 것이기 때문이다.
Web APIs라는 건 간단히 말해서 웹 브라우저가 직접 관리하는 별도의 영역을 말한다.
자바스크립트 엔진은 setTimeout과 같은 비동기 함수를 만나게 되면,
비동기 작업을 브라우저의 Web APIs에게 실행을 요청하게 되고,
타이머가 끝나면 실행할 콜백 함수까지 넘겨주게 된다.
위와 같은 방식으로 비동기 작업이 일어난다.
비동기 처리하기
1. 콜백함수
function add(a, b) {
setTimeout(()=>{
const sum = a + b;
console.log(sum);
}, 3000);
};
add(1, 2) >> 3초 뒤에 3 출력
-------------------------------------
// 비동기 처리의 결과값인 sum이라는 변수에 들어있는 값을 add 함수 밖에서 사용하기
function add(a, b, callback) {
setTimeout(()=>{
const sum = a + b; //3
callback(sum)
}, 3000);
};
add(1, 2, (value) => {
console.log(value)
}) >> 3초 뒤에 3 출력
비동기 작업을 하는 함수의 결괏값을 함수 외부에서 이용하고 싶다면
콜백 함수를 사용해서 비동기 함수 안에서 콜백 함수를 호출하도록 설정하면 된다.
비동기 작업의 결과를 또 다른 비동기 작업에 인수로 넣어주는 코드가 계속 반복이 되다 보면
callback 함수 안에서 계속 함수를 호출하는 문법으로 코드가 계속 작성되기 때문에
indent가 점점 깊어지는 형태로 코드가 진화할 수 있다.
이러한 현상을 지양하려면 "promise"라는 비동기 작업을 도와주는 객체를 사용해야 한다.
promise
promise 객체는 date 객체처럼 특수한 목적을 위해 존재하는 자바스크립트 내장 객체이다.
비동기 작업을 실행하고 그 결과를 처리하는 코드를 좀 더 효율적이고 좀 더 편하게 작성할 수 있도록 도와준다.
Promise의 3가지 상태
1. Pending 상태
아직 비동기 작업이 진행 중이, 그래서 완료되지 않은 상태를 의미합니다.
2. Fulfiled 상태
비동기 작업이 별 다른 오류 없이 성공적으로 마무리된 상태를 의미
3. Rejected 상태
비동기 작업이 모종의 이유로 네트워크 에러, 개발자 오류 등으로 실패된 상태를 의미한다.
promise 객체는 이런 세 가지의 상태를 통해서 비동기 작업을 관리합니다.
이때 추가로 어떠한 비동기 작업이 이렇게 대기 상태였다가, 작업이 성공적으로 완료되어서
성공 상태로 바뀌는 걸 Resolve 되었다고 표현합니다.
const promise = new Promise((resolve, reject) => {
const num = null;
if(typeof num === "number"){
resolve(num + 10);
} else {
reject("num이 숫자가 아닙니다");
}
}, 2000);
});
promise.then((value) => { // then 메서드 -> 그 후에
console.log(value);
});
promise.catch((error) => {
console.log(error);
});
위 코드에서 then은 자바스크립트의 Promise 객체와 함께 사용되는 메서드로,
비동기 작업이 완료된 후에 실행될 코드를 정의하는 데 사용됩니다.
then 메서드는 프로미스가 성공적으로 이행되었을 때만 호출되는 메서드이다.
그렇기 때문에 reject 상황에서는 then이 아닌 promise의 catch라는 메서드를 사용하면 됩니다.
이 catch는 실패 버전의 then 메서드와 같은데요 그래서 똑같이 인수로 callback 함수를 전달해 주시면
promise가 실패했을 때, 등록한 callback 함수를 실행시켜 주게 됩니다.
그리고 매개변수로 then 메서드와 마찬가지로 결과 값까지 제공해 줍니다.
Promise 객체를 이용할 땐 then과 catch 메서드를 잘 활용하면 promise가 관리하는
비동기 작업이 성공하거나 실패했을 때, 그 결과 값을 우리가 이용할 수 있습니다.
const promise = new Promise((resolve, reject) => {
const num = null;
if(typeof num === "number"){
resolve(num + 10);
} else {
reject("num이 숫자가 아닙니다");
}
}, 2000);
});
promise.then((value) => { // then 메서드 -> 그 후에
console.log(value);
}).catch((error) => {
console.log(error);
});
또한 promise의 then 메서드는 promise를 한 번 다시 반환합니다.
then 메서드의 호출 결과가 promise를 반환한다는 것입니다.
그렇기 때문에 catch메서드를 호출하고 있는 promise나
then 메서드가 호출하고 있는 promise나 똑같은 promise 객체라는 것이다.
그렇기 때문에 따로 catch 메서드를 호출하지 않고 서로 연결해서 사용할 수 있습니다.
3. async / await
promise 객체를 좀 더 직관적이고 편하게 사용할 수 있도록 도와주는 async와 await라는 기능에
대해서 알아보도록 하겠습니다.
async란 함수 앞에 붙이는 키워드입니다.
어떤 키워드이냐면 어떤 함수를 비동기 함수로 만들어주는 키워드입니다.
이걸 다른 말로 하면 함수가 promise를 반환하도록 변환해 주는 키워드라고 할 수 있습니다.
// get data 함수를 선언
// 이 함수는 서버로 부터 유저의 데이터를 받아오는 함수라고 가정하였을 때
function getDate() {
return {
name: "김",
id: "serin",
};
}
// 만약 function 앞에 async가 있다면, 비동기 함수로 변환 됩니다.
// 객체를 그대로 반환하는 함수가 아니라 이 객체를 결과값으로 갖는
// 새로운 promise를 반환하는 함수로 변환된다는 것 입니다.
async function getDate() {
return {
name: "김",
id: "serin",
};
}
실제로 async를 붙인 함수를 확인하기 위해서 console.log를 통해 알아보면
promise 객체가 반환이 되는 것을 확인할 수 있고, 이때의 promise state는 성공 상태 그리고 promise result 결괏값은
반환하도록 설정한 객체 name: "김", id: "serin"이 promise의 결괏값으로 들어간 것을 확인할 수 있습니다.
// 만약 async가 붙어 있는 함수가 이렇게 일반적으로 객체 값을 반환하는게 아니라
// return new promise라고 해서 이렇게 promise를 반환하는 함수였다면
// 이 promise 객체 자체를 반환하도록 내버려둡니다.
async function getDate() {
return new Promise((resolve, reject)=>{
setTimeout(() => {
resolve({
name:"김",
id:"serin",
});
}, 1500);
});
}
// 위와 같이 애초에 promise 를 반환하고 있는 비동기 함수 였다면
// 그 때에는 async 키워드가 별 다른 일을 하지 않고 promise가 반환되도록 내버려둔다.
이런 async는 두 번째로 살펴볼 await이라는 키워드와 함께 사용했을 때
진정한 기능을 보여줄 수 있습니다.
await라는 키워드는 async 함수 내부에서만 사용할 수 있는 키워드입니다.
이때 async 함수라는 건 이렇게 async라는 키워드가 붙은 함수를 의미합니다.
그래서 async 함수 내부에서만 사용이 가능한 키워드로 비동기 함수가 다 처리되기를
기다리는 역할을 하는 그런 키워드입니다.
async function getDate() {
return new Promise((resolve, reject)=>{
setTimeout(() => {
resolve({
name:"김",
id:"serin",
});
}, 1500);
});
}
// await
// 아래 선언된 get data 함수를 호출하고 이 get data 함수가 반환되는 promise에 담겨있는 결과값을
// 사용해야한다면 원래 같으면 "then" 메서드를 사용했어야 했는데
function printData() {
getData()".then((result) => {
console.log(reuslt);
}");
}
printData();
// 이 호출의 결과가 promise를 반환하니까 여기에 then 메서드를 붙여서
// result 매개변수로 받아와서
// print data를 호출해보면 1.5초 정도 뒤에 result가 출력된다
위 코드에서는 then 메서드를 복잡하게 쓰고 있는 것을 확인할 수 있다.
하지만 이번에 학습하는 async와 await를 이용하면 더 이상 이렇게 then 메서드를 복잡하게 사용할 필요가 없다.
async function getDate() {
return new Promise((resolve, reject)=>{
setTimeout(() => {
resolve({
name:"김",
id:"serin",
});
}, 1500);
});
}
function printData() {
const data = await getData();
}
printData();
// 함수의 호출 앞에 await이라는 키워드를 붙여주게 되면 then 메서드를 쓰지 않아도 알아서
// get data 함수가 반환하는 이 promise가 종료되기를 기다린다.
위와 같이 코드를 작성하면 const data는 await getData() 이런 식으로 그냥 일반적으로 함수를 호출하듯이
작성해 주면 data라는 변수에 getData 함수가 반환하는 promise의 비동기 작업이 종료되기까지 기다렸다가
종료가 되면 결과 값을 data에 넣어주는 기능을 하게 된다.
따라서 async와 await를 이용하면 비동기 작업을, 마치 동기 작업을 처리하듯이 아주 간결한 코드로 수행할 수 있다.