🐕Promise는 뭐냐..
비동기로 작동하는 코드를 제어할 수 있는 방법 중 하나입니다.
자바스크립트에서 비동기의 처리 순서를 보장하기 위해서는
콜백함수 기법을 사용하는 방법을 주로 사용했습니다.
예컨대 이런 코드를 작성해서 실행시켜보면
아주 직관적으로 이해가 됩니다.
let UMJUNSICK
setTimeout(() => {
UMJUNSICK = "어떻게 사람이름이"
} , 4)
console.log(UMJUNSICK) // undefined
우리의 기대는 setTimeout을 통해
0.004초 뒤에 UMJUNSICK 변수에 "어떻게 사람이름이"를 할당해주고
출력해주는 것이었지만 안타깝게도 그것은 불가능했습니다.
undefined가 반환되어 버리죠
아주 당연한 일입니다. 좀 더 심화해서 콜백을 사용할 수 밖에 없는 이유를 한번 봐볼까요?
const asyncCallback = (str , callback) => {
setTimeout(() => {
console.log(str);
callback()
} , Math.floor(Math.random() * 1000))
}
const printAll = () => {
asyncCallback("엄", () => {
asyncCallback("준", () => {
asyncCallback("식" , () => {
asyncCallback("이", () => {
})
})
})
})
}
printAll()
타이머함수는 먼저 타이머가 종료되는 함수가 우선적으로 태스크큐에 들어가 실행을 대기합니다.
그렇기 때문에 타이머 시간이 random이면 처리순서가 보장되지 않을 것입니다.
큰 문제가 될 수 있겠죠?
예컨대 이렇듯이 실행순서가 보장되지 않고 랜덤으로 출력되는 모습을 볼 수 있습니다.
항상 순서대로 실행되게 비동기함수를 사용하기위해서
저런 콜백패턴을 사용하는것이 필수 불가결했던 것이지요
비동기 함수 setTimeout() 내부에 콜백함수를 호출하는 패턴을 두는 것을 통해
반드시 어떠한 비동기 함수가 실행이 완료된 다음에 콜백함수가 호출될 수 있도록하는 원리죠!
하지만.. CallbackHell이라고도 불리는 저 극악의 들여쓰기는
많은 개발자들에게 큰 피로감을 불러왔습니다.
지금이야 간단한 예제코드이니 한줄로 끝나기때문에 읽을만하지만
만약 코드가 엄청 거대하다면?? ㅎㅎ;;;
이러한 문제를 해결할 기법으로 프로미스가 등장했지만
이 프로미스마저도 프로미스 헬이 생기는 건 좀 더 나중의 이야기
지금의 프로미스를 집중해서 봅시다.
👻프로미스는 주로
서버에서 받아온 데이터를 화면에 표시할 때에 많이 사용하게 됩니다.
서버가 언제 응답을 줄 지 모르기 때문에
서버의 응답을 하염없이 기다리며 태스크를 블로킹 해두면
굉장히..... 렌더가... 오래 걸릴 가능성도 생기겠죠?
그런데 그래서 서버에서 받는 데이터를 비동기로 처리하게되는데...!
아까 타이머 예시에서 봤듯이 비동기 함수의 실행순서 보장을 위해서는
콜백패턴이 필요합니다.
또한 비동기 함수 내부의 변수나 값을 사용하려고 할 때
비동기 함수의 밖에서 사용을 하려고하면
우리가 아까 UMJUNSICK 변수 예시에서 봤듯이 undefined가 반환됩니다.
undefined만 반환되면 다행이지 거기에 점표기법으로 메서드나 프로퍼티를 사용하려고했다?하면
에러를 만나게 되겠죠?
🥶 Promise의 사용법
Promise 생성자 함수를 new 연산자와 함께 호출하면 프로미스 객체를 생성합니다.
Promise 객체 또한 호스트 객체가 아니라 ES6 ECMASCRIPT 사양에서 정의된
표준 빌트인 객체입니다.
!!!! Promise 생성자 함수는 비동기 처리를 수행할 콜백 함수를 인수로 전달받습니다.
또한 이 콜백 함수는 resolve와 reject 함수를 인수로 전달받아요!!
const promise = new Promise((resolve, reject) => {
if(--비동기처리--) {
resolve('result)
}
else { // 비동기처리 실패 했을때 로직
reject('reject')
}
})
그래서 기본적인 구조는 다음과 같습니다.
비동기 처리가 성공하면 콜백함수의 인수로 전달받은 resolve 함수를 호출하고
비동기 처리가 실패하면 reject 함수를 호출합니다.
🌞프로미스의 상태 정보
프로미스의 상태 정보 | 의미 | 상태 변경 조건 |
pending | 비동기 처리가 아직 수행되지 않음 | 프로미스가 생성된 직후 상태 |
fulfilled | 비동기 처리 수행된 상태(성공) | resolve 함수 호출 |
rejected | 비동기 처리 수행된 상태(실패) | reject 함수 호출 |
또한 fulfilled , rejected 인상태를 settled 상태라고 합니다.
settled 상태는 아무튼 pending 이 아닌 비동기 처리가 수행된 상태를 의미해요
그런데 상태 변경 조건을 유심히 보면 알 수 있는 점이
함수 내부에서 resolve 함수를 호출하느냐 reject함수를 호출하느냐로
프로미스의 상태가 변경된다는 것입니다.
비동기 처리 성공 | resolve 함수를 호출해 프로미스를 fulfilled 상태로 변경 |
비동기 처리 실패 | reject 함수를 호출해 프로미스를 rejected |
저는 비동기 처리가 성공했는지 실패했는지에 따라
resolve , reject 상태가 변경된다는.. 바보같은 생각을 했는데
어떤 함수를 호출하느냐로 상태가 결정되는 것이었습니다.
참고로 저 resolve, reject 함수의 인자로는
프로미스의 후속처리 메서드에 넘겨줄 밸류가 들어갑니다.
😋프로미스의 후속처리 메서드
프로미스의 비동기 처리 상태가 변화하면 후속 처리를 해줘야합니다.
fulfilled 상태가 되면 처리 결과를 어떻게 해야하고
rejected 상태가 되면 에러를 핸들링해주고...
따라서 프로미스는 후속처리 메서드 then, catch, finally를 제공합니다.
then() 메서드
const a = () => {return new Promise(resolve => resolve('fulfiled'))}
a().then(v => console.log(v) , e => console.error(e)) //fulfiled
간단하게 프로미스를 반환하는 함수를 값으로 가진 변수 a를 만들어줬습니다.
resolve 함수만 인자로 받아서 resolve 함수안에 'fulfiled'란 인자를 넣어 호출하는 일을 하는
간단한 프로미스입니다.
이제 a()함수를 호출하면 반환값으로 프로미스 객체가 반환될 것입니다.
프로미스 객체에는 프로미스 후속처리 메서드를 붙여서 사용할 수 있습니다.
then 메서드의 첫번째 콜백함수는 resolve함수가 호출된 상태일 때 호출됩니다.
이때 콜백 함수는 프로미스의 비동기 처리 결과를 인수로 전달받습니다.
두번째 콜백함수는 reject 함수가 호출된 상태일 때 호출 됩니다.
이때 콜백 함수는 프로미스의 에러를 인수로 전달 받습니다.
또한 then은 항상 프로미스를 반환한다는 특징이 있습니다.
만약 then 메서드의 콜백함수가 프로미스가 아닌 값을 반환하면
그 값을 암묵적으로 resolve하거나 reject해서
프로미스를 생성하여 반환합니다.
catch() 메서드
catch메서드는 then 메서드와 달리 한개의 콜백 함수를 인수로 전달 받습니다.
catch 메서드의 콜백 함수는 프로미스가 reject상태인 경우에만 호출됩니다.
catch 메서드는 then메서드의 두번째 콜백함수와 동일하게 동작합니다.
finally() 메서드
한개의 콜백 함수를 인수로 전달받습니다.
finally()의 콜백함수는 성공 실패 상관없이 무조건 한번 호출되기 때문에 공통적으로 수행할 처리가 있을 때 유용합니다.
🤢프로미스의 에러처리
비동기 처리를 위한 콜백 패턴의 가장 심각한 문제점은 에러처리가 곤란하다는 것입니다.
try {
setTimeout(() => {throw new Error("다은 is legend")} , 1000)
} catch (e) {
console.error("캐치한에러", e)
}
setTimeout(() => {throw new Error("다은 is legend")} , 1000)
^
Error: 다은 is legend
at Timeout._onTimeout (c:\Users\qdv16\OneDrive\바탕 화면\json-server-exam\fds.js:3:29)
at listOnTimeout (node:internal/timers:568:17)
at process.processTimers (node:internal/timers:511:7)
Node.js v19.5.0
위 에러는 catch 코드 블록에서 캐치되지 않습니다.
왜냐면 비동기함수의 특성상 setTimeout의 콜백함수가 실행되는 시점에
이미 setTimeout 함수는 콜스택에서 제거된 상태이기 때문입니다.
이벤트 루프에 의해 호출자가 명확하지 않은 상태로 호출된 setTImeout의 콜백함수는
setTimeout에게 에러를 전파해주지 못합니다.
에러는 호출자 방향으로 전파(콜스택의 아래방향)으로 전파되지만
비동기 함수의 특성상 콜스택이 이미 비워진 상태여야 호출되기 때문에
catch 블록에서 비동기의 콜백함수를 캐치하는게 불가능합니다.
🤮프로미스의 동작 방식
프로미스는 조금 특이한 동작 방식을 가지고 있습니다.
그래서 이 부분에 대한 이해가 반드시 필요합니다.
1. 프로미스는 정의되는 시점에 실행된다.
let a = new Promise((resolve, reject) => {
console.log('hi');
resolve('hi');
});
위 코드의 실행 결과는 어떻게 될 것 같나요?
놀랍게도 콘솔창엔 hi가 출력됩니다.
왜 놀라운걸까요?
우린 함수의 정의 시점과 호출 시점은 다르다고 알고있습니다.
즉시실행함수로 만들어주지 않으면 정의와 동시에 실행되지않죠
(function () {
console.log("hi");
})();
이렇게요!
하지만 위의 promise 문법은 변수에 할당을 하는 동시에 실행이 되어버립니다.
console.log('프로미스앞에서 실행');
let a = new Promise((resolve, reject) => {
console.log('hi');
resolve();
});
console.log('프로미스 뒤에서');
위 코드의 실행결과는
프로미스앞에서 실행
hi
프로미스 뒤에서
이렇게 되어버립니다.
조금 특이하죠?
2. 프로미스는 비동기로 동작한다?
위 예제만으로도 알 수 있지만 프로미스 자체는 동기적으로 동작합니다.
만약 프로미스 자체가 비동기적으로 동작했다면
hi가 가장 마지막에 출력되었어야 할 것입니다.
심지어 resolve나 reject함수를 호출하는 것까지도 동기적으로 실행되죠!
그저 프로미스 내부의 코드 중 비동기 함수가 있을 경우
그 비동기 함수가 비동기로 동작하는 것일 뿐입니다.
(비동기 함수가 비동기로 동작한다는 것은 조금 당연하죠?)
console.log('프로미스앞에서 실행');
let a = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('비동기는 여기서');
resolve('hi');
}, 1000);
console.log('비동기 밖에선 그냥 실행');
});
console.log('프로미스 뒤에서');
프로미스앞에서 실행
비동기 밖에선 그냥 실행
프로미스 뒤에서
비동기는 여기서
위 코드는 이렇게 동작합니다.
그리고 당연한 논리로
let a = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('비동기는 여기서');
resolve('hi');
}, 1000);
console.log('비동기 밖에선 그냥 실행');
});
while (true) {}
이렇게 평생 끝나지 않는 코드를 만들어서 실행시켜보면
비동기 밖에선 그냥 실행
비동기는 여기서라는 콜백은 평생 실행될 수 없게됩니다!
만약 프로미스의 실행 시점을 조절하고 싶다면
앞서 한 것처럼 함수의 리턴값으로 만들어주면 되겠죠?
🤮프로미스 체이닝
프로미스는 극한의 들여쓰기로 인해 발생하는 콜백지옥을
극한의 후속처리메서드 체이닝을 통해 해결합니다.
물론 그 과정에서 극한의 체이닝 또한 지옥이다라는 결론을 얻을 수 있습니다.
아무튼 들여쓰기보다는 나음
프로미스의 후속처리 메서드들은 언제나 프로미스를 반환하기 때문에
연속적으로 호출 할 수 있어서 프로미스 체이닝이 가능합니다.
그런데 생각을하면서 코드를 짜보니까 좀 문제가 발견되었습니다.
const zzinsole = () => {
return new Promise((resolve,reject) => {
setTimeout(() => resolve("kim"))
})
}
let kimZzin = zzinsole()
kimZzin
.then((res) => {
return res + " zzin"
})
.then((res) => {
return res + 'solen'
})
.then((res) => {
return res + ' jun sul'
})
.then((res) => {
return res + 'ida'
})
.then((res) => {
console.log(res)
})
예컨대 이런식으로 잔뜩 체이닝을 걸어서 실행시켰을 때
then으로 체이닝 된 애들도 비동기라면
먼저 처리되는 순서대로 실행이 되어버려서 실행순서가 전혀 보장이 안되는 것
const zzinsole = () => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve('kim'));
});
};
let kimZzin = zzinsole();
kimZzin
.then((res) => {
setTimeout(() => {
console.log(res + ' zzin');
}, 2000);
return res + 'zzin';
})
.then((res) => {
setTimeout(() => {
console.log(res + ' sole');
}, 300);
return res + 'sole';
});
예컨대 이런식으로 코드를 작성하면
kimzzinsole이 kimzzin보다 빨리 출력된다.
사실 생각해보면 당연함...
then은 순서대로 실행되는 대신 setTimeout은 호출스케줄만 잡고 바로 다음코드를 실행하고
setTImeout의 콜백이 또 들어가는거니까 당연한 결과긴한데..
내가 원하는 것은 이게 아니란 거죠 난 처리 순서를 보장해주고 싶은데요
🐶프로미스로 처리 순서를 보장해주는 방법
const kim = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('kim');
resolve('kim');
}, 2000);
});
};
const jin = (kim) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
let kimjin = kim + 'jin';
console.log(kimjin);
resolve(kimjin);
}, 1000);
});
};
const sole = (kimjin) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
let kimjinsol = kimjin + 'sol';
console.log(kimjinsol);
resolve(kimjinsol);
}, 200);
});
};
kim()
.then((res) => jin(res))
.then((res) => sole(res));
이렇게 프로미스를 반환하는 함수를 여러개 만들어서
then의 콜백함수가 호출하는 함수로 사용해주면!!!!!!!!!!!!!!!!!!!!!!!!!!!!
1. 비동기 함수 내부의 값들을 활용할 수 있으면서
2. 처리 순서가 완전히 보장되는(비동기처리가 끝나고 resolve가 실행되어야 다음 then이 실행됨)
우리가 원하던 처리 결과를 얻을 수 있습니다.
실제로 저 코드는 뒤로 갈수록 타이머의 시간이 짧아지기 때문에
그냥 무지성 then 체이닝만 걸었다면 뒤죽박죽 실행되었겠지만
항상 처리순서가 보장되도록 바뀐 것을 확인할 수 있어요
어라..? 그런데 프로미스를 반환하는 함수를 통해서 순서보장이 가능하다면
then 메서드 안에서 새로운 프로미스를 반환해주는 식의 코드를 작성하면???
resolve 함수도 계속 호출할 수 있으니까 괜찮지않을까요?
const ryu = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('kim');
resolve('kim');
}, 2000);
});
};
let ryujisuMaker = ryu();
ryujisuMaker
.then((res) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(res + 'ji');
resolve(res + 'ji');
}, 1200);
});
})
.then((res) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(res + 'su');
resolve(res + 'su');
}, 1000);
});
})
.then((res) => {
setTimeout(() => {
console.log(res + '그냥 미쳤다');
}, 500);
});
간단한 그냥 미쳤다 생성기를 만들었습니다.
타이머가 뒤로 갈수록 짧아지기 때문에 순서보장이 안된다면
순서가 뒤죽박죽 꼬일것입니다.
체이닝해준 then 메서드들은 각각 새 프로미스를 반환하며 이전 프로미스의 반환값에 자신의 값을 추가하며
반환된 프로미스는 setTimeout 함수를 호출해주고
setTimeout의 콜백 함수는 resolve 함수를 호출해주는 일을 수행합니다
출력결과는..?
정말 멋지네요...
🙄곁다리로 보는 fetch() 함수
fetch('https://jsonplaceholder.typicode.com/todos/1').then((response) =>
console.log(response),
);
fetch() 함수는 http 요청 전송 기능을 제공하는 클라이언트 사이드 Web API입니다.
fetch() 함수 역시 프로미스를 지원하기 때문에 비동기 처리를 위한 콜백 패턴에서 자유롭고
또 위 예제에서 볼 수 있듯이 promise 후속처리 메서드 또한 사용 가능합니다.
fetch 함수는 HTTP 응답을 나타내는 response 객체를 래핑한 promise 객체를 반환해요
말로보면 몰?루니까 저 코드의 출력결과를 가져와보면
Response {
[Symbol(realm)]: null,
[Symbol(state)]: {
aborted: false,
rangeRequested: false,
timingAllowPassed: true,
requestIncludesCredentials: true,
type: 'default',
status: 200,
timingInfo: {
startTime: 38.47969999909401,
redirectStartTime: 0,
redirectEndTime: 0,
postRedirectStartTime: 38.47969999909401,
finalServiceWorkerStartTime: 0,
finalNetworkResponseStartTime: 0,
finalNetworkRequestStartTime: 0,
endTime: 0,
encodedBodySize: 71,
decodedBodySize: 0,
finalConnectionTimingInfo: null
},
cacheState: '',
statusText: 'OK',
headersList: HeadersList {
[Symbol(headers map)]: [Map],
[Symbol(headers map sorted)]: null
},
urlList: [ [URL] ],
body: { stream: undefined }
},
[Symbol(headers)]: HeadersList {
[Symbol(headers map)]: Map(25) {
'date' => [Object],
'content-type' => [Object],
'transfer-encoding' => [Object],
'connection' => [Object],
'x-powered-by' => [Object],
'x-ratelimit-limit' => [Object],
'x-ratelimit-remaining' => [Object],
'x-ratelimit-reset' => [Object],
'vary' => [Object],
'access-control-allow-credentials' => [Object],
'cache-control' => [Object],
'pragma' => [Object],
'expires' => [Object],
'x-content-type-options' => [Object],
'etag' => [Object],
'via' => [Object],
'cf-cache-status' => [Object],
'age' => [Object],
'server-timing' => [Object],
'report-to' => [Object],
'nel' => [Object],
'server' => [Object],
'cf-ray' => [Object],
'content-encoding' => [Object],
'alt-svc' => [Object]
},
[Symbol(headers map sorted)]: null
}
}
대충 엄청많은 프로퍼티가 담긴 객체가 반환된다는 뜻
HTTP 응답 몸체를 위한 다양한 메서드가 담겨있는걸 볼 수 있습니다.
다만 fetch 함수는 에러 처리에 유의해야하고
(CORS 에러나 오프라인 네트워크 장애가 일어난 경우에나 reject하고
404 not found 500 server error 같은 건 reject 안해줌)
불편한 점들이 있기 때문에
그냥 axios 라이브러리를 깔아서 사용하는 편이 심신건강에 좋다고 합니다.
axios는 모든 HTTP 에러를 reject 하는 프로미스를 반환하기 때문에
모든 에러를 catch에서 처리할 수 있어서 아주 편해요
정말.. 프로미스는 좋은 자료를 찾기도 힘들고
수많은 콜백함수가 사용되다보니 이해하는 것도 쉽지 않은 것 같습니다.
나름 여러번 정독을 해도 잘 이해가 안되는 것 같아요
이번에 나름대로 이해도가 늘었다는 생각이 들지만
항상 마음대로 안되는 건 어쩔 수 없는 것 같습니다.
'javascript' 카테고리의 다른 글
node.js 환경에서 fetch API를 사용하는 방법 (0) | 2023.03.21 |
---|---|
자바스크립트 this 디스합니다. 비트주세요 (0) | 2023.03.16 |
타이머 함수를 케이크처럼 쉽게 이해하는 법 (2) | 2023.03.12 |
이벤트 루프와 블록 컨텍스트를 알아보자. (0) | 2023.03.10 |
생존법칙1 reduce 메서드를 이해해라. (0) | 2023.03.09 |