Js- promise
in Javascript
참고
[1]. https://ko.javascript.info/async
- Promise란..?
- Promise 객체의 then, catch, finally 메소드
- callback -> promise 예시
- 프라미스 체이닝
- Thenable
- fetch와 체이닝 함께 응용하기
- Promise와 에러 처리
- Promise의 API
- Promisification
Promise란..?
- Promise는 기본적으로 객체이다.
- Promise는 함수(excutor)를 인자로 받고 객체가 생성될 때, 해당 함수가 실행된다.
- excutor 함수는 두개의 함수를 인자로 받는다.
- resolve와 reject는 자바스크립트에서 제공하는 함수이다.
- Promise는 성공 또는 실패만 한다. 따라서 executor 함수 내에서 resolve(성공)와 reject(실패) 함수 둘 중 하나는 반드시 콜 해야한다.
- 또한 한번만 호출되어야 한다. 두번 이상 호출되면 두번째 호출부터는 실행되지 않는다.
- resolve 함수와 reject 함수는 기본적으로 callback 함수이다.
- excutor 함수가 실행되고 내부에서 resolve, reject 함수가 실행되면 resolve 함수와 reject함수는 background에 보내지고 바로 스택에 쌓인다.
// resolve와 reject는 callback 함수이기 때문에 then 메소드는 바로 실행되지 않고 background로 넘어간 이후에 비동기로 실행된다. 따라서 console에 2, 5, 4, 1 순서로 찍힌다. test = new Promise((resolve, reject) => { console.log(2); resolve(1); }); console.log(5); test.then((result) => {console.log(result);}); console.log(4);- resolve(value) - 일이 성공적으로 끝난 경우 그 결과를 나타내는 value와 함께 호출.
- reject(error) - 에러 발생 시 에러 객체를 나타내는 error와 함께 호출.
// resolve 호출 let promise = new Promise(function(resolve, reject){ setTimeout(() => resolve("done"), 1000); }); // reject 호출 let promise2 = new Promise(function(resolve, reject){ setTimeout(() => reject(new Error("error")), 1000); })Promise 객체의 주요 property
- state : 문자열 데이터이며, 처음에는 “pending” 이었다가 excutor에서 resolve 함수가 호출되면 “fulfilled”, reject 함수가 호출되면 “reject”로 변환된다.
- result : 처음에는 undefined 이며, resolve가 호출되면 value, reject가 호출되면 error를 반환한다.
- 하지만 state, result property 모두 직접 접근할 수 없다.(심볼로 설정되어 있나?? 아예 undefined가 나온다. 이유는 모르겟다.)
- then, catch, finally 메소드를 사용하여 상태나 결과값에 접근해야 한다.
Promise 객체의 then, catch, finally 메소드
- Promise 객체는 excutor 함수의 실행 결과를 처리하기 위해, then, catch, finally 3개의 메소드를 가진다.
then 메소드
- resolve나 reject 이벤트가 발생하면 콜되는 함수이다.
- 두개의 함수를 인자로 받는다. 각각의 함수는 인자를 하나씩 가지고 있고, 첫번째 함수는 promise가 resolve 되었을 때 실행되는 함수이고, 두번째 함수는 promise가 reject 되었을 때 실행되는 함수이다.
let promise = new Promise(function(resolve, reject){ setTimeout(() => resolve("done!"), 1000); }); promise.then(result => alert(result), error => alert(error)); /// 혹은 promise.then(result => alert(result)); // 작업이 성공된 경우에만 다루고 싶다면!! 하나만 적어도 유효 /// 두개의 .then을 사용한다면 두개다 실행된다.let promise = new Promise(function(resolve, reject){ setTimeout(() => reject(new Error("error")), 1000); }); promise.then(result => alert(result), error => alert(error)); promise.then(error => alert(error)) // 오류!! 작업이 실패한 경우를 then으로 다루고 싶아면 항상 두 인자 모두 적어줘야함.catch 메소드
- 에러가 발생한 경우만 다루고 싶다면, then(null, new Error) 같이 첫번째 인자를 null로 전달하면 되는데, 이와 같은 구문을 catch(new Error)를 써도 같은 작동을 합니다.
let promise = new Promise(function(resolve, reject){ setTimeout(() => reject(new Error("error")), 1000); }) promise.catch(error => alert(error));finally 메소드
- promise의 성공과 실패의 상관없이 마지막에 실행이 됨. 인자는 함수 하나를 받는데 해당 함수는 인자를 갖지 않음(전달 받는게 없다).
let promise = new Promise(function(resolve, reject){ setTimeout(() => reject(new Error("error")), 1000); }); promise.then(result => alert(result)); promise.catch(error => alert(error)); promise.finally(() => alert("promise 이행 완료"));
callback -> promise 예시
- 기존의 콜백 함수를 통해 체인 구조를 이루면, 코드가 복잡해지는 문제가 있는데, 이를 promise를 통해 어느정도 해결할 수 있다.
// callback function loadScript(src, callback){ let script = document.createElement('script'); script.src = src; script.onload = () => callback(script); script.onerror = () => callback(null, new Error("에러발생")); document.head.append(script); } loadScrript('test.js', (script, error) => {alert(script.src);});// promise function loadScript(src){ return new Promise(function(resolve, reject){ let script = document.createElement('script'); script.src = src; script.onload = () => resolve(script); script.onerror = () => reject(new Error("error")); document.head.append(script); }) } let promise = loadScript("test.js"); promise.then(script => alert("good")); promise.catch(error => alert(error)); // or promise.then((null, error) => alert(error));
프라미스 체이닝
- 비동기 작업을 순차적으로 처리하도록 함.
- 아래 코드에서 보면 then 메소드의 인자 함수는 value를 리턴한다. 하지만 then 함수는 promise 객체를 리턴한다.
- 또한 then 메소드의 인자 함수가 value를 리턴한다면 then 함수는 리턴한 promise 객체의 resolve(value) 함수를 call 해서 다음 then 함수가 호출되도록 할 수 있다.
- 따라서 아래 코드는 console에 2, 4, 8이 찍힌다.
new Promise(function(resolve, reject) { setTimeout(() => resolve(1), 1000); }).then(function(result){ console.log(result); return result * 2; }).then(function(result){ console.log(result); return result * 2; }).then(function(result){ console.log(result); return result * 2; })- 첫번째 예시에서는 then 메소드의 인자 함수는 값(value)이 리턴됐다. 하지만 그 다음 then 메소드도 호출되었다. 이것은 내부적으로 then 메소드는 Promise 객체를 생성해서 resolve(value) 함수를 실행하기에 가능하다. 아래 예시는 그것을 실제 구현한 코드이다.
new Promise(function(resolve, reject) { setTimeout(() => resolve(1), 1000); }).then(function(result){ console.log(result); return new Promise((resolve, reject) => { resolve(result*2); }) }).then(function(result){ console.log(result); return new Promise((resolve, reject) => { resolve(result*2); }) }).then(function(result){ console.log(result); return new Promise((resolve, reject) => { resolve(result*2); }) })
- 아래 코드는 체인으로 연결되지 않는다. 독립적으로 처리할 뿐이다.
let promise = new Promise(function(resolve, reject){ setTimeout(() => resolve(1), 1000); }); promise.then(function(result){ alert(result); // 1 return result * 2; }); promise.then(function(result){ alert(result); // 1 return result * 2; }); promise.then(function(result){ alert(result); // 1 return result * 2; });Promise 체이닝 추가 예시
- 아래 코드는 Promise 객체를 변수에 저장하지 않고 바로 생성해서 then 메소드를 붙여준 것.
- Promise 객체는 생성과 동시에 excutor 함수가 실행되기 때문에 then 메소드가 순차적으로 콜된다.
new Promise(function(resolve, reject){ setTimeout(() => resolve(1), 1000); }).then(function(result){ alert(result); // 1 return new Promise((resolve, reject) => { setTimeout(() => resolve(result*2), 1000); }); }).then(function(result){ alert(result); // 2 return new Promise((resolve, reject) => { setTimeout(() => resolve(result*2), 1000); }); }).then(function(result){ alert(result); // 4 return new promise((resolve, reject) => { setTimeout(() => resolve(result*2), 1000); }); });- 아래 코드는 test1.js, test2.js, test3.js를 비동기 순차적으로 로딩을 한다.
loadScript("test1.js") .then(function(script){ return loadScript("test2.js"); }).then(function(script){ return loadScript("test3.js"); });
Thenable
- Promise의 then 메소드는 Promise 객체를 반환하거나, Value를 반환할 경우 자체적으로 Promise 객체를 생성 및 반환한다.
- 하지만 Promise 객체의 then 메소드가 아닌 then 메소드를 가지는 다른 객체를 반환할 수도 있다.
- 이러한 객체를 thenable 객체라고 부르며, then 메소드를 가지고 있어야 하며, (resolve, reject) 인자를 가지는 함수여야만 한다.
- 사실상 then 메소드가 아닌 excutor 함수를 가지는 객체라고 볼 수 있다.
- thenable 객체의 then 메소드는 resolve(result)를 호출하거나, reject(error)를 호출해야만 한다.
function Thenable(){ this.num = 2; this.then = function(resolve, reject){ setTimeout(() => resolve(this.num * 2)); }; } new Promise(resolve => resolve(1)) .then(result => { return new Thenable(); }).then(alert);
fetch와 체이닝 함께 응용하기
- fetch 함수는 url을 입력으로 받고, 해당 url로 네트워크 요청을 보낸다.
- 해당 url의 서버가 응답을 보내면 fetch 함수는 Promise 객체를 리턴하며, excutor 함수에서 resolve(response) 함수를 실행한다.
// "jongyeon/test.js" 주소에서 파일을 읽는 요청을 보내고 // 해당 파일을 text 형식으로 알림한다. fetch('jongyeon/test.js') .then(function(response){ return response.text(); // response.text() 함수는 Promise 객체를 return 하면서 resolve(text) 함수를 call 한다. }).then(function(text){ alert(text); });- 비동기(Asynchronous)은 항상 promise 객체를 리턴하도록 하는게 좋다.
- 현재는 chain을 확장할 계획이 없더라도 나중에는 언제라도 chain 확장을 손쉽게 할 수 있기 때문이다.
- 또한 재사용 가능하도록 함수 단위로 분리하는게 좋다.
// "/article/promise-chaining/user.json" 에서 파일을 읽는 요청을 보내고 // 파일에서 user name의 정보를 읽어 해당 유저의 깃허브에 요청을 보낸다. // 깃허브에서 이미지를 읽어와서 창에 보여주자. function loadJson(url){ return fetch(url).then(response => response.json()); } function loadGithubUser(name){ return fetch(`https://api.github.com/users/${name}`).then(response => response.json()); } function showAvatar(githubUser){ return new Promise(function(resolve, reject){ let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.append(img); setTimeout(() => { img.remove(); resolve(githubUser); }, 3000); }); } // loadScript('/article/promise-chaining/user.json') .then(user => loadGithubUser(user.name)) .then(showAvatar) .then(githubUser => alert(`Finished showing ${githubUser.name}`));
Promise와 에러 처리
- 콜백 함수를 사용해서 에러처리를 했던 경우에는 error first callback 기법을 사용해서 처리해주었다.
- Promise를 사용할 경우 rejection 함수를 사용하여 catch 메소드에서 다루어주면 된다.
- catch 메소드는 첫번째 핸들러일 필요는 없고 여러 개의 then 뒤에 올 수 있다.
fetch('/article/promise-chaining/user.json') .then(response => response.json()) .then(user => fetch(`https://api.github.com/users/${user.name}`)) .then(githubUser => new Promise(resolve, reject) => { let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.append(img); setTimeout(() => { img.remove(); resolve(githubUser); }, 3000); }) .catch(error => alert(error.message));에러 다시 던지기
- 일반 try…catch 구문에서는 catch 구문에서 에러를 분석하고, 처리할 수 없는 에러라고 판단이 되면 에러를 다시 던질 때가 있다.
- Promise에서도 이와 유사한 일을 할 수 있다.
new Promise((resolve, reject) =>{ throw new Error("에러 발생"); }).catch(function(error) { if (error instanceof URIError){ // 에러 처리 } else { alert("처리할 수 없는 에러"); throw error; // 에러 다시 던지기! } }).then(function() { // 에러가 잘 처리되었으면 여기로 제어가 이동 }).catch(error => { // 에러가 잘 처리되지 않았으면 여기로 제어가 이동 });
Promise의 API
- Promise 객체는 5가지 메소드(all, allSettled, race, resolve, reject)를 가지고 있다. (resolve, reject…?)
Promise.all()
- 여러 개의 Promise들을 동시에 실행시키고 모든 Promise가 준비될 때까지 기다릴려고 할 때 사용한다.
- 예를 들면 여러 개의 URL에 동시에 요청을 보내고, 다운로드가 모두 완료된 후에 일괄적으로 처리할 때 쓸 수 있다.
- Promise.all 메소드에 들어가는 순서에 따라 출력이 된다.
// 3초 후에 resolve([1,2,3])이 실행된다. Promise.all([ new Promise(resolve => setTimeout(() => resolve(1), 1000)), new Promise(resolve => setTimeout(() => resolve(2), 2000)), new Promise(resolve => setTimeout(() => resolve(3), 3000)) ]).then(alert);// 3개의 url에 동시에 요청을 보내고 응답을 받기. let urls = [ 'https://api.github.com/users/iliakan', 'https://api.github.com/users/remy', 'https://api.github.com/users/jeresig' ] let requests = urls.map(url => fetch(url)); // [promise1, promise2, promise3] 이 생성되고 각각의 excutor 함수가 실행된다. Promise.all(requests) // 꼭 Promise 객체가 생성되는 것을 넣지 않아도 된다. 기존 Promise 객체를 넣어도 된다. .then(responses => { for(let response of responses){ alert(`${response.url}: ${response.status}`); // 정상적으로 응답이 오면 모든 url의 응답코드가 200입니다. } return responses; }.then(responses => Promise.all(responses.map(r => r.json()))) .then(users => users.forEach(user => alert(user.name)));- Promise.all에 전달되는 Promise 중 하나라도 거부되면, Promise.all이 반환하는 Promise는 에러와 함께 거부된다.
- 에러가 발생하면 정상적으로 이행된 Promise도 무시된다.
Promise.all([ new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)), new Promise((resolve, reject) => setTimeout(() => reject(new Error("에러 발생")))), new Promise((resolve, reject) => setTimeout(() => resolve(2), 1000)) ]).catch(alert);- Promise.all은 Promise 객체가 아닌 일반 value도 입력으로 받는다. 입력받은 value 들은 그대로 resolve(value)가 실행된다.
Promise.all([ new Promise(resolve => setTimeout(() => resolve(1), 1000), 2, 3 ]).then(alert); // 1, 2, 3Promise.allSettled
- Promise.all 메소드는 하나의 Promise 라도 이행에 실패하면 전체 Promise가 이행되지 않는다.
- 이를 해결하기 위해 나온 것이 allSettled 메소드이며, 몇개의 Promise가 이행되지 않더라도 then 메소드가 실행된다.
- allSettled 메소드는 결과로 출력된 배열의 요소들은 두개의 property를 가지고 있다.
- 응답에 성공했을 경우 - {status:”fulfilled”, value:result}
- 응답에 실패했을 경우 - {status:”rejected”, reason:error}
let urls = [ 'https://api.github.com/users/iliakan', 'https://api.github.com/users/remy', 'https://no-such-url' ]; Promise.allSettled(urls.map((url => fetch(url)))) .then(results => { results.forEach((result, num) => { if (result.status == "fulfilled"){ alert(`${urls[num]}: ${result.value.status}`); } if (result.status == "rejected"){ alert(`${urls[num]}: ${result.reason}`); } }); });Promise.race
- 여러 Promise를 입력으로 받은 후에 가장 먼저 실행되는(background에서 가장 먼저 위로 콜되는) Promise를 반환한다.
Promise.race([ new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)), new Promise((resolve, reject) => reject(new Error("에러 발생"))), new Promise((resolve, reject) => resolve(() => resolve(3), 3000)) ]).then(alert); // 1
Promisification
- 콜백 기반의 함수를 Promise 어법상에서도 돌아가도록 만들어주는 것, 감싸 안은 것.
// 콜백 기반의 함수 // usage : // loadScript('test.js', (err, script) => {스크립트가 로드되고 할 일}); function loadScript(src, callback) { let script = document.createElement('script'); script.src = src; script.onload = () => callback(null, script); script.onerror = () => callback(new Error("에러 발생")); document.head.append(script); } // promisification // usage : // loadScriptPromise('test.js').then((result) => {스크립트가 로드되고 할 일}) let loadscriptPromise = function(src){ // src라는 인자를 받아야하기 때문에 Promise 객체가 아닌 function으로 설정 return new Promise((resolve, reject) => { loadScript(src, (err, script) => { if (err) { reject(err); } else { resolve(script); } }); }); }- 아래의 코드는 콜백 함수를 입력 받고 promisification 해주는 함수를 구현한 것이다.
function promisify(f) { return function(...args) { return new Promise((resolve, reject) => { function callback(err, result) { if (err) { reject(err); } else { resolve(result); } } args.push(callback); f.call(this, ...args); // 여기의 this는 global이다. 근데 왜 this를 써줬을까...? }) } } let pf = promisify(loadScript); pf("test.js").catch(err => {});- Promisification는 콜백을 완전히 대체하지는 못한다.
- Promise는 하나의 결과만 가질 수 있지만, 콜백은 여러 번 호출할 수 있기 때문이다…?
- 따라서 Promisification은 콜백을 단 한번 호출하는 함수에만 적용해야 한다.

