드림코딩 by 엘리 : 자바스크립트 기초 강의 (ES5+)
JavaScript는 동기적(synchronous)이다.
호이스팅이 된 이후부터 코드가 우리가 작성한 순서에 맞춰서 하나하나씩 동기적으로 실행이 된다.
hoisting
- 인터프리터가 변수와 함수의 메모리 공간을 선언 전에 미리 할당하는 것을 의미
var
로 선언한 변수의 경우 호이스팅 시undefined
로 변수를 초기화 한다.- 반면
let
과const
로 선언한 변수의 경우 호이스팅 시 변수를 초기화하지 않는다.function declaration
도 호이스팅 된다.
비동기적(Asynchronous)란 동기(synchronous)와 다르게 언제 실행될 지 예측할 수 없는 코드들을 말한다.
JavaScript엔진은 위에서 아래로부터 순차적(동기)으로 실행하게 된다.
setTimeout
은 대표적인 비동기 함수로 콜백함수를 호출한다. 단, 브라우저는 setTimeout
이 끝날때까지 기다리지 않는다.
때문에 결과는 1→3→2로 실행되며 이것이 비동기적 실행이다.
console.log("1"); // 동기
setTimeout(() => console.log("2"), 1000); // 비동기
console.log("3"); // 동기
콜백은 2가지 경우로 나뉜다.
console.log("1"); // 동기
setTimeout(() => console.log("2"), 1000); // 비동기
console.log("3"); // 동기
// Synchronous callback
function printImmediately(print) {
print();
}
printImmediately(() => console.log("hello")); // 동기
console.log("1"); // 동기
setTimeout(() => console.log("2"), 1000); // 비동기
console.log("3"); // 동기
// Synchronous callback
function printImmediately(print) {
print();
}
printImmediately(() => console.log("hello")); // 동기
// Asynchronous callback
function printWithDelay(print, timeout) {
setTimeout(print, timeout);
}
printWithDelay(() => console.log("async callback"), 2000); // 비동기
함수의 선언은 호이스팅이 되어 제일 위로 올라감 → 함수 선언 → 순차적으로 실행
Synchronous
는 차례대로 실행Asynchronous
는 빠졌다가 지정된 시간에 실행
따라서 실제 JavaScript엔진은 아래와 같이 코드가 실행된다.
function printImmediately(print) {
print();
}
function printWithDelay(print, timeout) {
setTimeout(print, timeout)
}
console.log("1"); // 동기
setTimeout(() => console.log("2"), 1000); // 비동기
console.log("3"); // 동기
printImmediately(() => console.log("hello")); // 동기
printWithDelay(() => console.log("async callback"), 2000); // 비동기
- loginUser API(로그인하는 API)
- getRoles API(유저인지 관리자인지 check API)
class UserStorage {
loginUser(id, password, onSuccess, onError) {
setTimeout(() => {
if (
(id === "hashin" && password === "hello") ||
(id === "bomnal" && password === "2022")
) {
onSuccess(id);
} else {
onError(new Error("not found"));
}
}, 2000);
}
getRoles(user, onSuccess, onError) {
setTimeout(() => {
if (user === "hashin") {
onSuccess({ name: "hashin", role: "admin" });
} else {
onError(new Error("no access"));
}
}, 1000);
}
}
- Client에서 ID & Password 입력
- 입력 받아온 ID & Password로 Server에서 login(
setTimeout()
을 이용해 Server에서 login하는 것처럼 보여지게 함)- login 성공 시 받아오는 ID를 통해
getRoles
API요청- 마지막으로 API요청에 성공하면
name
과role
을 출력
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);
}
);
이런식으로 콜백함수 안에 또 콜백함수를 부르고 꼬리를 물듯이 계속 이어지면 이것을 콜백 지옥이라고 부른다.
콜백 지옥 문제점
1. 가독성이 떨어짐, business logic을 알아보기 어려움
2. 에러 발생 및 디버깅 시 어려움을 가짐, 유지보수가 어려움
✨ 이러한 콜백 지옥에서 벗어나기 위해선 Promise
와 aync + await
을 사용한다.
promise는 JavaScript에서 비동기(Asynchoronous)연산을 간편하게 처리해주는 Object이다.
Promise는 정해진 임무를 수행하고 나서 정상적으로 기능이 수행되었다면 성공의 메시지와 함께 처리된 결과값을 전달해 준다. 만약 임무를 수행하다 정상적으로 수행되지 못하면 에러를 발생시킨다.
[프로미스는 2가지 개념을 중심으로 이해]
1. State
- pending(operation수행중)
- fulfilled(operation완료) or rejected(operation문제발생)
- Producer & consumer 차이점
- Producer(원하는 데이터를 제공)
- Consumer(정보를 소비)
const promise = new Promise(executor);
promise는 class이기 때문에 new
키워드를 통해 객체를 생성한다.
promise는 executor
라는 callback함수를 전달해야 하고, 이 callback함수에는 또 다른 두 가지의 callback함수를 받는다.
resolve
: 기능을 정상적으로 수행해서 마지막에 최종 데이터를 전달reject
: 기능을 수행하다가 중간에 문제가 생기면 호출
const promise = new Promise((resolve, reject) => {
// doing some heavy work (network, read files) => asynchronous
console.log("doing something...");
setTimeout(() => {
// resolve("hashin");
reject(new Error("no network"));
}, 2000);
});
executor
가 바로 실행되고, 네트워크 통신을 수행하게 된다.executor
라는 callback 함수가 바로 실행되기 때문에 이 점에 유의하면서 코드를 작성.- then, catch, finally를 이용해서 값을 받아올 수 있다.
- then : promise operation이 성공적으로 작동했을 때, resolve에서 전달된 값 및 원하는 기능 수행
- catch : promise operation 에러가 발생했을 때, reject에서 받아온 값 수행
- finally : promise 성공 or 에러 상관없이 무조건 마지막에 호출
const promise = new Promise((resolve, reject) => {
// doing some heavy work (network, read files) => asynchronous
console.log("doing something...");
setTimeout(() => {
resolve("hashin");
}, 2000);
});
promise
.then((value) => {
console.log(value);
});
resolve
콜백함수 통해 전달된 값이 value
로 들어옴resolve
가 'hashin'라는 값을 전달const promise = new Promise((resolve, reject) => {
// doing some heavy work (network, read files) => asynchronous
console.log("doing something...");
setTimeout(() => {
reject(new Error("no network"));
}, 2000);
});
promise
.then((value) => {
console.log(value);
})
.catch((error) => {
console.log(error);
});
reject
에서 받아온 값이 error
로 들어옴catch
를 사용해서 error
발생 시 어떻게 처리할 것인지에 대한 콜백함수 등록const promise = new Promise((resolve, reject) => {
// doing some heavy work (network, read files) => asynchronous
console.log("doing something...");
setTimeout(() => {
reject(new Error("no network"));
}, 2000);
});
promise
.then((value) => {
console.log(value);
})
.catch((error) => {
console.log(error);
})
.finally(() => {
console.log("finally");
});
then
은 값을 전달할 수도 있고, 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));
catch
메서드를 통해 에러를 받아올 수 있다.then
메서드에서 에러가 발생하면 값을 받아온 then
메서드 바로 밑에 catch
메서드를 넣음으로써 catch
메서드로 값을 반환해주며 에러 페이크 처리를 할 수 있다.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((hen) => getEgg(hen))
.catch((error) => {
return "🍗";
})
.then((egg) => cook(egg))
.then((meal) => console.log(meal))
.catch(console.log);
getHen()
.then(getEgg)
.catch((error) => {
return "🍗";
})
.then(cook)
.then(console.log)
.catch(console.log);
위의 콜백지옥 예제를 Promise로 코드를 변경한 것이다.
class UserStorage {
loginUser(id, password) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (
(id === "hashin" && password === "hello") ||
(id === "bomnal" && password === "2022")
) {
resolve(id);
} else {
reject(new Error("not found"));
}
}, 2000);
});
}
getRoles(user) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (user === "hashin") {
resolve({ name: "hashin", role: "admin" });
} 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);
aync & await은 Promise를 조금 더 간결하고 간편하고 동기적으로 실행되는 것처럼 보이게 만들어준다.
Promise를 여러 개 체인형식으로 묶을 수 있는데 Promise마다 then
을 여러 개 chaining
을 하게 되면 코드가 난잡해 질 수 있다.
async & await은 마치 동기식으로 순서대로 작성하는 것처럼 간편하게 해주는 API제공해준다.
Syntactic sugar
기존에 존재하는 것 위에 간편하게 쓸 수 있는 api를 제공하는 것 ex) Class
async & await은 promise 위에 덮혀진 Syntactic sugar이지만 그렇다고 해서 async & await만 쓰는 것이 아니라 때에 따라서 Promise를 유지해서 써야할 경우가 있으며 async await을 사용해야 더 깔끔해지는 경우도 있다.
function fetchUser() {
// do network request in 10 secs...
return "hashin";
}
const user = fetchUser();
console.log(user);
자바스크립트는 동기적으로 실행되기 때문에, 예를 들어 위의 함수처럼 네트워크 요청을 10초동안 받아오는 함수가 있다면 동기로 처리되기 때문에 fetchUser함수가 끝날때까지 기다려야하는 현상을 유발시킨다.
만약, 이 함수 뒤에 브라우저 UI를 나타내야 하는 중요한 코드들이 기다리고 있다면 브라우저는 fetchUser함수가 끝나길 기다리고 끝날 때에서야 비로소 다음 코드들이 실행되는 것을 알 수 있다.
하나의 함수 때문에 모든 코드들이 실행될 수 없는 현상이 발생하기 때문에 이렇게 요청이 긴 함수들은 반드시 비동기로 처리해야 한다.
function fetchUser() {
// do network request in 10 secs...
return new Promise((resolve, reject) => {
resolve("hashin");
})
}
const user = fetchUser();
use.then(console.log);
console.log(user);
Promise((resolve, reject)=> ...)
를 사용하지 않고 function 앞에 async를 붙이면 code block이 자동으로 promise로 바꿔준다.
async function fetchUser() {
// do network request in 10 secs...
return "hashin";
}
const user = fetchUser();
user.then(console.log);
// 결국 promise를 return하기 때문에 .then을 사용하여 출력한다.
console.log(user);
function delay
: 정해진 ms(시간)를 지나면 resolve를 호출하는 promise를 return한다.function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function getApple() {
await delay(2000);
return "🍎";
}
async function getBanana() {
await delay(1000);
return "🍌";
}
만약 위의 getBanana
함수를 Promise로 만들게 되면
function getBanana() {
return delay(3000).then(() => "🍌");
}
이렇게 chaining을 해줘야 한다.
async와 await은 Promise에서 각각 Producer(←async)와 Consumer(←await)의 역할을 한다고 볼 수 있다.
function pickFruits() {
return getApple().then(apple =>{
return getBanana().then(banana => `${apple} + ${banana}`);
});
}
pickFruits().then(console.log);
Promise도 체이닝을 여러번 하게 되면 콜백지옥과 같이 비슷한 문제점이 발생한다.
우리는 이것을 async & await로 간결하게 만들어줄 수 있다.
async function pickFruits() {
const apple = await getApple();
const banana = await getBanana();
return `${apple} + ${banana}`;
}
pickFruits().then(console.log);
동기식으로 코드를 작성해도 비동기식으로 작동하며 코드도 상당히 간결해진다.
async function pickFruits() {
try {
const apple = await getApple();
const banana = await getBanana();
return `${apple} + ${banana}`;
} catch {
// ...
} finally {
// ...
}
}
then
은 async & await에서 try
와 흡사하며 catch와 finally는 async & await에서 catch / finally
와 똑같다.async function pickFruits() {
const apple = await getApple();
const banana = await getBanana();
return `${apple} + ${banana}`;
}
pickFruits().then(console.log);
위의 코드에는 await로 getApple()
함수를 1초 기다리고 그 뒤에 getBanana()
함수를 또 1초 기다리는 문제점이 있다.
두 개의 함수는 별개이므로 서로 기다려 줄 필요가 없으니 아래처럼 코드를 짜서 해결할 수 있다.
async function pickFruits() {
// await 병렬처리
const applePromise = getApple();
const bananaPromise = getBanana();
const apple = await applePromise;
const banana = await bananaPromise;
return `${apple} + ${banana}`;
}
pickFruits().then(console.log);
이렇게 하면 apple함수와 banana함수가 동시에 실행되고 실행되자마자 Promise객체를 생성하기 때문에 병렬적으로 실행되게 된다.
위와 같이 병렬 처리를 할 때 유용한 API가 있다.
function pickAllFruits() {
return Promise.all([getApple(), getBanana()]).then((fruits) =>
fruits.join(" + ")
);
}
pickAllFruits().then(console.log);
function pickOnlyOne() {
return Promise.race([getApple(), getBanana()]);
}
pickOnlyOne().then(console.log);
-> banana가 먼저 전달 되어서 banana가 출력됨
callback이해 & callback지옥 체험
Promise개념 & 활용
Async & Await, Promise APIs