📢 어렵고 정석적인 개념 설명보다는 저같은 초보자도 이해하기 쉽게 정리하는 것을 원칙으로 포스팅하고 있습니다. 😄
들어가기 전
이 글은 자바스크립트의 비동기식 처리에 대해서 간단히 이해하는 정도의 내용만 담고 있다.
그중에서도 Promise와 async-await 문법에 대해서만 다룰 예정이며, 이것 또한 맛보기 수준의 지식만을 전달하려 한다. 비동기 처리는 아무래도 초보 개발자들 입장에서는 이해하기 어려운 부분이다. 나 역시 그렇다.
때문에, 만약 A-Z까지 제대로 된 비동기식 처리를 공부하고자 한다면 '뒤로 가기'를 누르는 것도 조심스레 추천드린다.
하지만, 필자처럼 이제 개발을 막 공부하는 입장이라면 생각하는 수준이 고만고만(?)하고 사고하는 과정이 비슷해서 오히려 도움이 될 수도 있다. (최대한 이해하기 쉽게 비유를 이용하여 설명하였다.) 나도 비동기를 이해하는 데 오래 걸린 만큼 이 글에서는 쉽게 풀어서 설명하고자 한다.
설명
비동기식이란?
console.log("1등");
setTimeout(function (){
console.log("2등)"
}, 3000);
console.log("3등");
비동기를 모르는 사람에 봤을 때, 위 코드의 결과를 '1등-2등-3등' 순서대로 나올 것이라 생각할 것이다.
하지만 setTimeout()은 대표적인 비동기식 함수이므로, 결과는 '1등-3등-2등' 순서대로 나온다.
이 코드만 이해해도 비동기가 무엇인지 이해할 수 있을 것이다. 이처럼 코드의 순서대로(위→아래) 처리되는 게 아닌 어디 방구석(백그라운드)에서 따로 자기 혼자 처리되는 게 비동기식 처리이다.
"와~ 비동기 정말 좋은데?"라고 할 수 있지만, 이런 비동기식에도 문제점이 있다.
비동기식은 순서가 없는 게 맞는 건데, 이게 비동기식끼리도 순서가 없다 보니 체계가 없고 교통정리가 필요한 상황이 생겨버린다. 즉, 비동기식 안에 교통 경찰 역할을 해줄 비동기식의 탈을 쓴 동기식 역할이 필요한 것이다.
일반적으로는 교통 경찰 역할로 콜백 함수를 많이 쓴다. 하지만, 콜백 지옥이라는 말이 있듯이 콜백 함수로만 이를 처리하기에는 무리이다. 비동기식 처리를 하면서 콜백 함수처럼 질서를 지켜줄 만한 뭔가 없을까?
Promise
function isFiveAbove(num){
return new Promise((resolve, reject) => {
if(num >= 5) resolve(num);
else reject(new Error("This is Error!"));
})
}
isFiveAbove(4)
.then((getNum) => console.log("Good!"))
.catch((error) => console.log(error));
그 역할을 하는 것이 바로 Promise이다. Promise는 비동기식으로 resolve와 reject를 이용해서 질서를 지켜준다.
항상 new Promise()를 쓰면 콜백 함수의 인자로 resolve와 reject를 넣어주자.
"넣으라니까 넣었는데 resolve와 reject가 뭔데요?" Promise는 3가지 상태를 가지고 있다.
대기 상태인 pending, 이행(성공) 상태인 resolve, 실패 상태인 reject이다.
위 코드를 보면 num이 5 이상일 때 resolve한다고 했다. 그리고, 그 외의 상황에는 reject한다고 했다.
여기서 궁금한 점이 바로 resolve와 reject의 인자 값이다. resolve에는 (num)을 넣었고, reject에는 (new Error(...))를 넣었다. 이것들은 나중에 then()과 catch()로 인자를 받아올 때의 값이다.
즉, resolve(num)은 .then((getNum)...)과 연결되며 num = getNum이다.
reject(new Error(...))도 마찬가지로 .catch()와 연결되며 new Error(...) = error이다.
.then과 .catch의 콜백 함수 인자는 resolve와 reject의 인자 값을 그대로 받아온다.
const abc = new Promise((resolve, reject) => {
resolve(100);
})
abc.then(res => console.log(res)); // 100
다시 쉽게 설명해보자면, abc는 Promise로 선언되었고 Promise 안에는 성공했을 때의 resolve와 실패했을 때의 reject가 존재한다. 위 코드에서는 실패 없이 성공만 한다고 가정했다. resolve(100)이라고 하면, 성공했을 때 100이라는 값을 반환한다는 것이다. 그리고 abc가 Promise이기 때문에 then/catch를 뒤에 쓸 수 있고 여기서 res는 resolve의 값을 가리키므로 100을 의미한다. 굉장히 쉬운 Promise 예제이므로 이것부터 이해해보자.
const abc = (num) => {
return new Promise((resolve, reject) => {
if (num >= 10) resolve();
else reject();
})
}
abc
.then(() => console.log("10 이상입니다."))
.catch(() => console.log("10 미만입니다."))
// 이렇게도 가능 (둘의 결과는 같다.)
const abc = (num) => {
return new Promise((resolve, reject) => {
if (num >= 10) resolve("10 이상입니다.");
else reject("10 미만입니다.");
})
}
abc
.then((res) => console.log(res))
.catch((res) => console.log(res))
이번엔 Promise에 인자가 있을 경우이다. 첫 번째 예시 코드와 형식은 같다. num이라는 인자를 받고, num의 값에 따라 resolve되거나 reject된다. 하지만, 이 코드에서는 첫 번째 예시와 달리 resolve와 reject에 값을 넣어주지 않았기 때문에 then/catch에서도 받을 값이 없어서 () => {} 빈칸으로 두었다.
async-await
async function getData(){
const rawRes = await fetch('http://jsonplaceholder...'); // 포장지
const jsonRes = await rawRes.json(); // 포장지 안에 있는 '진짜' 데이터
}
getData();
// fetch()란 API 호출을 도와주는 비동기적 내장 함수이다.
async function fetchAuthorName(postId){
const postResponse = await fetch(
`https://jsonplaceholder.typicode.com/posts/${postId}`
);
const post = await postResponse.json();
const userId = post.userId;
const userResponse = await fetch(
`http://jsonplaceholder.typicode.com/users/${userId}`
);
const user = await userResponse.json();
return user.name;
}
fetchAuthorName(1).then((name) => console.log("name:", name));
// 출처 - https://www.daleseo.com/js-async-async-await/
Promise의 문제점을 보완하고 조금 더 편하게 쓰기 위해서 고안된 게 바로 async-await 문법이다.
함수 앞에 async를 붙이고, await 다음에는 비동기 식을 써준다. await는 반드시 async 함수 내부에서만 사용 가능하다.
비동기식에도 동기식처럼 교통을 정리해주는 교통경찰이 필요하다고 했는데, 여기서의 교통경찰은 await이다.
await은 비동기 처리처럼 바로 넘어가는 것이 아닌, 결과값을 얻을 때까지 기다려주는 역할을 한다.
async가 붙어있는 함수는 따로 Promise를 생성하지 않았어도 Promise 객체가 된다. 따라서 코드 맨 밑줄에 .then을 사용할 수 있는 것이다.
비동기식 처리는 fetch()와 함께 주로 API를 통해 서버에서 데이터를 받아올 때 많이 사용한다.
(데이터를 받아올 때까지 아무것도 안 하고 있으면(동기식 처리) 안 되기 때문)
// #1
const test = async (num) => {
const promise = new Promise(resolve => {
setTimeout(resolve, num);
})
console.log('start');
await promise;
console.log('end');
}
test(3000);
// -- Console --
// start
// (3초 후) end
// #2
const test = (val, num) => {
return new Promise(resolve => {
setTimeout(() => {
resolve(val); // resolve 값으로 test()의 인자인 val을 가져온다.
}, num);
})
}
const test2 = async () => {
console.log('start');
const end = await test('end', 4000);
console.log(end);
}
test2();
// -- Console --
// start
// (4초 후) end
----------
// #2-1 (#2는 이런 식으로 만들어도 된다. -> 아예 Promise에 console.log를 넣어버림)
const test = (val, num) => {
return new Promise(resolve => {
setTimeout(() => {
resolve(); // resolve(console.log(val))도 가능
console.log(val);
}, num);
})
}
const test2 = async () => {
await test('start', 2000);
await test('end', 4000);
}
다음은 async-await을 이용한 setTimeout() 사용법이다. 첫 번째 식은 딜레이만 주었고, 두 번째 식은 딜레이와 출력할 내용까지 Promise 객체에 주었다. 쉽게 생각하면 실행할 함수를 Promise로 만들고, 실행은 async 함수 안에서 await으로 Promise 객체를 실행한다고 보면 된다.
function timeOut(res){
return new Promise(resolve => {
setTimeout(() => {
resolve(res);
}, 2000)
})
}
async function test(){
let our = "나열 : ";
async function test2(val){
our += await timeOut(val) + " ";
}
await test2('0');
console.log(our); // "나열 : 0"
await test2('1');
console.log(our); // "나열 : 0 1"
await test2('2');
console.log(our); // "나열 : 0 1 2"
}
test();
위의 코드를 바탕으로 지금까지의 비동기 처리를 정리해보겠다. 우선, 위 코드의 결과는 "나열 : 0" ... "나열 : 0 1 2" 까지 2초마다 순차적으로 출력된다. 일단, 준비물은 Promise를 return하는 함수와 async 함수가 필요하다.
Promise를 return하는 함수(=이 자체가 곧 Promise)의 역할은 async 함수 안에서 await를 통해 실행된다.
async 함수 안에서는 await을 이용해서 timeOut() 함수를 불러와준다. 하지만, 위 코드에서는 async 함수를 한 번 더 선언해서 숫자를 순차적으로 나열하는 함수를 만들었다. 즉, timeOut()을 test2()에 담고, test2()를 test()에 담은 것이다. async 함수 안에는 await이 있고, await 뒤에는 Promise 객체가 있어야 한다.
정리하자면, Promise를 리턴하는 함수를 만들고(비동기 객체를 만들고) 그 함수를 async 함수에서 await과 함께
불러와주면 되는 게 큰 형식이다. await 다음에는 fetch()나 Promise와 같은 비동기적 성격의 함수가 와야한다.
1. 코드를 보는데 Promise, async, await이 보인다? => 비동기식 처리
2. Promise((resolve, reject) ⇒ {...}) // Promise를 쓴 다음 resolve와 reject를 써주자. (하나만 써도 됨)
3. resolve(인자) .then(인자 ⇒ {...})에서 resolve의 인자와 .then의 인자는 이름이 달라도 값은 같다. (reject는 .catch)
4. resolve/reject의 인자는 어디서 참조하는 인자가 아니라, resolve/reject 시에 반환하는 값이다.
5. await는 async가 붙은 함수 안에서만 사용 가능하고, await은 결과값을 얻을 때까지 기다려주는 역할을 한다.
6. async 함수 안에 await을 쓰고, await 뒤에는 비동기 함수(Promise를 반환하는 함수 등)가 와야 한다.
7. await 뒤에는 Promise를 반환하는 async 함수 혹은 new Promise가 와야 한다. (대기/실행/오류의 상태가 있는 객체)
8. Promise는 기다림을 당하는 객체, 즉 기다려줘야 하는 객체이다. 따라서 await 뒤에 쓰이는 것이다.
'JavaScript' 카테고리의 다른 글
[JavaScript] 배열과 객체의 비구조화 할당 (구조 분해 할당) (0) | 2022.05.08 |
---|---|
[JavaScript] 자기 안에 저장소를 가지고 있는 reduce() (0) | 2022.05.02 |
[JavaScript] 비교하며 알아보는 for~in과 for~of 문법 (0) | 2022.04.20 |
[JavaScript] 알아두면 유용한 배열 메서드 (map(), forEach() 등) (0) | 2022.04.14 |
[JavaScript] 스크롤 제어하기 및 스크롤 숨기는 방법 (0) | 2022.04.07 |