웹 브라우저가 서버와 통신하기 때문
웹 기초 다지기
fetch함수 사용해보기
fetch('https://www.google.com')
.then((response)=> response.text())
.then((result)=>{console.log(result);});
JavaScript
복사
출력내용 → 웹 브랄우저가 서버로부터 받은 내용
html과 javascript 내용이다.
fetch함수 살펴보기
// request -> 요청
// response -> 응답
// fetch -> 가져오다, 서버에 request를 보내서 값을 가져오는 것
fetch('https://www.google.com')
.then((response) => response.text()) // 어떤 일이 일어나고 발생하는 함수
.then((result)=> {console.log(result);}); //
//promise객체의 then메소드는 콜백을 등록하는 메소드
//서버의 response가 왔을 때 콜백이 실행된다.
//이전 콜백의 return값을 다음 콜백이 넘겨받는다.
// response.text()가 result가 된다.
서버의 응답을 받아서 response객체로 넘긴다.
JavaScript
복사
response 객체
fetch('https://www.google.com')
.then((response) => response.text()) // 어떤 일이 일어나고 발생하는 함수
.then((result)=> {console.log(result);}); //
JavaScript
복사
fetch함수는 어떤 객체를 리턴하는데(promise객체)
이 객체의 then 메소드로, '리스폰스가 왔을 때 실행할 콜백'을 등록할 수 있다.
이렇게 등록된 콜백들은 then메소드로 등록된 순서대로 실행되고, 이전 콜백의 리턴값을 넘겨받아서 사용할 수 있다.
response ⇒ 내용만 있는게 아니라 객체다. 각종 부가 정보들과 실제 내용들이 합쳐짐
웹이란?
URL → uniform Resource Locator
웹에 존재하는 특정 데이터를 나타내는 문자열
Host → 해당 도메인 주소에 해당하는 서버를 특정
Path → 경로, 서버에 있는 데이터 중 원하는 데이터를 특정, 해당 서비스를 설계하고 만든 개발자들이 정하는 것이다.
쿼러 → 존재할 때도 있고 아닐 때도 있는데, 데이터에 관한 세부적인 요구사항, URL 끝에 쿼리를 붙여준다.
https:
스킴(Scheme) → 프로토콜의 이름
프로토콜 : 통신을 하는 두 주체가 지켜야하는 통신 규약
https → 프로토콜의 이름 통신을 할 때 https 프로토콜을 지킨다.
http → hyper text transfer protocol,
hyper text → 다른 텍스트에 대한 참조를 갖고 있는 텍스트
이미지, 영상 등등을 http로 전달한다.
http에다가 보안성이 더해진 https를 쓴다. https: 안전한 http
네트워크 탭
각각 한줄 : 하나의 리퀘스트 리스폰스 쌍이다.
Web API 배우기
Json이란?
Json → 데이터를 나타내는 객체
//프로퍼티를 큰따옴표로 감싸줘야한다.
//문자열을 ""처럼 항상 큰따옴표로 감싸줘야한다.
const memeber = {
"name":"Michael Kim",
"height": 180,
"weight": 70,
"hobbies":["basketball","Listening to music"]
}
JavaScript
복사
Json데이터를 객체로 변환하기
fetch('https://jsonplaceholder.typicode.com/users')
.then((response)=> response.text())
.then((result)=>{ const users = JSON.parse(result)
console.log(users.length);
users.forEach((user)=>{
console.log(user.name)
});
});
//Json.parse()를 통해 객체로 변환해줬다
JavaScript
복사
메소드의 의미
요청 → 데이터의 조회, 데이터 추가, 데이터 수정, 데이터 삭제
데이터 조회: GET
데이터 추가: POST
데이터 수정: PUT
데이터 삭제: DELETE
Request → Head / Body
Head : Request에 대한 부가 정보
Body: 실제 데이터를 담는 부분
Post, Put ⇒ body에 데이터를 담아서 보내준다
GET, DELETE ⇒ body가 필요하지 않다.
네트워크 탭 ⇒ 웹 브라우저가 보낸 request와 response를 직접 볼 수 있다.
fetch함수로 보낸 request와 response를 볼 수 있다.
POST request 보내기
//get reqeust
fetch('https://learn.codeit.kr/api/members')
.then((response) => response.text())
.then((result) => { console.log(result);
});
//id가 3인 애만 데리고 온다.
fetch('https://learn.codeit.kr/api/members/3')
.then((response) => response.text())
.then((result) => { console.log(result);
});
//post request
const newMember = {
name: 'Jerry',
email: 'jerry@codeitmail.kr',
department: 'engineering',
};
fetch('https://learn.codeit.kr/api/members',{
method: 'POST',
body: JSON.stringify(newMember),//JS객체를 Json객체로 바꾼다.
}//option 객체
)
.then((response) => response.text())
.then((result) => {console.log(result);});
JavaScript
복사
PUT request, DELETE request
//put request
const Member = {
name: 'Alice',
email: 'alice@codeitmail.kr',
department: 'marketing',
};
fetch('https://learn.codeit.kr/api/members/2',{
method: 'PUT',
body: JSON.stringify(Member),//JS객체를 Json객체로 바꾼다.
}//option 객체
)
.then((response) => response.text())
.then((result) => {console.log(result);});
//DELETE request
fetch('https://learn.codeit.kr/api/members/2',{
method: 'DELETE',
}//option 객체
)
.then((response) => response.text())
.then((result) => {console.log(result);});
JavaScript
복사
(1) URL은 리소스를 나타내기 위해서만 사용하고, 리소스에 대한 처리는 메소드로 표현해야 합니다.
(2) 도큐먼트는 단수 명사로, 컬렉션은 복수 명사로 표시합니다.
하나의 객체로 표현할 수 있는 리소스를 '도큐먼트'라고 합니다. 그리고 여러 개의 '도큐먼트'를 담을 수 있는 리소스를 '컬렉션'이라고 하는데요. 쉽게 비유하자면, 도큐먼트는 하나의 '파일', 컬렉션은 여러 '파일'들을 담을 수 있는 하나의 '디렉토리'에 해당하는 개념입니다.
deserialization 한번에 수행하기
fetch('https://jsonplaceholder.typicode.com/users')
.then((response) => response.json())
.then((result) => { const users = result; });
JavaScript
복사
response객체의 json()메소드 사용하기
Status Code
Response ⇒ Head, Body
network 탭에 headers → Status Code(상태코드)
상태코드 → request를 받은 서버가 작업 결과를 나타내기 위해서 헤더에 넣는 숫자
200 : 서버가 정상 처리를 했다.
404: 해당 URL에 해당하는 데이터를 찾을 수 없다.
Content-type
Content-type 헤더
데이터가 어떤 타입인지를 나타낸다
주타입/서브타입 의 형식으로 나타낸다
1.
주 타입이 text인 경우
text/css
text/html
text/javascript
2.
주 타입이 image인 경우
image/bmp
image/gif
image/png
3.
주 타입이 audio인 경우
audio/mp4
audio/ogg
4.
주 타입이 video인 경우
video/mp4
video/H264
5.
위 타입에 속하지 않으면 → application
application/json
application/octet-stream
const newMember = {
name: 'Jerry',
email: 'jerry@codeit.kr',
department: 'engineering',
};
fetch('https://learn.codeit.kr/api/members', {
method: 'POST',
headers: { // 추가된 부분
'Content-Type': 'application/json',
},
body: JSON.stringify(newMember),
})
.then((response) => response.text())
.then((result) => { console.log(result); });
JavaScript
복사
content-type 심화
1. JSON 말고 XML도 있어요.
2. form 태그에서 사용되는 타입
(1) application/x-www-form-urlencoded 타입
id=6&name=Jason&age=34&department=engineering
이런 식으로 보낸다.
(2) multipart/form-data 타입
text/html, vidoe/mp4, application/json 등 모두 데이터 하나의 타입을 나타냈었죠? 그런데 이 multipart/form-data는 좀 특별합니다. multipart(여러 개의 파트)라는 단어에서도 유추할 수 있듯이 이 값은 여러 종류의 데이터를 하나로 합친 데이터를 의미하는 타입입니다.
multipart/form-data 타입의 데이터는 그 안에 여러 종류의 데이터들이 들어있다고 했습니다. 따라서 서버가 이것을 받았을 때 각 데이터를 구분할 수 있도록 이런 boundary 값이 필요한데요.
<형식>
Content-Disposition: form-data; name=데이터의 이름
// blank line
값
그 외 기타
데이터를 수정하는 메소드 중에서 PUT은 덮어쓰기, PATCH는 일부 수정를 의미한다는 사실을 기억해두세
비동기 실행과 promise 객체
fetch함수와 비동기 실행
console.log('start!');
fetch('https://jsonplaceholder.typicode.com/users')
.then((response)=> response.text())//콜백을 등록만 한다.
.then((result)=> {console.log(result);});//콜백을 등록만한다.
console.log('End'); // 등록하고 나면 바로 End가 출력됨
//start, end , 서버 응답
JavaScript
복사
이와 다르게 비동기 실행은 한번 작업을 시작해두고, 그 작업이 완료되기 전이더라도 콜백만 등록해두고, 코드의 실행 흐름이 바로 그 다음 코드로 넘어갑니다. 그리고 추후에 특정 조건이 만족되면 콜백이 실행됨으로써 해당 작업을 완료하는 방식이죠. 따라서 비동기 실행에서는 코드가 꼭 등장하는 순서대로 실행되는 것이 아닙니다. 그래서 코드를 해석할 때 주의해야 하는데요.
알아야하는 비동기 실행 함수들
setTimeout(function, time);
콜백함수, 시간
콜백함수를 시간 만큼 미룬다.
setInterval(콜백, 시간);
콜백함수를 시간 간격마다 계속 실행한다.
addEventListenter()
fetch함수는 Promise객체를 리턴한다.
promise객체란?
작업에 관한 '상태정보'를 갖고 있는 객체
promise.then()은 promise객체의 메소드이다
promise → pending, fulfilled, rejected
pending: 진행중, fulfilled: 완료, rejected: 중단
fulfilled 상태가 되면 작업 성공 결과도 함께 가진다.
rejected 상태가 되면 작업 실패 이유에 대한 정보도 갖게 된다.
then메소드 → promise객체의 메소드
pending → fulfilled 상태가 될 때 콜백 함수를 호출
Promise Chaining
then()메소드 뒤에는 계속 메소드를 붙일 수 있다.
then()메소드가 promise객체를 리턴한다.
then메소드가 리턴한 promise 상태는 원래 pending상태인데, 리턴하는 값에 따라 상태가 달라진다.
콜백에서 1. promise객체를 리턴하든가, 2. promise객체가 아닌 객체를 리턴한다.
1.
만약 promise객체를 리턴하는 경우, then 메소드의 promise객체도 같은 상태를 가진다.
2.
그냥 리턴을 받으면 fulfilled상태가 되고, 작업 결과를 받는다.
1. text 메소드
fetch 함수로 리스폰스를 잘 받으면, response 객체의 text 메소드는, fulfilled 상태이면서 리스폰스의 바디에 있는 내용을 string 타입으로 변환한 값을 '작업 성공 결과'로 가진 Promise 객체를 리턴합니다. 문장이 조금 기니까 반복해서 읽어보세요. 이때 그 작업 성공 결과는 string 타입인데요. 이때 그 값이 만약 JSON 데이터라면 이전에 배운 것처럼 JSON 객체의 parse 메소드로 Deserialize를 해줘야합니다.(JSON.parse(result);)
2. json 메소드
fetch 함수로 리스폰스를 잘 받으면, response 객체의 json 메소드는, fulfilled 상태이면서, 리스폰스의 바디에 있는 JSON 데이터를 자바스크립트 객체로 Deserialize해서 생겨난 객체를 '작업 성공 결과'로 가진 Promise 객체를 리턴합니다. 만약 리스폰스의 바디에 있는 내용이 JSON 타입이 아니라면 에러가 발생하고 Promise 객체는 rejected 상태가 되면서 그 '작업 실패 정보'를 갖게 됩니다.
Promise Chaining이 필요한 경우
비동기 작업을 순차적으로 나타내야할 때, 전체적인 코드를 깔끔하게 나타내기 위해서
rejected 상태가 되면 실행할 콜백
fetch('https://jsonoplaceholder.typicode.com/users')
.then((response)=>response.text(),(error)=>{console.log(error);})
.then((result)=>{console.log(result);});
JavaScript
복사
2. 실행된 콜백이 아무 값도 리턴하지 않는 경우
자바스크립트에서는 함수가 아무것도 리턴하지 않으면 undefined를 리턴한 것으로 간주됩니다. 따라서 방금 전 1. (2) 규칙에 따라 then 메소드가 리턴했던 Promise 객체는 fulfilled 상태가 되고, 그 작업 성공 결과로 undefined를 갖게 됩니다.
3. 실행된 콜백 내부에서 에러가 발생했을 때
then 메소드가 리턴한 Promise 객체가 rejected 상태가 되고, 작업 실패 정보로 해당 에러 객체를 갖게 됩니다.
4. 아무런 콜백도 실행되지 않을 때
이전 Promise 객체와 동일한 상태와 결과를 갖게 됩니다.
catch 메소드
fetch('https://jsonlaceholder.typicode.com/users')
.then((response)=> response.text())
.catch((error) => {console.log(error);})//.then(undefined,(error)=>{console.log(error);})
.then((result)=>{console.log(result);});
JavaScript
복사
catch 메소드는 마지막에 쓴다.
rejected 상태를 처리해주지 않으면, 브라우저는 에러를 처리하지 못한다.
에러를 처리하기 위해서 catch 메소드를 맨 아래에 내린다.
finally 메소드
fetch('https://jsonlaceholder.typicode.com/users')
.then((response)=> response.text())
.then((result)=>{console.log(result);})
.catch((error) => {console.log(error);})//.then(undefined,(error)=>{console.log(error);})
.finally(()=>{consle.log('exit');});
JavaScript
복사
항상 무조건 실행해야하는 코드가 있을 때 사용한다.
로그 기록을 남긴다.
특정 변수의 값을 변경해줘야할 때
직접 만들어보는 Promise 객체
const p = new Promise((resolve,reject)=>{
//executor 함수
//resolve -> 생성될 promise객체를 fulfilled상태로 만들 수 있는 함수
//reject > 생성될 promise 객체를 rejected상태로 만들 수 있는 함수
setTimeout(()=> { resolve('success');},2000);
setTimeout(()=>{reject( new Error('fail'));},2000);
});
JavaScript
복사
언제 promise객체를 생성하는가
1. setTimeout 함수 예시
예를 들어 이런 wait이라는 함수가 있다고 합시다.
function wait(text, milliseconds) {
setTimeout(() => text, milliseconds);
}
Plain Text
복사
wait 함수는 특정 밀리세컨즈만큼 시간이 지난 후에 text 파라미터로 전달받은 값을 리턴하는 함수입니다. 지금 보이는 setTimeout 함수는 이전에 '알아야하는 비동기 실행 함수들' 노트에서 배웠었죠? 이 wait 함수를 Promise Chaining 코드에서 사용해볼게요.
fetch('https://jsonplaceholder.typicode.com/users')
.then((response) => response.text())
.then((result) => { console.log(result); });
Plain Text
복사
바로 이 Promise Chaining 코드에 wait 함수를 추가해볼 건데요. 이렇게 써보겠습니다.
function wait(text, milliseconds) {
setTimeout(() => text, milliseconds);
}
fetch('https://jsonplaceholder.typicode.com/users')
.then((response) => response.text())
.then((result) => wait(`${result} by Codeit`, 2000))// 2초 후에 리스폰스의 내용 뒤에 'by Codeit' 추가하고 리턴
.then((result) => { console.log(result); });
Plain Text
복사
기존 코드에 두 번째 then 메소드를 추가하고, 그 안에서 wait 함수를 호출했습니다. 이렇게 쓰면 2초 후에 리스폰스의 내용 뒤에 by Codeit이라는 문구를 붙여서 출력될 것 같은데요. 정말 그렇게 되는지 확인해봅시다.
코드를 실행해보면,
리스폰스의 내용과 by Codeit이 출력되지 않았습니다. 그 대신 undefined가 출력되었는데요.
왜 그런 걸까요?
그 이유는 바로 wait 함수에 있습니다.
function wait(text, milliseconds) {
setTimeout(() => text, milliseconds);
}
Plain Text
복사
이 wait 함수는 내부에서 setTimeout 함수를 호출합니다. 그리고 setTimeout 함수의 첫 번째 파라미터로 들어간 콜백이 2초 후에 text를 리턴하죠. 그런데 여기서 혼동하면 안 되는 것은 wait 함수가
...
.then((result) => { return wait(`${result} by Codeit`, 2000); })
...
Plain Text
복사
이 두 번째 then 메소드 안의 콜백에서 실행될 때,
wait 함수는 setTimeout 함수를 실행할 뿐 아무것도 리턴하지 않는다는 사실입니다.
setTimeout 함수 안의 콜백이 2초 후에 리턴하는 text는, wait 함수의 리턴값이 아닙니다.
이 사실에 유의해야 하는데요. wait 함수는 단지 setTimeout 함수를 실행하고 아무것도 리턴하지 않는 함수일 뿐입니다. 그리고 자바스크립트에서는 이전에 배운대로 함수에서 아무것도 리턴하지 않으면 undefined를 리턴하는 것으로 간주하기 때문에 wait 함수의 리턴값은 undefined입니다.
따라서 세 번째 then 메소드의 콜백으로 undefined가 넘어가고, 그래서 위 이미지에서 보이는 것처럼 undefined가 출력된 겁니다.
setTimeout은 비동기 실행되는 함수인데요. Promise Chaining 안에서 이렇게 비동기 실행되는 함수를 바로 사용하면, 나중에 실행되는 부분의 리턴값(여기서는 text)를 Promise Chain에서 사용할 수 없게 됩니다.
이 문제를 해결하려면 이전 영상에서 배웠던 Promise 객체를 직접 생성하는 방법을 사용하면 됩니다. wait 함수를 이렇게 수정해볼게요.
// function wait(text, milliseconds) {// setTimeout(() => text, milliseconds);// }function wait(text, milliseconds) {
const p = new Promise((resolve, reject) => {
setTimeout(() => { resolve(text); }, 2000);
});
return p;
}
Plain Text
복사
지금 wait 함수 안에서 Promise 객체를 직접 생성했고, executor 함수 안에서 setTimeout 함수를 호출했습니다. 그리고 setTimeout 함수 안의 콜백에서 resolve 함수를 호출하는데 이 때 그 아규먼트로 text를 넣었습니다. 그렇다면 Promise 객체 p는 2초 후에 fulfilled 상태가 될 것이고, 그 작업 성공 결과는 파라미터 text의 값이 될 될 것입니다. wait 함수는 이제 Promise 객체 p를 리턴합니다.
자, 이 상태에서 코드를 다시 실행해보면
function wait(text, milliseconds) {
const p = new Promise((resolve, reject) => {
setTimeout(() => { resolve(text); }, 2000);
});
return p;
}
fetch('https://jsonplaceholder.typicode.com/users')
.then((response) => response.text())
.then((result) => wait(`${result} by Codeit`, 2000))// 2초 후에 리스폰스의 내용 뒤에 'by Codeit' 추가하고 리턴
.then((result) => { console.log(result); });
Plain Text
복사
이번에는 약 2초 후에 리스폰스의 내용이 잘 출력되고,
리스폰스의 내용 맨 마지막에는 by Codeit이라는 문구가 잘 붙어서 출력되는 것을 알 수 있습니다.
방금처럼 기존의 비동기 실행 함수(여기서는 setTimeout)의 콜백이 리턴하는 값을 Promise Chain에서 사용하고 싶다면, 해당 함수를 감싸서 Promise 객체를 직접 생성하는 코드를 작성해야 합니다. 그리고 그 Promise 객체를 리턴해야 방금처럼 Promise Chain에서 해당 리턴값을 받아서 사용할 수 있습니다.
이렇게 전통적인 형식의 비동기 실행 함수를 Promise 객체로 감싸서 그 Promise 객체를 리턴하는 형식으로 만드는 작업을 Promisify(프로미스화하다)라고 하는데요. 앞으로도 이 Promisify라는 용어를 사용하겠습니다. 계속 내용을 읽어봅시다.
2. 콜백 헬(callback hell)과 Promise
이번에는 Promisify의 또 다른 예시를 보겠습니다. 그런데 이번에는 브라우저가 아니라 조금 다른 환경에서의 코드를 볼 건데요. 바로 Node.js라고 하는 환경입니다. 오늘날 자바스크립트가 실행되는 환경에는 웹 브라우저뿐만 아니라 Node.js라고 하는 것도 있습니다. 이 Node.js는 오늘날 자바스크립트를 서버에서도 실행할 수 있게 해주는 또 다른 '자바스크립트 실행 환경'인데요. 이 Node.js에서는 브라우저에서와는 또 다른 비동기 함수들이 제공됩니다. (Node.js가 뭔지 더 궁금하신 분들은 이 영상을 참고하세요.)
Node.js에는 다음과 같이 특정 파일의 내용을 읽기 위해 사용되는 readFile이라는 비동기 실행 메소드가 있습니다.
fs.readFile('file1.txt', 'utf8', (error, data) => {
if (err) {
console.log(err);
} else {
console.log(data);
}
});
Plain Text
복사
여기서 fs는 readFile 메소드를 가진 객체로, 파일에 관한 기능들을 갖고 있습니다. 일단 여기서 당장 중요한 내용은 아니니까 readFile 메소드에만 집중합시다. readFile 메소드는 첫 번째 파라미터로 파일의 이름, 두 번째 파라미터로 파일 해석 기준(인코딩 기준), 세 번째 파라미터로 콜백을 받는데요. readFile 함수는 파일을 읽다가 에러가 발생하면 콜백의 첫 번째 파라미터(error)에, 해당 에러 객체를 전달하고 콜백을 실행합니다. 만약 파일을 정상적으로 다 읽었으면 콜백의 두 번째 파라미터(data)에, 읽어들인 파일 내용을 전달하고 콜백을 실행하는데요.
이 readFile 메소드도, 콜백을 파라미터에 바로 넣는 비동기 실행 함수라는 점에서 setTimeout 함수, addEventListener 메소드와 비슷합니다. 그런데 이런 형식의 함수(또는 메소드)들은 한 가지 단점이 있다고 했었죠?(참고) 그건 바로 콜백 헬(callback hell) 문제입니다. 예를 들어, 위 코드에서 이제 file1.txt 파일의 내용을 출력하고 나서 그 다음에 file2.txt라는 파일의 내용을 또 출력해야한다고 해봅시다. 그럼 코드가 이렇게 되겠죠?
fs.readFile('file1.txt', 'utf8', (error1, data1) => {
if (error1) {
console.log(error1);
} else {
console.log(data1);
fs.readFile('file2.txt', 'utf8', (error2, data2) => {
if (error2) {
console.log(error2);
} else {
console.log(data2);
}
});
}
});
Plain Text
복사
이렇게 코드를 쓰면 file1.txt의 내용이 출력되고, 그 다음에 file2.txt의 내용이 출력될 겁니다. 코드가 좀 복잡해졌지만 아직은 읽을만한 것 같습니다. 그런데 이제 그 다음으로 file3.txt의 내용도 출력해야 한다고 해봅시다.
그렇다면
fs.readFile('file1.txt', 'utf8', (error1, data1) => {
if (error1) {
console.log(error1);
} else {
console.log(data1);
fs.readFile('file2.txt', 'utf8', (error2, data2) => {
if (error2) {
console.log(error2);
} else {
console.log(data2);
fs.readFile('file3.txt', 'utf8', (error3, data3) => {
if (error3) {
console.log(error3);
} else {
console.log(data3);
}
});
}
});
}
});
Plain Text
복사
코드가 이렇게 됩니다. 이제 코드를 읽기 너무 어려워지지 않았나요?
콜백을 바로 파라미터에 집어넣는 전통적인 형식의 비동기 실행 함수들은 이런 문제가 있습니다. 바로 순차적으로 비동기 실행 함수들을 실행하려고 하면 콜백 안에 또 콜백이 있고, 그 안에 또 콜백이 있는 콜백 헬(콜백 지옥, callback hell) 현상을 초래하게 된다는 겁니다.
실제로 실무에서 개발을 하다 보면 이런 콜백 헬이 아주 큰 문제가 됩니다. 그런데 이런 함수들은 Promise 객체를 리턴하는 것도 아니고 애초에 이런 형식으로 정의되어 있기 때문에 문제를 해결하기가 어려워 보이는데요. 이 문제에 대한 대표적인 해결책이 바로 우리가 배운 Promisify입니다.
지금 이 readFile 메소드를 Promisify해보겠습니다.
function readFile_promisified(filename) {
const p = new Promise((resolve, reject) => {
fs.readFile(filename, 'utf8', (error, data) => {
if (error) {
reject(error);// 에러 발생 시 -> rejected
} else {
resolve(data);// 파일 내용 읽기 완료 -> fulfilled
}
});
});
return p;
}
Plain Text
복사
이런 식으로 readFile_promisified라는 이름의 함수를 정의했는데요. 지금 함수 안에서는 Promise 객체를 직접 생성하고 있습니다.
그리고 Promise 객체가 생성될 때 실행되는 executor 함수 안에서는 fs 객체의 readFile 메소를 호출했습니다.
여기서 중요한 것은 작업을 수행하다가 에러가 나면 readFile 함수의 콜백에서
... (error, data) => {
if (error) {
reject(error);// 에러 발생 시 -> rejected
} else {
resolve(data);// 파일 내용 읽기 완료 -> fulfilled
}
}
Plain Text
복사
reject 함수를 호출하고, 파일의 내용을 정상적으로 다 읽었을 때는 resolve 함수를 호출한다는 사실입니다. 그리고 reject 함수의 파라미터에는 error 객체를, resolve 함수의 파라미터에는 파일의 내용인 data를 전달했는데요. 이 각각은, 생성된 Promise 객체의 작업 실패 정보 또는 작업 성공 결과가 되겠죠?
이제 readFile 메소드를 Promisify해서 만든 readFile_promisified 함수를 사용해서 위의 콜백 헬 코드에서 작성했던 내용을 똑같이 작성해봅시다.
readFile_promisified('file1.txt')
.then((data) => { console.log(data); return readFile_promisified('file2.txt'); })
.then((data) => { console.log(data); return readFile_promisified('file3.txt'); })
.then((data) => { console.log(data); })
.catch((error) => { console.log(error); });
Plain Text
복사
짠! 어떤가요? 코드가 훨씬 깔끔해졌죠? readFile_promisified 함수는 Promise 객체를 리턴하기 때문에 이렇게 자유롭게 Promise Chain 안에서 사용할 수 있습니다.
이렇게 원하는 경우에는 전통적인 형식의 비동기 실행 함수를 Promisify해서 콜백 헬을 방지하고, 가독성 높은 코드를 작성할 수 있습니다.
3. Promisify를 하면 안 되는 함수들도 있습니다.
이제 기존의 전통적인 형식의 비동기 실행 함수도 원하는 경우에는 Promisify해서 콜백 헬을 방지할 수 있다는 것을 알게 되었습니다. 하지만 전통적인 형식의 비동기 실행 함수라고 해서 모두 Promisify해서 사용해도 되는 것은 아닙니다.
기존의 비동기 실행 함수들 중에서도 그 콜백을 한번만 실행하는 것들(setTimeout, readFile 등)만 Promisify해서 사용해도 되는데요.
이것들과 달리 만약 콜백을 여러 번 실행하는 함수들(setInterval, addEventListener 등)인 경우에는 이렇게 Promisify하면 안 됩니다. 왜냐하면 Promise 객체는 한번 pending 상태에서 fulfilled 또는 rejected 상태가 되고나면 그 뒤로는 그 상태와 결과가 바뀌지 않기 때문입니다. 이게 무슨 말인지 다음 코드를 보고 이해해봅시다.
const box = document.getElementById('test');
let count = 0;
function addEventListener_promisified(obj, eventName) {// 이런 Promisify는 하지 마세요const p = new Promise((resolve, reject) => {
obj.addEventListener(eventName, () => {// addEventListener 메소드
count += 1;
resolve(count);
});
});
return p;
}
addEventListener_promisified(box, 'click').then((eventCount) => { console.log(eventCount); });
Plain Text
복사
이 코드에서 보이는 addEventListener_promisified 함수는 DOM 객체의 addEventListener 메소드를 Promisify한 함수인데요.
지금 Promise 객체가 생성될 때 실행되는 executor 함수 안에서는, DOM 객체에 어떤 이벤트가 발생할 때, 실행할 콜백을 등록하고 있습니다.
특정 이벤트가 발생할 때마다 count라고 하는 변수의 값을 1씩 늘려서 resolve 함수의 파라미터로 전달해서 실행하도록 하는 내용이 들어있는데요.
마지막 코드를 보면,
addEventListener_promisified(box, 'click')
.then((eventCount) => { console.log(eventCount); });
Plain Text
복사
이렇게 addEventListener_promisified 함수의 아규먼트로 DOM 객체 box와 문자열 'click'을 넣어서 box 객체가 클릭 이벤트에 반응하도록 했습니다.
(HTML 코드는 생략된 상태입니다.)
하지만 이 코드를 실행하고 box를 클릭해보면
처음에 1이 딱 출력되고 나서 그 다음 count 값들은 출력되지 않습니다.
왜냐하면 pending 상태에 있던 Promise 객체(여기서는 p 객체)가 한번 fulfilled 상태 또는 rejected 상태가 되고 나면
Promise 객체의 상태 및 결과가 고정되어 그 뒤로는 바뀌지 않기 때문입니다.
따라서 지금 위 코드에 보이는 resolve(count)라고 하는 코드가 box 버튼을 클릭할 때마다 여러 번 실행된다고 해도 p 객체가 갖고 있는 상태와 결과는 변하지 않습니다. 그래서 then 메소드 안의 콜백도 처음 클릭했을 때 딱 한번 실행되고 끝인 겁니다.
이렇게 콜백이 여러 번 실행되어야하는 비동기 실행 함수인 경우에는 Promisify를 하면 안 됩니다. Promisify를 하고 싶은 경우라도, 콜백이 딱 한 번 실행되는 함수인 경우에만 해야한다는 사실, 잘 기억하세요
1. all 메소드
설명을 하기에 앞서 바로 코드를 보겠습니다.
// 1번 직원 정보const p1 = fetch('https://learn.codeit.kr/api/members/1').then((res) => res.json());
// 2번 직원 정보const p2 = fetch('https://learn.codeit.kr/api/members/2').then((res) => res.json());
// 3번 직원 정보const p3 = fetch('https://learn.codeit.kr/api/members/3').then((res) => res.json());
Promise
.all([p1, p2, p3])
.then((results) => {
console.log(results);// Array : [1번 직원 정보, 2번 직원 정보, 3번 직원 정보]
});
Plain Text
복사
지금 이 코드에는 서로 다른 3개의 URL로 리퀘스트를 보내는 fetch 함수들이 보입니다. URL을 자세히 보니 이전에 사용했던 직원 정보에 관한 학습용 URL이네요. 지금 1번, 2번, 3번 직원의 정보를 각각 요청하고 있죠?
그 다음 부분을 보면, Promise의 all이라는 메소드를 호출하고 있고, all 메소드의 아규먼트로는 배열 하나가 들어있습니다. 그 배열의 요소들은, 각 직원 정보를 요청하고 받아서 Deserialize까지 수행한 작업 성공 결과를 담고 있는 Promise 객체들인 p1, p2, p3 객체입니다.
이 all 메소드는 무슨 기능을 하는 걸까요? all 메소드도 then 메소드처럼 새로운 Promise 객체를 리턴하는데요.
all 메소드는 이렇게 아규먼트로 들어온 배열 안에 있는 모든 Promise 객체가 pending 상태에서 fulfilled 상태가 될 때까지 기다립니다.
그리고 모든 Promise 객체들이 fulfilled 상태가 되면, all 메소드가 리턴했던 Promise 객체는 fulfilled 상태가 되고,
각 Promise 객체의 작업 성공 결과들로 이루어진 배열을, 그 작업 성공 결과로 갖게 됩니다.
이 코드를 직접 실행해보면,
이렇게 all 메소드가 리턴한 Promise 객체는,
(1) 각 개별 Promise 객체의 작업 성공 결과로 이루어진 배열을
(2) 자신의 작업 성공 결과로 갖는다는 것을 알 수 있습니다.
배열의 각 요소로 각 직원 정보 객체가 잘 보이죠? 이렇게 all 메소드는 여러 Promise 객체의 작업 성공 결과를 기다렸다가 모두 한 번에 취합하기 위해서 사용합니다.
그런데 만약 p1~3 객체들 중 하나라도, rejected 상태가 되면 어떻게 될까요?
// 1번 직원 정보const p1 = fetch('https://learn.codeit.kr/api/members/1').then((res) => res.json());
// 2번 직원 정보const p2 = fetch('https://learn.codeit.kr/api/members/2').then((res) => res.json());
// 3번 직원 정보const p3 = fetch('https://learnnnnnn.codeit.kr/api/members/3').then((res) => res.json());
Promise
.all([p1, p2, p3])
.then((results) => {
console.log(results);// Array : [1번 직원 정보, 2번 직원 정보, 3번 직원 정보]
});
Plain Text
복사
마지막 fetch 함수에 존재하지 않는 URL 주소를 적고 코드를 다시 실행해보겠습니다. 코드를 실행해보면
마지막 fetch 함수에서 문제가 발생해서 p3가 rejected 상태가 되면,
all 메소드가 리턴한 Promise 객체는 p3 객체처럼 rejected 상태가 되고 동일한 작업 실패 정보를 갖게 됩니다.
이렇게 all 메소드는 하나의 Promise 객체라도 rejected 상태가 되면, 전체 작업이 실패한 것으로 간주해야 할 때 사용합니다.
그리고 이렇게 Promise 객체가 하나라도 rejected 상태가 되는 경우에 대비하려면
이전에 배웠던 것처럼
// 1번 직원 정보const p1 = fetch('https://learn.codeit.kr/api/members/1').then((res) => res.json());
// 2번 직원 정보const p2 = fetch('https://learn.codeit.kr/api/members/2').then((res) => res.json());
// 3번 직원 정보const p3 = fetch('https://learnnnnnn.codeit.kr/api/members/3').then((res) => res.json());
Promise
.all([p1, p2, p3])
.then((results) => {
console.log(results);// Array : [1번 직원 정보, 2번 직원 정보, 3번 직원 정보]
})
.catch((error) => {
console.log(error);
});
Plain Text
복사
그냥 이렇게 catch 메소드를 붙여주면 됩니다. 어차피 all 메소드도 Promise 객체를 리턴하니까 특별히 새로울 건 없겠죠?
2. race 메소드
race 메소드도 all 메소드와 마찬가지로 여러 Promise 객체들이 있는 배열을 아규먼트로 받습니다. 그리고 race 메소드도 all 메소드처럼 Promise 객체를 리턴하는데요. 하지만 그 적용 원리가 다릅니다.
race 메소드가 리턴한 Promise 객체는 아규먼트로 들어온 배열의 여러 Promise 객체들 중에서
가장 먼저 fulfilled 상태 또는 rejected 상태가 된 Promise 객체와 동일한 상태와 결과를 갖게 됩니다.
예를 들어 이런 코드가 있다고 할 때,
const p1 = new Promise((resolve, reject) => {
setTimeout(() => resolve('Success'), 1000);
});
const p2 = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('fail')), 2000);
});
const p3 = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('fail2')), 4000);
});
Promise
.race([p1, p2, p3])
.then((result) => {
console.log(result);// hello 출력
})
.catch((value) => {
console.log(value);
});
Plain Text
복사
지금 race 메소드 안의 배열에 들어있는 Promise 객체들 중에서 무엇이 가장 빨리 fulfileld 또는 rejected 상태가 될까요?
답은 1초 후에 fulfilled 상태가 되는 p1 객체입니다.
p1 객체는 1초 후에 fulfilled 상태가 되고, 그 작업 성공 결과로 문자열 Success를 가지게 되는데요.
p2는 2초 후에, p3는 4초 후에 rejected 상태가 됩니다.
race 메소드가 리턴한 Promise 객체는 이 중에서 가장 빨리 상태 정보가 결정된 p1 객체와 동일한 상태와 결과를 가집니다.
말그대로 race 메소드는 여러 Promise 객체들을 레이스(race, 경쟁)시켜서 가장 빨리 상태가 결정된 Promise 객체를 선택하는 메소드입니다.
이 코드를 실행하면
p1 객체의 작업 성공 결과였던 문자열 Success가 잘 출력됩니다.
만약 setTimeout에 넣었던 밀리세컨즈를 이렇게 바꾼다면
const p1 = new Promise((resolve, reject) => {
setTimeout(() => resolve('Success'), 6000);
});
const p2 = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('fail')), 2000);
});
const p3 = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('fail2')), 4000);
});
Promise
.race([p1, p2, p3])
.then((result) => {
console.log(result);// hello 출력
})
.catch((value) => {
console.log(value);
});
Plain Text
복사
이번에는 p2가 p1보다 더 빨리 상태가 결정됩니다. 그럼 결국 race 메소드가 리턴한 Promise 객체는 p2처럼 rejected 상태가 되고 동일한 작업 실패 정보를 갖게 됩니다. 이 코드를 실행해보면
Error 객체의 정보가 잘 출력되는 것을 알 수 있습니다.
자, all 메소드와 race 메소드 잘 이해되시나요? 실무에서는 이렇게 여러 Promise 객체들을 한꺼번에 다뤄야할 때도 있습니다. 그럴 때 각 용도에 적합한 메소드를 사용하면 되는데요.
all 메소드나 race 메소드 말고 allSettled, any라는 메소드도 있습니다.
이것들도 all, race 메소드처럼 Promise 객체 배열을 아규먼트로 받고 Promise 객체를 리턴하는데요.
이것들도 간단하게 설명하겠습니다.
각 메소드가 리턴한 Promise 객체가 A라고 할 때,
allSettled 메소드 : 배열 내의 모든 Promise 객체가 fulfilled 또는 rejected 상태가 되기까지 기다리고, pending 상태의 Promise 객체가 하나도 없게 되면, A의 상태값은 fulfilled 상태가 되고 그 작업 성공 결과로, 하나의 배열을 갖게 됩니다.
이 배열에는 아규먼트로 받았던 배열 내의 각 promise 객체의
(1) 최종 상태를 status 프로퍼티,
(2) 그 작업 성공 결과는 value 프로퍼티,
(3) 그 작업 실패 정보는 reason 프로퍼티
에 담은 객체들이 요소로 존재합니다.
이런 식으로 말이죠.
[
{status: "fulfilled", value: 1},
{status: "fulfilled", value: 2},
{status: "fulfilled", value: 3},
{status: "rejected", reason: Error: an error}
]
Plain Text
복사
참고로 fulfilled 상태와 rejected 상태를 묶어서 settled 상태라고 하는데요. allSettled 메소드는 말 그대로 배열 속 Promise 객체들이 settled 상태가 되기만 하면 되는 겁니다. 이에 반해 위에서 배운 all 메소드는 모든 Promise 객체들이 fulfilled 상태가 되기를 기다리는 거구요.
any 메소드 : 여러 Promise 객체들 중에서 가장 먼저 fulfilled 상태가 된 Promise 객체의 상태와 결과가 A에도 똑같이 반영됩니다. 만약 모든 Promise 객체가 rejected 상태가 되어버리면 AggregateError라고 하는 에러를 작업 실패 정보로 갖고 rejected 상태가 됩니다. any라는 단어의 뜻처럼 배열 속의 Promise 객체 중 단 하나라도 fulfilled 상태가 되면 되는 겁니다.
자, 각 메소드의 이름과 그 성질을 매칭해서 기억해보세요. 나중에 혹시 정확한 성질이 기억나지 않더라도 다시 찾아보면 되니까 지금 잘 이해해두는 것이 중요합니다. 참고로 어떤 메소드든 결국 하나의 Promise 객체를 리턴하기 때문에 그 리턴 결과를 Promise Chain에서 자유롭게 사용할 수 있다는 점을 기억해두세요.
혹시 각 메소드들의 사용법을 좀더 자세히 보고 싶은 분들은 아래 링크를 참조하세요.
async/await를 활용한 세련된 비동기 코드
async/await란?
fetch('https://jsonplaceholder.typicode.com/users')
.then((response)=> response.texT())
.then((result)->{console.log(result);});
async function fetchAndPrint(){
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const result = await response.text();
console.log(result);
}
fetchAndPrint();
//async -> 함수 안에 비동기 실행이 있다라는 뜻
//await -> 프로미스 객체를 리턴하는 코드 앞에 붙어있음.
//그 뒤의 코드를 실행하고 프로미스 객체를 기다려준다.
//해당 프로미스 객체가 fulfilled 상태이거나 rejected상태일 때까지 기다린다.
//상태가 바뀌면 결과를 받아서 리턴한다.
//await이란 함수는 async함수 안에서만 사용가능하다.
//await을 만나는 순간 async함수 바깥에 나가서 실행하고 돌아온다.
JavaScript
복사
catch 문과 finally문
reject상태가 될 때는 어떻게 해야하냐?
try{
}catch(error){
console.log(error);
}finally{
console.log('exit')
}
try안에서 에러가 발생하면, 실행 흐름이 catch문으로 넘어간다.
async함수는 Promise객체를 리턴합니다.
async함수는 숫자 3을 작업 결과 내용으로 가지는 fulfilled promise객체를 리턴한다.
안의 규칙 → then메소드와 비슷하다
async 함수 안의 async함수
하나의 async함수 안에서 또 다른 async함수를 사용할 수 있다.
await async와 같이 사용하면 된다.