[JavaScript] 비동기,동기,Promise,async 공부하기
in JavaScript on Study
1️⃣ 목표
- 동기와 비동기 프로그래밍 이해
- 콜백 함수를 이용하여 비동기를 처리하여 사용하기
- Promise사용법 배우기
- Promise의 비동기기능을 사용해보기(login폼)
- async사용법 배우기
2️⃣ 동기와 비동기
(1) 동기(Synchronous)
- 동기형 함수들만 있으면 단순히 작성된 순서로 코드가 실행됩니다.
console.log(1);
console.log(2);
console.log(3);
2
3
(2) 비동기(Asynchronous)
- 만약
하나의 함수 가 처리하는데 시간이 오래걸린다면 그다음번 코드는 끝날때까지 대기 하게 됩니다. - 실제로 서버에서 클라이언트의 입출력(IO)에서 지연이 많이 발생한다고 합니다. 만약 클라이언트의 입출력 빈도가 더 늘어난다면
서버 는 무한대기상태에 빠지게 될 것입니다. - 이때 비동기프로그래밍을 이용하면 좀 더 효율적으로 처리할 수 있습니다. (예를들어 멀티쓰레드 방식으로 동시에 처리)
- 하지만 자바스크립트는 싱글쓰레드로 처리합니다. 그대신에 웹 브라우저에서 제공하는 API를 이용하면 웹에 있는 js엔진에서
비동기적 으로 처리해줍니다. - 웹API의 대표적인 예로
setTimeout()
콜백 함수가 있습니다.
console.log(1);
setTimeout(() => console.log(2), 1000);
console.log(3);
3
2
3️⃣ call back 함수로 비동기 처리하기
(1) 비동기를 동기 프로그래밍 처리하기
- 하지만 비동기로 처리된 후의 값이 필요한 경우에는 동기적으로 처리해줄 수 밖에 없습니다.
- 이때 콜백함수를 이용하면되는데
처리를 실패 했을때 처리도 고려하여 코드를 구성하여야 합니다. - 간단한 예시로 Popcorn메이커를 만들어 봤습니다.
(2) call back함수 예시(Popcorn메이커)
class Cooker {
inspectFood(food, available, onError) {
console.log("getting food...");
setTimeout(() => {
if (food === "🌽" || food === "🥔") {
available(food);
} else {
onError(new Error("no food available"));
}
}, 2000);
}
makePopcorn(food, available, onError) {
console.log("cooking...");
setTimeout(() => {
if (food === "🌽") {
available(`${food} => 🍿`);
} else {
onError(new Error(`${food} => ❌`));
}
}, 2000);
}
}
- 위의 코드는 다음의 메소드를 가진 Cooker 클래스입니다.
- 식재료를 유효한 식재료인지 검사하는
inspectFood()
메소드 - 식재료로 팝콘을 만드는
makePopcorn()
메소드
- 식재료를 유효한 식재료인지 검사하는
(3) Popcorn메이커(call back) 사용 해보기
const cooker = new Cooker();
const CORN = "🌽";
const POTATO = "🥔";
const APPLE = "🍎";
const food = CORN; // 사용자가 임의의 음식 설정
cooker.inspectFood(
food,
(availableFood) => {
cooker.makePopcorn(
availableFood,
(availablePopcorn) => {
console.log(availablePopcorn);
},
(error) => {
console.log(error);
}
);
},
(error) => {
console.log(error);
}
);
- 위에서 🌽 와 🥔 의 경우 출력을 보면 재료를 검사할때까지 기다린 후 요리를 시작 합니다.
(4) Popcorn메이커(call back)의 단점
- 위에서 파악한 Popcorn메이커는 사용하는데 있어서 가독성이 너무 떨어집니다. (callback함수의 중첩…)
- Popcorn메이커는 콜백함수가 겨우
2단계 로만 사용됐는데도 불구하고 코드를 읽기가 쉽지않습니다. 대규모 프로젝트에서는 비동기 처리가 좀 더 깊게 구현할때가 있을 텐데 만약 이런식으로 콜백함수를 사용한다면 코드를 알아보기는 커녕유지보수 조차 할 수 없을 것 입니다.
4️⃣ Promise
(1) Promise기본사용
- Promise를 사용하면 콜백함수보다 더 깔끔하게 호출하여 사용할 수 있습니다.
/* 방법 1 */
sampleFunc(param) {
return new Promise((resolve, reject) => {
/* 비동기함수 생략 */
/* 성공시 */
resolve(a);
/* 실패시 */
reject(b);
})
}
/* 방법 2 */
const sample = new Promise((resolve, reject) => {
/* 생략 */
})
- Promise는 Javascript에서 기본적으로 제공하는 오브젝트입니다. 위처럼 두가지방법으로 사용이 가능한데 방법 2의 방법은 비추천하는 선언방법 입니다. 그 이유는 방법 2는 선언과 동시에 Promise가 호출되어 실행되기 때문입니다. (만약 홈페이지를 열때마다 promise의 비동기과정이 일어나면서 데이터를 읽어오면 매우 비효율적이기 때문)
- Promise는 비동기 진행 중 일때는 pending(대기)상태에 있다가 비동기 처리가 완료되면 fulfilled(이행)상태로 되고 비동기 처리가 실패하면 rejected(실패)상태가 됩니다.
- 간단하게 처리성공시 resolve를 처리실패시 reject콜백함수를 줍니다.
- 어떻게보면 콜백함수로 만든 것과 별차이가 없어보이지만
consumer(사용) 하는 방법에서 큰차이가 나며 Promise가 압도적으로 깔끔합니다.
(2) Popcorn메이커 Promise로 변환하기
class Cooker {
inspectFood(food) {
return new Promise((resolve, reject) => {
console.log("getting food...");
setTimeout(() => {
if (food === "🌽" || food === "🥔") {
resolve(food);
} else {
reject(new Error("no food available"));
}
}, 1000);
});
}
makePopcorn(food) {
return new Promise((resolve, reject) => {
console.log("cooking...");
setTimeout(() => {
if (food === "🌽") {
resolve(`${food} => 🍿`);
} else {
reject(new Error(`${food} => ❌`));
}
}, 1000);
});
}
}
- 콜백함수와 복잡도면에서는 별차이가 없어보입니다. 이번엔 사용(consumer)코드를 보겠습니다.
< 콜백함수 consumers>
< 프로미스 consumers>
- 2단계구성된 비동기처리지만 프로미스(promise)가 눈에 띄게 깔끔함을 알 수 있습니다.
- 프로미스(Promise)는 Promise 오브젝트를 반환하기 때문에 연달아서
then, catch, finally
와 같은 Promise관련 메소드를 사용할 수 있습니다. - 먼저 then은 앞선처리가 성공하면 호출됩니다.
- catch는
reject
를 감지하여에러값을 처리 합니다. - .finally는 성공유무를 떠나서 반드시 호출됩니다. (try-catch구문에서 쓰임과 비슷)
5️⃣ Promise(비동기 함수) 보충
(1) Promise 의미 다시 생각해보기
- Popcorn메이커는
비동기적 처리 에 대한 좋은 예시가 아닌 것같습니다. 단순히 Promise로 구현한 것이 callback함수로 작성한 것보다 사용하는데 있어서 깔끔한 것을 보여주는 예시일 뿐입니다. - 사실 Promise를 우리말로 직역하면 “약속”이라는 뜻으로 “내가
비동기 적으로 처리할태니 너는하던거해, 대신에 나중에 반환값을 줄 것을 약속할게!”로 이해할 수 있습니다. - 위에서도 언급했듯이 Promise가 가질 수 있는 상태는 pending(대기), fulfilled(이행), rejected(실패) 세가지 입니다. resolve, reject를 정의해주지 않으면 pending상태에 있을것이며, 성공유무에 따라 fulfilled나 rejected상태가 될 것입니다.
- 하지만 이런식으로 상태를 가질 수 있게하려면 Promisee를 선언해주어야합니다. 대신 이 곳 에서 말한 이유로 함수안에서 promise를 선언하여 사용해야합니다.
(2) 로그인폼 예시
- promise를 의미있게 사용하는 예시를 보여주기 위해 로그인폼을 만들어 봤습니다.
- 아래 코드처럼
사용자 정보 를 읽어오는 함수로써 promise를 사용했습니다. (실제 데이터를 읽어오는 것처럼만 흉내)
function login(id, password) {
return new Promise((resolve, reject) => {
if (id === "kirkim" && password === "111") {
setTimeout(() => {
resolve(`<사용자 정보>\n아이디: ${id}\n이름: 김기림\n국적: 대한민국`);
}, 4000);
} else {
reject(new Error(`login fail!`));
}
});
}
- 작동방식은 다음과 같습니다.
- ID와 Password를 입력하여 로그인
- 유효한 로그인이면 사이트가 접속과 동시에 Promise 함수를 호출하여 정보를 비동기적으로 읽도록함
- 정보를 읽어오면 “logout”, “정보보기”버튼이 생성됨
- “정보보기”버튼을 누르면 Promise 함수로 읽어온 데이터를 바로 볼 수 있습니다.
👉🏻 (ID: kirkim, PW: 111)
6️⃣ Async
(1) Async사용
function sampleFunc() {
return new Promise((resolve, reject) => {
resolve("success);
})
}
async function sampleFunc() {
return "success";
}
function
앞에"async" 를 붙이면 자동으로 promise처럼 동작하게 만들어줍니다.
async function s1() {
/* 코드 생략 */
}
async function s2() {
/* 코드 생략 */
}
async function sampleFunc() {
try (
const value1 = await s1();
const value2 = await s2();
) catch (err) {
throw (err);
}
}
await
를 붙이면 비동기함수가 완료될때까지 기다려줍니다. 단,async 가 붙은 함수 내부에서만 사용이 가능합니다.- 결론적으로 위와같이 비동기함수를 동기함수처럼 작성하여 사용할 수 있습니다.
- 오류같은경우
try, catch
구문을 이용하면 됩니다. (더욱이 try-catch식의 코드가 좀더 익숙하기까지함)
(2) Async가 필요할 때?
async 를 사용하면 비동기함수를 평소에 익숙한식으로 코드를 작성하여 사용할 수 있습니다.- 만약 Promise함수를 연달아서 사용된다면
then
이 연속적으로 오기 때문에 코드가 지저분해질 수 있습니다. - 다음의 극단적인 예로 Promise와 async를 비교해 보겠습니다.
< promise >
< async >
- 위는 promise, async에 맞게 함수들을 구현해준 모습입니다. 이 함수들은 어디까지나 임시로 구현한 것이기 때문에
작성 방법 정도만 비교하면 될 것 같습니다. - 중점적으로
비교해야될 부분은 위의 비동기함수들을 최종적으로 사용한 다음의 코드들입니다.
< promise >
< async >
- 두코드 모두 비동기함수들이 연쇄적으로 이어진거의 똑같이 동작하는 코드입니다.
- 한눈에봐도 async를 이용한 쪽이 사용하기가 더 깔끔합니다.
- 그러나 이 예시는 async의 사용이 유리할 경우일 뿐이고, 분명 promise를 사용하는게 더 좋은 경우도 있기 때문에 “상황에 따라 잘 선택해서 사용하는 것이 중요”합니다.
(3) async 비동기적으로 사용하기
- async도 미리 선언시켜놓는 방법으로 사용하면 비동기적으로 사용할 수 있습니다.
function sleep(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms);
});
}
async function func1() {
await sleep(1000);
return `1`;
}
async function func2(a) {
await sleep(1000);
return `2`;
}
async function func3() {
await sleep(1000);
return `3`;
}
async function func4() {
await sleep(1000);
return `4`;
}
async function usePromiseChain() {
const a1 = func1();
const a2 = func2();
const a3 = func3();
const a4 = func4();
const c1 = await a1;
const c2 = await a2;
const c3 = await a3;
const c4 = await a4;
console.log(c1, c2, c3, c4);
}
usePromiseChain();
- 위와 같이
a
변수들에async 함수들을 미리 선언하여 사용했습니다. (선언과 동시에 비동기처리 시작) - 주의할 점은 동기적으로 사용하기 이전에
await
로 비동기과정을 기다려주는 작업이 필요합니다. (console.log로 출력하기 이전에 c1,c2,c3,c4 데이터들을 읽어올때까지 기다려야함) - 만약 비동기작업들이 서로 연관된게 아니라면 위의 경우처럼 미리 선언하여 사용하는 것이 좋고 그것이 “비동기프로그래밍을 하는 이유”라고 생각합니다.