
JavaScript의 실행 컨텍스트는 동기적(synchronous)이며, 코드는 작성된 순서대로 실행된다. 그러나 JavaScript는 비동기적(asynchronous) 처리를 지원하는 몇 가지 기능을 제공한다.
비동기적 처리는 특정 코드를 실행 완료를 기다리지 않도 다음 코드를 실행하는 방식을 말한다. 예를 들어, 웹 어플리케이션에서 서버로 데이터를 요청하면서 동시에 다른 작업을 계속 진행하는 것이 비동기 처리의 한 예시다.
console.log('1');
console.log('2');
console.log('3');
console.log('1');
setTimeout(() => console.log('2'), 1000); // 1초 후 '2' 출력
console.log('3'); // 즉시 '3' 출력
콜백 함수는 다른 함수에 인자로 넘겨지는 함수로, 그 함수의 실행이 끝난 후 실행된다. 콜백 함수는 주로 비동기 작업의 완료 후 필요한 작업을 수행하기 위해 사용된다.
function printImmediately(print) {
print();
}
printImmediately(() => console.log('hello')); // 즉시 'hello' 출력
function printWithDelay(print, timeout) {
setTimeout(print, timeout);
}
printWithDelay(() => console.log('async callback'), 2000); // 2초 후 'async callback' 출력
콜백 지옥은 콜백 함수를 과도하게 중첩 사용하여 코드의 가독성과 유지 보수성이 떨어지는 상황을 말한다. 코드가 계단처럼 보이며, 각 단계마다 중첩된 콜백 함수가 존재한다.
class UserStorage {
loginUser(id, password, onSuccess, onError) {
setTimeout(() => {
if (
(id === 'ellie' && password === 'dream') ||
(id === 'coder' && password === 'academy')
) {
onSuccess(id);
} else {
onError(new Error('not found'));
}
}, 2000);
}
getRoles(user, onSuccess, onError) {
setTimeout(() => {
if (user === 'ellie') {
onSuccess({ name: 'ellie', role: 'admin' });
} else {
onError(new Error('no access'));
}
}, 1000);
}
}
const userStorage = new UserStorage();
const id = prompt('enter your id');
const password = prompt('enter your passrod');
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);
}
);
위 코드는 먼저 사용자 로그인을 시도하고(loginUser), 그 후 사용자 역할을 가져오는 (getRoles) 과정을 거친다. 각 단계마다 콜백 함수를 사용하여 다음 단계를 실행한다. 이러한 중첩은 코드의 복잡성을 증가시키고, 에러 처리와 유지 보수를 어렵게 만든다.
Promise는 JavaScript에서 비동기 작업을 편리하게 처리할 수 있도록 하는 객체다. 주로 네트워크 요청이나 파일 읽기와 같이 시간이 걸리는 작업을 처리할 때 사용된다. Promise는 다음 세 가지 상태를 가진다.
Promise 객체는 new Promise로 생성되며, 이 때 실행자(executor) 함수가 자동으로 실행된다. 이 함수는 resolve와 reject 두 가지 인자를 받는다.
const promise = new Promise((resolve, reject) => {
// doing some heavy work (network, read files)
console.log('doing something...');
setTimeout(() => {
resolve('ellie');
// reject(new Error('no network'));
}, 2000);
});
setTimeout은 2초 후에 resolve 함수를 호출하여 Promise를 이행(fulfilled) 상태로 만든다. resolved가 호출되면 Promise는 성공적으로 결과 값을 가진 상태가 된다.reject를 호출하면 Promise는 실패하여 거부(rejected) 상태가 된다.then이 호출된다. then은 Promise가 반환한 데이터를 받아 처리한다.catch가 호출된다. catch는 오류를 받아 처리한다.promise
.then(value => {
console.log(value); // 'ellie'
})
.catch(error => {
console.log(error);
})
.finally(() => {
console.log('finally');
});
여러 개의 Promise를 연결하여 순차적으로 처리할 수 있다.
const fetchNumber = new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000);
});
fetchNumber
.then(num => num * 2)
.then(num => num * 3)
.then(num => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(num - 1), 1000);
});
})
.then(num => console.log(num)); // 5
.catch()를 사용하여 Promise 체인 중 발생한 오류를 캐치하고 처리할 수 있다.
const getHen = () => new Promise((resolve, reject) => {
setTimeout(() => resolve('🐓'), 1000);
});
const getEgg = hen => new Promise((resolve, reject) => {
setTimeout(() => reject(new Error(`error! ${hen} => 🥚`)), 1000);
});
const cook = egg => new Promise((resolve, reject) => {
setTimeout(() => resolve(`${egg} => 🍳`), 1000);
});
getHen()
.then(getEgg)
.catch(error => {
return '🍞';
})
.then(cook)
.then(console.log)
.catch(console.log);
여기서 getEgg에서 오류가 발생하면 catch가 실행되어 '🍞'을 반환하고, cook은 이를 받아 계란 대신 빵을 요리한다.
async와 await은 JavaScript에서 비동기 작업을 간결하고 명확하게 표현할 수 있는 문법이다. 이들은 Promise를 더 쉽게 사용할 수 있도록 도와준다.
async 키워드를 함수 앞에 사용하면, 해당 함수는 항상 Promise를 반환한다.Promise로 감싸진 값으로 변환된다.async function fetchUser() {
return 'ellie'; // Promise로 감싸진 'ellie' 반환
}
const user = fetchUser();
user.then(console.log); // ellie
await 키워드는 async 함수 내부에서만 사용할 수 있다.await는 Promise가 처리될 때까지 함수 실행을 일시 중지하고, Promise의 결과 값을 반환한다.function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function getApple() {
await delay(2000); // 2초 기다림
return '🍎';
}
async function getBanana() {
await delay(1000); // 1초 기다림
return '🍌';
}
async function pickFruits() {
const apple = await getApple();
const banana = await getBanana();
return `${apple} + ${banana}`;
}
pickFruits().then(console.log); // 🍎 + 🍌
try...catch: async 함수 내에서 await 키워드를 사용할 때, try...catch 구분을 활용하여 동기 코드에서의 오류 처리와 유사하게 오류 처리를 할 수 있다.fianlly: finally 블록은 try...catch와 함께 사용되어, 성공이든 실패든 상관없이 특정 코드를 실행할 수 있게 한다. 예를 들어, 로딩 상태를 관리할 때 유용할 수 있습니다.async function fetchData() {
try {
const data = await fetchSomeData();
console.log(data);
} catch (error) {
console.error('오류 발생:', error);
} finally {
console.log('데이터 요청 완료'); // 항상 실행
}
}
주의 사항
await 키워드를 사용하는 모든 비동기 작업은 오류가 발생할 가능성이 있으므로, try...catch 구문으로 감싸는 것이 좋다.try...catch 블록으로 감쌀 수도 있고, 전체 작업을 하나의 try...catch 블록으로 감싸서 오류를 한 곳에서 처리할 수도 있다.promise.all: 여러 Promise를 병렬로 처리하고, 모든 Promise가 완료되면 결과를 배열로 반환한다.function pickAllFruits() {
return Promise.all([getApple(), getBanana()]).then(fruits =>
fruits.join(' + ')
);
}
pickAllFruits().then(console.log); // 🍎 + 🍌
Promise
Promise.race: 여러 Promise 중 가장 먼저 완료되는 하나의 결과만 반환한다.function pickOnlyOne() {
return Promise.race([getApple(), getBanana()]);
}
pickOnlyOne().then(console.log); // 🍌 또는 🍎 (둘 중 빠른 것)
위의 콜백 지옥의 예를 async와 await으로 해결해보자.
class UserStorage {
loginUser(id, password) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (
(id === 'ellie' && password === 'dream') ||
(id === 'coder' && password === 'academy')
) {
resolve(id);
} else {
reject(new Error('not found'));
}
}, 2000);
});
}
getRoles(user) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (user === 'ellie') {
resolve({ name: 'ellie', role: 'admin' });
} else {
reject(new Error('no access'));
}
}, 1000);
});
}
async getUserWithRole(user, password) {
const id = await this.loginUser(user, password);
const role = await this.getRoles(id);
return role;
}
}
const userStorage = new UserStorage();
const id = prompt('enter your id');
const password = prompt('enter your passrod');
userStorage
.loginUser(id, password)
.then(userStorage.getRoles)
.then(user => alert(`Hello ${user.name}, you have a ${user.role} role`))
.catch(console.log);
userStorage
.getUserWithRole(id, password) //
.catch(console.log)
.then(console.log);