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);