서버와 통신을 배우며, 비동기 과정을 학습하여 javascript가 동작하는 순서를 익히는 것도 필요해졌다.
가장먼저 callback 함수라는 것의 이론정리가 필요했다.
JS엔진은 기본적으로 함수를 호이스팅, 출력, 호이스팅 된 함수 출력, 서버요청, 출력 이런방식으로 진행된다.
console.log('1');
// setTimeout(function(){
// console.log(2);
// }, 1000)
setTimeout(()=>console.log('2'), 1000);
console.log('3');
*화살표 함수는 원칙적으로 호이스팅 되지않는다. setTimeout이 비동기적으로 작동하기 때문에 event queue에서 대기하게 되는데 이 부분이 화살표 함수가 호이스팅 된 탓인지 착각할 수 있다.
위의 함수는 1과 3이 출력된 이후 1초를 기다리고 2를 출력하게된다.
콜백 함수는 요청하고, 받아오는 것
서버나 브라우저에 값을 요청하고 그 값을 어떻게 받을 것인지를 정한다.
// function printGameName(print){
// print();
// }
const printGameName = (print) => print();
printGameName(() => console.log('Reverse1999'));
다음의 코드는 서버에 요청하고 동기처리를 거쳐 Javascript에 기본 규칙인 위에서 아래로 실행되는 순서에 따라 출력한다.
setTimeout(()=>console.log('Godgame'), 1000);
다음의 코드는 1000ms이라는 시간을 지정 한 뒤 메시지를 출력한다. 와 같은 함수들은 동기 처리가 된 이후 순서를 기다리게 되는 것이다.
그런데 만약 비동기 처리가 된 함수가 많다면 매번 출력되는 순서를 각각의 시간에 설정해서 두어야 하는 것일까.
그럴 수도 있지만, 그를 위한 콜백함수가 따로 존재한다.
콜백함수의 존재의의는 비동기 처리가 끝난 뒤, 즉 비동기 함수가 포함된 코드를 순서에 맞게 처리 할 수 있도록 보장해줄 수 있다.
class UserStorage {
loginUser(id, password, onSuccess, onError) {
setTimeout(() => {
if (
(id === 'sotheby' && password === 'potion') ||
(id === 'sonetto' && password === 'support')
) {
onSuccess(id);
} else {
onError(new Error('not found'));
}
}, 2000)
}
getRoles(user, onSuccess, onError) {
setTimeout(() => {
if (user === 'sotheby') {
onSuccess({ name: 'sotheby', role: 'healer' });
} else {
onError(new Error('no access'));
}
}, 1000);
}
}
const userStorage = new UserStorage();
const id = prompt('enter your id');
const password = prompt('enter your password');
userStorage.loginUser(
id,
password,
user => {
userStorage.getRoles(
user,
userWithRole => {
alert(`hello ${userWithRole.name}, you have a ${userWithRole.role} role`);
},
error => {
console.log(error);
}
);
},
error => {
console.log(error);
}
);
id와 password를 주고 로그인 시, 아이디와 역할을 알림창으로 띄워주는 매커니즘을 만들었다고 할 때,
성공하면 onSuccess 를 실행하고, 실패시 onError와 error를 띄우는 문장이다.
이러한 과정은 비동기로 이루어지며, 누적될수록 문장이 굉장히 길어지는 단점이 있다.
이 부분은 promise 오브젝트를 이용하면 조금 더 간략하게 표현 할 수 있다.
class UserStorage {
loginUser(id, password) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (
(id === 'sotheby' && password === 'potion') ||
(id === 'sonetto' && password === 'vertin')
) {
resolve(id);
} else {
reject(new Error('not found'));
}
}, 2000)
});
}
getRoles(user) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (user === 'sotheby') {
resolve({ name: 'sotheby', role: 'healer' });
} else {
reject(new Error('no access'));
}
}, 1000);
})
}
}
const userStorage = new UserStorage();
const id = prompt('enter your id');
const password = prompt('enter your password');
userStorage.loginUser(id, password)
.then(userStorage.getRoles)
.then(user => alert(`hello ${user.name}, you have a ${user.role} role`))
.catch(console.log)
가장먼저 onSucces, onError, userWithRole 부분이 사라졌다.
promise를 이용하면 자체적으로 resolve와 reject로 구현이 되므로 따로 선언할 필요가 없어지는 것이다.
그에 맞는 then과 catch부분도 존재한다.
resolve일 때 then을 실행하고, reject일때 catch로 어떻게 처리할 지를 정하는 것이다.
promise 체이닝시 (연속으로 promise 데이터를 받아오는 과정을 의미한다.) 인자가 같은 경우 생략이 가능한데
위의 코드에서는 다음의 문장이 축소되었다.
.then(userStorage.getRoles)
//.then (user => userStorage.getRoles)
getRoles는 이미 함수이며 ( promise를 가지고 있다.) 이 getRoles가 호출되기 위해서는 loginUser의 resolve를 받아야 한다.
따라서 getRoles는 이미 user를 받고 있으므로 적지 않고 생략해줘도 무관하다.
마찬가지로
.then(alert(`hello ${user.name}, you have a ${user.role} role`))
//.then(user => alert(`hello ${user.name}, you have a ${user.role} role`))
이렇게 생략도 가능하지만, 호이스팅이 되어 실행될 때 getRoles 함수 부분이 먼저 실행이 '완료' 되어있어야 하므로 오류가 발생할 수 있다. 따라서 이 부분은 한 번 더 명시해 사용하게 된다.
위의 promise 또한 async , await 으로 정리가 가능한데, 에이싱크, 어웨잇 으로 읽는다 (어싱크 ㄴㄴ)
promise를 선언하지 않고 함수 앞에 async를 붙이는 것 만으로도 해결이 된다.
function delay(ms){
return new Promise(resolve => setTimeout(resolve, ms));
}
async function getcleardrop() {
await delay(2000);
return '☔';
}
async function getdust() {
await delay(2000);
return '🪙';
}
async function levelup() {
const rain = await getcleardrop();
const dust = await getdust();
return `${rain} + ${dust}`;
}
levelup().then(console.log);
delay라는 함수를 선언하고
async 함수를 선언하면 자동으로 promise로 적용이 된다.
await는 async를 사용중인 함수 내에서만 사용이 가능한데, 뒤에 선언된 시간만큼 (즉, promise가 끝날 때까지) 기다려준 뒤 그 다음 문장을 실행하게 된다.
그런데, 다음과 같은 문장에서 await가 중복되게 되면, 받아오는 시간동안 순차적으로 기다리게 된다.
(즉 getcleardrop과 getdust가 서로 delay 2000ms만큼을 각각 기다린 뒤 실행되게 된다)
그때, 각 promise가 서로에게 영향을 주는 것이 없다면 병렬적으로 실행한다면 더욱 빠르게 데이터를 받아올 수 있는데
다음과 같은 함수를 사용하게 된다.
function getlevelup(){
return Promise.all([getcleardrop(), getdust()])
.then(level => level.join(' + '));
}
levelup().then(console.log);
Promise.all 같은 경우는 promise를 배열에 넣어 모두 병렬적으로 실행해서 모든 값을 받아두고,
배열을 문자열로 묶는 join이라는 메서드를 이용해서 묶어주게 되어도 같은 값이 된다.
다만 이 과정에서 순차적으로 실행 후, 정렬하는 것과 병렬적으로 실행 후 정렬하는것의 차이는 거의 나지 않았다.
이유는 요소가 2개인 탓에 각각 2000ms 만 기다리면 되기 때문이다. 하지만 추후 더 많은 작업을 한 번에 진행하게 될 때는
확실하게 Promise.all 통해 진행하는 것이 좋다고 하였으니, 이 부분이 체감이 될 때 다시 적게 될 것 같다.
'JavaScript' 카테고리의 다른 글
2023.12.27 기록 (foreach와 map, 그리고 firebase) (1) | 2023.12.27 |
---|---|
2023.12.26 기록 (firebase hosting) (0) | 2023.12.26 |
2023.12.23 기록 (JS 객체와 배열 1. 생성자 함수) (1) | 2023.12.23 |
2023.12.22 기록 (firebase 관련) (1) | 2023.12.22 |
2023.12.20 기록 (객체와 prototype, 그리고 includes) (0) | 2023.12.20 |