앞서, JS의 비동기 처리 및 콜백 함수에 대해서 알아보았다.
[JavaScript] 싱글 쓰레드와 비동기(Asyncronous)
JavaScript에서 매우 중요한 비동기 처리 방식을 공부하다 보면 JavaScript는 싱글 쓰레드 언어라는 말을 한 번쯤은 접했을 것이다.실제로, JavaScript의 비동기 처리는 싱글 쓰레드와 아주 밀접한 관련
junhee1203.tistory.com
[JavaScript] 콜백 함수와(Callback) 비동기(Asyncronous) 처리
흔히 JavaScript의 비동기를 처리하는 방법은 크게 세 가지가 있다. 1. 콜백 함수(Callback)2. Promise3. async awiat 이 중에서 가장 기본이 되는 콜백 함수에 대해서 알아보자. 먼저, 비동기를 어떻게 콜백
junhee1203.tistory.com
콜백 함수만을 이용해서 비동기를 처리하면 흔히 콜백 지옥에 빠진다.
이번 글에서는 콜백 지옥에 벗어나는 방법 중 하나인 Promise에 대해서 알아본다.
애초에, 또 하나의 방법인 async await 역시 Promise를 이용하기에
Promise를 반드시 이해하고 넘어갈 필요가 있다.
1️⃣ Promoise 개념
Promise 는 한국어로 약속이란 뜻이다.
말 그대로 JS에서 Promise 역시 약속의 뜻을 내포하고 있다.
Promise 의 정의는 어떤한 일을 수행할 것인지에 대한 약속이다.
Promise 는 내장 클래스이므로 new 키워드 함께 사용되어야 한다.
즉, 인스턴스를 만들어서 사용할 수 있다.
const promise = new Promise()
흔히, 위와 같이 인스턴스를 만들어서 사용한다.
물론 다음과 같이 어떤 함수의 return 값으로 promise를 반환할 수도 있다.
function getPromise(){
return new Promise()
}
Promise 는 resolve, reject 함수를 인자로 갖는 콜백 함수를 매개변수로 받는다.
const promise = new Promise((resolve, reject) =>{
})
다시 말하지만, resolve 와 reject 는 함수이다.
이때, resolve 에는 promise 가 성공했을 때 반환할 값을
reject 에는 promise 가 실패했을 때 반환할 값을 매개변수로 받는다.
일반적으로 Promise에서 '성공'과 '실패'의 유무는 다음과 같이 구별된다.
- 성공(resolve) : 작업이 에러 없이 완료되었을 때
- 실패(reject): 작업 중 에러가 발생했을 때
하지만, 중요한 점은 반드시 에러의 발생 유무로 나뉘는 것이 아니라 개발자 입맛대로 정할 수 있다.
무슨 의미인지는 예시를 계속 확인하면 알 수 있다.
예시를 들어보자.
let number = 1
const promise = new Promise((resolve, reject)=>{
resolve(number+1)
})
Promise 안 resolve 함수에 number+1 즉, 2를 인자로 넣어주었다.
정의대로 하면, Promise 가 성공했을 때 2를 반환하라는 의미이다.
단순히 console.log(promise) 를 하면 2가 나오는 것이 아니라
성공했을 때의 값을 받는 then 메소드를 사용해야 한다.
let number = 1
const promise = new Promise((resolve, reject)=>{
resolve(number+1)
})
promise.then(result => console.log(result))
then 메소드도 콜백함수를 인자로 받는다.
resolve 한 결과를 result로 받고, result 를 console.log 해주었다.
즉, promise 성공 결과로 2를 받고, 2를 콘솔에 출력해주었다.
이번엔 promise 가 실패했다고 가정하고, reject 함수를 이용해보자.
다시 말하지만 reject는 promise 가 실패한 경우에 사용된다.
즉, 보통 에러가 발생된 상황에 사용하지만 위에서 언급했듯이, 반드시 그런 것은 아니다.
개발자 마음대로 정의할 수 있다.
아래 예시는 문법적으로 에러는 없지만, 내 마음대로 reject에 인자를 넣어주었다.
let number = 1
const promise = new Promise((resolve, reject)=>{
reject(number-1)
})
promise.catch(result => console.log(result))
promise가 실패했다고 가정하고, reject의 인자로 실패했을 때의 값인 0을 할당해주었다.
실패했을 때는 then이 아니라 catch 로 값을 받아올 수 있다.
즉, 여기까지 이해가 됐다면 promise를 의사코드로 다음과 같이 작성할 수 있다.
const promise = new Promise((resolve, reject)=>{
if(promise 가 성공하면){
resolve(성공했을 때 넣어줄 값)
}else{
reject(실패했을 때 넣어줄 값)
}
})
promise.then(result => console.log(result))
promise.catch(result => console.log(result))
2️⃣ Promise 메소드 체이닝
promise는 메소드 체이닝이 가능하다.
즉, then과 catch를 체이닝 해서 연달아 사용이 가능하다.
위의 의사코드도 메소드 체이닝을 하면 다음과 같다.
const promise = new Promise((resolve, reject)=>{
if(promise 가 성공하면){
resolve(성공했을 때 넣어줄 값)
}else{
reject(실패했을 때 넣어줄 값)
}
})
promise
.then(result => console.log(result))
.catch(result => console.log(result))
메소드 체이닝이 가능한 이유가 무엇일까 ?
아마 메소드 체이닝의 원리를 안다면 굳이 말을 하지 않아도 알 것이다.
그 이유는, then 과 catch 메소드 역시 promise를 반환하기 때문이다.
메소드 체이닝의 예시를 보자.
let number = 1;
let random = Math.random();
const promise = new Promise((resolve, reject) => {
if (random >= 0.5) {
resolve(number + 1);
} else {
resolve(number - 1);
}
});
promise
.then((result) => console.log(result))
.catch((result) => console.log(result));
random 변수에 0~1 사이의 랜덤 값을 할당해준다.
random 변수가 0.5 이상이면 성공했다고 가정하고, resolve에 number+1 (2) 값을,
random 변수가 0.5 미만이면 실패했다고 가정하고, reject에 number-1 (0) 값을 인자로 넣어준다.
그리고 성공했다면 number+1(2) 값을 result로 받아 출력해주고
실패했다면 number-1(0) 값을 result로 받아 출력해준다.
메소드 체이닝을 더 적용해보자.
let number = 1;
let random = Math.random();
const promise = new Promise((resolve, reject) => {
if (random >= 0.5) {
resolve(number + 1);
} else {
reject(number - 1);
}
});
promise
.then((result) => result+1)
.then((result) => result+1)
.then((result) => result+1)
.then((result) => console.log(result))
.catch((result) => console.log(result));
메소드 체이닝을 이용하여 then의 로직을 더 추가하였다.
우선 random이 0.5 이상이면 resolve 에 number+1 (2) 값을 인자로 넣어준다.
그리고 then의 result(2)로 받아 result +1 (3) 해준다.
다시 then의 result(3) 로 받아 result+1 (4) 해준다.
다시 then의 result(4) 로 받아 result+1 (5) 해준다.
다시 then의 result(5) 로 받아 콘솔에 출력한다.
만약, random이 0.5 미만이면 reject에 number-1(0) 값을 인자로 넣어준다.
그리고 catch의 result(0) 로 받아 콘솔에 출력한다.
코드가 매우 직관적이고 알아보기 쉽다.
콜백함수도 익명함수로 작성하는 것보다 다음과 같이 함수화를 하면
유지보수도 좋아지고 더욱 코드가 깔끔해진다.
let number = 1;
let random = Math.random();
function increase(number) {
return number + 1;
}
const promise = new Promise((resolve, reject) => {
if (random >= 0.5) {
resolve(number + 1);
} else {
reject(number - 1);
}
});
promise
.then(increase)
.then(increase)
.then(increase)
.then(console.log)
.catch(console.log);
마지막으로 then, catch 이외에도 promise에는 finally 메소드가 존재한다.
finally 메소드는 promise의 성공과 실패의 관계없이 실행된다.
바로 예시를 확인하자.
const promise = new Promise((resolve, reject) => {
if (random >= 0.5) {
resolve(number + 1);
} else {
reject(number - 1);
}
});
promise
.then((result) => console.log("Promise 성공 값: ", result))
.catch((result) => console.log("Promise 실패 값: ", result))
.finally(() => console.log("finally !!"));
3️⃣ Promise의 상태
Promise 는 총 3가지의 상태를 갖는다.
- pending (보류 중) : Promise 에 resolve, reject 에 어떠한 값도 담기지 않았을 때
- fulfilled (이행 된) : Promise가 성공하여 resolve가 작동됐을 때
- rejected (거절 된) : Promise가 실해파여 reject가 작동됐을 때
예시를 확인하자.
참고로, Node.js보다 브라우저가 Promise의 상태를 확인하는 것에 있어 훨씬 직관적이다.
Promise의 resolve, reject에 어떠한 값도 할당해주지 않으니 pending이라는 상태가 나타난다.
이번에는 Promise가 성공했다고 가정하고, resolve에 값을 할당해주겠다.
이번에는 Promise가 실패했다고 가정하고, reject에 값을 할당해보자.
이번에는 1/2 확률로 promise를 성공하게 해보자.
4️⃣ Promise와 비동기 처리
지금까지 Promise의 개념에 대해서 살펴보았다.
여기까지만 보았을 때, Promise를 어디에 사용해야 될지 의문이 들 수 있다.
Promise를 이용하면 콜백 헬을 벗어나, 비동기 처리를 직관적이고 가독성 있게 바꾸어준다.
바로 적용을 해보자.
function delay(number,nextFunc = null) {
setTimeout(() => {
console.log(number);
if (nextFunc) nextFunc();
}, 1000);
}
delay(1, () => {
delay(2, () => {
delay(3, () => {
delay(4, () => {
delay(5, () => {
delay(6, () => {
delay(7, () => {
delay(8, () => {
delay(9);
});
});
});
});
});
});
});
});
1초마다 1,2,3,4,5,6,7,8,9 를 순서대로 출력해주는 코드이다.
대표적인 콜백 헬의 예시이다.
이제 위 코드를 promise를 이용하여 리팩토링 해보자.
function delay(number) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(number);
resolve(number);
}, 1000);
});
}
delay(1)
.then((result) => delay(result+1))
.then((result) => delay(result+1))
.then((result) => delay(result+1))
.then((result) => delay(result+1))
.then((result) => delay(result+1))
.then((result) => delay(result+1))
.then((result) => delay(result+1))
.then((result) => delay(result+1))
처음 resolve 에 1 값이 들어가고 then 메소드를 통해 result에 resolve 한 값인 1을 받아와
delay(result+1) 해준다.
그리고 계속 then 메소드를 통해 result+1 을 해주므로
1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 순서대로 delay 에 인자로 넣어주는 것이다.
확실히 콜백함수를 이용하여 비동기를 처리하는 것보다, 훨씬 직관적이게 되었다.
이번에는 reject도 활용한 다를 예시를 들어보자
function randomGame(number) {
const random = Math.random();
return new Promise((resolve, reject) => {
setTimeout(() => {
if (random >= 0.5) {
console.log("성공: ", number);
resolve(number);
} else {
console.log("실패: ", number);
reject(number);
}
}, 1000);
});
}
randomGame(1)
.then((result) => randomGame(result + 1))
.then((result) => randomGame(result + 1))
.then((result) => randomGame(result + 1))
.then((result) => randomGame(result + 1))
.catch(()=>console.log('game over'))
.finally(()=>console.log('game end'))
50% 로 성공과 실패가 나뉘는 확률 게임이다.
50% 확률로 성공하면 결과값에 +1 을 하여 다음 게임을 하게 된다.
catch 메소드를 통해 만약 실패하면, game over 이 출력되며 게임이 끝이난다.
finally 메소드를 통해 게임의 성공과 실패의 관계없이 마지막에는 game end 를 출력한다.
자 이제, 콜백 함수 글에서 마지막 예시였던 콜백 헬에 빠진 릴레이 경주 게임을
promise를 이용해서 리팩토링 해보자.
기존 코드는 다음과 같았다.
function relayRunning(player, nextFunc, totalTime = 0) {
const runningTime = Math.random() * 1000;
totalTime += runningTime;
setTimeout(() => {
console.log(`${player} 도착 시각 : ${totalTime / 1000}초`);
nextFunc?.(totalTime);
if (!nextFunc) {
totalTime / 1000 < 2 ? console.log("PASS") : console.log("FAIL");
}
}, runningTime);
}
relayRunning("철수", (totalTime) => {
relayRunning(
"영희",
(totalTime) => {
relayRunning(
"민수",
(totalTime) => {
relayRunning("민주", null, totalTime);
},
totalTime
);
},
totalTime
);
});
철수, 영희, 민수, 민주가 릴레이 경주 게임을 하는데 마지막 주자까지 총 2초 안으로 들어와야 성공한다.
이제 promise를 이용하여 리팩토링 해보자.
const DEADLINE = 2000;
function relayRunning(player, totalTime = 0) {
const runningTime = Math.random() * 1000;
totalTime += runningTime;
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`${player} 도착 시각 : ${totalTime / 1000}초`);
if (totalTime < DEADLINE) {
resolve(totalTime);
} else {
reject(totalTime);
}
}, runningTime);
});
}
relayRunning("철수")
.then((totalTime) => relayRunning("영희", totalTime))
.then((totalTime) => relayRunning("민수", totalTime))
.then((totalTime) => relayRunning("민주", totalTime))
.then(() => console.log("SUCCESS"))
.catch(() => console.log("FAIL"))
.finally(() => console.log("---경기 종료---"));
로직이 조금 바뀐 점은, 콜백함수를 이용할 때는 중간에 2초가 넘어도 아직 주자가 남았다면
일단 진행이 되었다. 그러나, 위의 로직은 중간에 2초가 넘으면 더 이상 남은 주자는 달리지 않는다.
사실, 리팩토링 코드를 보면 콜백 함수보다 훨씬 나아진 것인가 ? 이러한 의문점이 들 수 있다.
메소드 체이닝을 연속적으로 과하게 하면 오히려 콜백 헬 못지 않게 가독성이 떨어진다.
결국, Promise도 지나친 메소드 체이닝을 하면 가독성이 떨어진다는 문제점이 발생하는 것이다.
그래서, JS는 이러한 문제점을 개선하고자 하였고 그 결과로 async await 문법이 탄생하였다.
다음 글에서는 Promise의 심화 파트로 Promise의 정적 메소드에 대해서 알아보겠다.
[JavaScript] Promise 의 정적 메소드 분석
이전 글에서, Promise 가 무엇이고 Promise를 이용한 비동기 처리에 대해 알아보았다. [JavaScript] Promise와 비동기(Asnycronous) 처리앞서, JS의 비동기 처리 및 콜백 함수에 대해서 알아보았다. [JavaScript]
junhee1203.tistory.com
또한 그 이후에 비동기 처리의 마지막 파트인 async await을 알아보겠다.
이번 글을 요약하겠다.
- Promise는 비동기 처리의 복잡성을 해결하기 위해 도입되었다.
- Promise는 생성자 함수이며, resolve, reject 함수를 인자로 갖는 콜벡 함수를 인자로 받는다.
- resolve 는 Promise가 성공했을 때 결과값을 인자로 받고 then 메소드를 통해 전달 받을 수 있다.
- reject는 Promise가 실패했을 때 결과값을 인자로 받고 catch 메소드를 통해 전달 받을 수 있다.
- then,catch 메소드 모두 Promise를 반환하므로 메소드 체이닝이 가능하다.
- Promise는 세 가지 (pending, fulfilled, rejected) 상태를 갖는다.
- 지나친 메소드 체이닝도 콜벡 헬처럼 가독성이 좋지 못하다. 이에 대한 해결책으로 async await 문법이 도입됐다.
'JavaScript' 카테고리의 다른 글
[JavaScript] 옵저버(Observer) 패턴을 이용한 상태 관리 (3) | 2024.10.03 |
---|---|
[JavaScript] Promise 정적 메소드 분석 (1) | 2024.09.15 |
[JavaScript] drag (드래그) 관련 이벤트 전격 분석 (0) | 2024.09.10 |
[JavaScript] Element.closest 메소드 분석 (0) | 2024.08.31 |
[JavaScript] 이벤트 버블링(Bubbling), 이벤트 위임(Delegation) (2) | 2024.08.31 |