앞에서 살펴봤던 콜백 지옥을 해결할, XMLHttpRequest
를 대체할 fetch
라는 이름의 API가 등장하였다!
기존의 XHR이 생성하는 인스턴스는 통신의 기능을 수행하는 XMLHttpRequest
객체를 반환했지만
fetch는 인스턴스를 만들지 않고 promise라는 약속을 반환한다는 차이점이 있다. 이게 무슨 말인지는 계속 알아보도록 하자.
커피를 주문하는 상황을 생각해보면 쉽게 이해할 수 있다.
위의 상황을 코드로 작성하면 다음과 같다.
// 커피를 주문하는 프로미스 객체를 생성합니다. 생성자에는 약속을 지키기 위한 resolve와, 약속을 지키지 못했을 때를 대비한 reject 두 가지를 인자로 전달합니다.
// 프로미스 객체를 생성하는 순간 프로미스 생성자함수의 콜백 함수가 실행됩니다. 이를 실행자(executor)라 부릅니다.
const orderCoffee = new Promise((resolve, reject) => {
const requestObj = new XMLHttpRequest();
requestObj.open('GET', 'orderCoffee.txt');
requestObj.onreadystatechange = () => {
if (requestObj.readyState === 4) {
if (requestObj.status === 200) {
const result = requestObj.responseText;
// resolve 메소드가 실행되면 then 메소드가 자동으로 호출됩니다.
resolve(result);
} else {
// resolve 메소드 호출이 없는 상태에서 reject 메소드가 실행되면 catch 메소드가 자동으로 호출됩니다.
reject(new Error(`커피주문이 정상적으로 이뤄지지 않았습니다.: ${requestObj.status}`));
}
}
};
requestObj.send();
});
// 이 부분에 주목해주세요. then 메소드를 사용하면 비동기 코드를 마치 동기적인 코드처럼 작성할 수 있습니다. 앞에서 작성한 XHR 코드와 비교해보는것도 좋습니다.
// resolve 메소드가 실행될때 전달된 인자는 then 메소드의 콜백함수의 인자로 전달됩니다.
orderCoffee.then((asyncResult) => {
console.log(asyncResult);
console.log('약속이 이루어졌습니다.');
return asyncResult;
}).catch((error) => { // then 메소드는 프라미스 객체를 반환하기 때문에 catch 메소드를 이어서 쓰는것이 가능합니다.
// resolve 메소드와 마찬가지로 reject 메소드가 실행될때 전달된 인자는 catch 메소드의 콜백함수의 인자로 전달됩니다.
console.log(error);
})
처음 배우는 입장에서는 resolve
, reject
, then
, catch
에 대해서만 알아도 괜찮다.
resolve
가 실행되면(제대로 통신이 완료되었으면) then
은 자동으로 실행되고 reject
가 실행되면(통신이 실패했으면) catch
메서드가 자동으로 실행된다.
여기서 onreadystatechange
라는 것은 Ajax 요청의 상태가 바뀔때마다(통신의 상태) 호출되는 이벤트 핸들러이다. 여기서는 제대로 동작하는지 확인하고 있다. console.log(error)
는 에러를 발생시키는 코드이다.
fetch
의 경우는 resolve
와 reject
상테의 promise(약속)을 자동으로 반환해주기에 코드가 훨씬 간결해진다.
let result = fetch('https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json');
console.log(result);
통신이 잘 이루어졌음을 확인 가능하다.
여기에 then
과 catch
를 사용하면 데이터를 잘 가져오는 것을 확인할 수 있다.
let result = fetch('https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json');
result.then((data) => {
console.log(data);
});
통신에는 보통 JSON 데이터를 사용하는데 이 JSON 데이터는 자바스크립트에서 바로 사용하지 못하고 .json()메서드로 자바스크립트 객체로 변환하는 과정이 필요하다.
이 과정에서 promise 객체를 반환받기 때문에 한번 더 then 메서드를 사용해야 진짜 필요한 데이터를 뽑아낼 수 있다.
fetch('https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json')
.then((response) => {
// response.ok 는 응답이 성공적(200-299)일 경우 true, 아니면 false를 반환합니다.
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
console.log(response)
return response.json(); // then을 쓰기 전의 값 확인용
})
.then((data) => {
console.log(data);
return data;
})
// fetch 함수는 네트워크 오류가 발생하면 reject 상태의 프로미스를 반환합니다.
.catch((error) => {
console.error(error);
});
then
을 쓰고 난 후에 정상적으로 데이터를 뽑은 모습이다.
여기서 response.ok
는 응답이 성공적이면 true, 아니면 false를 반환한다. 앞에서 fetch
는 자동으로 약속이 이행된(fulfilled
) 상태의 프로미스가 반환된다고 했다.
그렇지만 사실 fetch
는 응답이 실패 상태여도(의도한 데이터를 받지 못한 상태) 서버와의 통신 자체는 성공했기에 약속이 이행됐다고 판단한다. 따라서 의도한 데이터를 무사히 받기 위해 필수적으로 response.ok
를 이용한 예외처리를 사용해야 한다!
이러한 promise
를 더 편리하게 다루고, 비동기 프로그래밍을더 간결하고 이해하기 쉽게 만들어주는 문법이다.
이를 통해 비동기 코드를 마치 동기 코드처럼 작성하도록 도와주며, 가독성을 크게 향상시켰다.
function test(){
return Promise.resolve();
}
위 코드와 아래 코드는 똑같이 동작한다.
async function test(){}
기능을 조금 더 살펴보자면, await
는 async
함수 내에서 promise
객체의 상태가 결정될 때까지 다음 코드를 실행시키지 않고 기다리게 만든다. 그리고 promise
객체의 fulfilled
(약속을 이행한)값을 반환한다.
async function message() {
const hello = await new Promise((resolve) => {
setTimeout(() => {
resolve('hello');
}, 100)
})
const world = await new Promise((resolve) => {
setTimeout(() => {
resolve('world');
}, 100)
})
console.log(`${hello} ${world}`);
}
message();
async
와 await
키워드가 없었으면 제대로 동작하지 않았을 코드가 마치 동기처럼 순서대로 실행되어 hello world
가 출력됨을 볼 수 있다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
function searchUser(userName, password) {
return fetch("users.json")
.then((response) => {
if (!response.ok) {
throw new Error(
`HTTP error! Status: ${response.status}`
);
}
return response.json();
})
.then((data) => {
// json 데이터와 일치하는지 검색하는 메소드
const info = data.user.find((item) => {
return (
item.userName === userName &&
item.password === password
);
});
if (info) {
// onSuccess(info);
return info;
} else {
throw new Error("일치하는 유저가 없습니다!");
// reject('일치하는 유저가 없습니다!');
// console.error('일치하는 유저가 없습니다!');
}
})
.catch((msg) => {
console.error(msg);
});
// const requestObj = new XMLHttpRequest();
// requestObj.open('GET', 'users.json');
// requestObj.onreadystatechange = () => {
// if (requestObj.readyState == 4 && requestObj.status == "200") {
// }
// };
// requestObj.send();
}
function sayHi(user) {
return fetch("greetings.json")
.then((response) => {
if (!response.ok) {
throw new Error(
`HTTP error! Status: ${response.status}`
);
}
return response.json();
})
.then((data) => {
const info = data.greetings.find((item) => {
return item.userName === user.userName;
});
if (info) {
return info;
// resolve(info);
} else {
throw new Error("일치하는 인사말 없습니다!");
// reject('일치하는 인사말 없습니다!');
// console.error('일치하는 인사말 없습니다!');
}
})
.catch((msg) => {
console.error(msg);
});
// const requestObj = new XMLHttpRequest();
// requestObj.open('GET', 'greetings.json');
// requestObj.onreadystatechange = () => {
// if (requestObj.readyState == 4 && requestObj.status == "200") {
// const result = JSON.parse(requestObj.responseText);
// json 데이터와 일치하는지 검색하는 메소드
// }
// };
// requestObj.send();
}
const userName = prompt("이름을 입력하세요");
const password = prompt("비밀번호를 입력하세요");
// searchUser(userName, password, (info) => {
// sayHi(info, (info) => {
// alert(info.greetings);
// });
// });
searchUser(userName, password)
.then((info) => {
return sayHi(info);
})
.then((info) => {
alert(info.greetings);
})
.catch((msg) => {
console.error(msg);
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
'use strict';
class UserStorage {
// 유저를 검색하는 메소드
async searchUser(userName, password) {
// 이제 fetch의 결과물을 then, catch로 받을 필요가 사라집니다. 대신 try-catch 문을 사용합니다.
try {
const response = await fetch('users.json'); // 사용자 정보를 가져오기 위한 API 호출
if (!response.ok) {
throw new Error(`HTTP ERROR!! State: ${response.status}`);
}
const data = await response.json(); // API 응답 데이터를 JSON 객체로 파싱. .json은 프로미스를 반환하기 때문에 비동기로 작동합니다.
const result = data.user.find(item => { // 사용자 정보가 있는지 확인
return item.userName === userName && item.password === password
});
if (!result) {
throw new Error('user not found');
}
return userName;
} catch (error) {
console.error('유저를 찾는중에 에러가 발생했습니다.:', error);
// 에러를 던져야 부모 try문 안에서 코드의 실행이 종료되고 catch 문으로 넘어갑니다.
throw error
}
}
// 유저에 맞는 인사말을 출력하는 메소드
async sayHi(user) {
try {
const response = await fetch('greetings.json'); // 인사말 정보를 가져오기 위한 API 호출
const data = await response.json(); // API 응답 데이터를 JSON 객체로 파싱
const result = data.greetings.find(item => { // 사용자 정보가 있는지 확인
return item.userName === user
});
if (!result) {
throw new Error('no greetings'); // 인사말 정보가 없을 경우, 에러 발생
}
return result;
} catch (error) {
console.error('인사말을 찾는중에 에러가 발생했습니다.:', error);
throw error
}
}
}
// 인스턴스를 만들고
const userStorage = new UserStorage();
// 사용자 입력을 받습니다.
const userName = prompt('이름를 입력하세요');
const password = prompt('등록한 비밀번호를 입력하세요');
// async/await 를 사용하여 비동기적으로 처리하며, try/catch 문으로 예외 처리한다.
async function sayHello() {
try {
const result = await userStorage.searchUser(userName, password); // 사용자 검색 메소드 호출
const user = await userStorage.sayHi(result); // 사용자 인사말 출력 메소드 호출
alert(`당신에게 인사합니다. ${user.userName}님 ${user.greetings}`); // 최종 결과 출력
} catch (error) {
console.error('통신 에러가 발생했습니다.:', error.message);
}
};
sayHello();
</script>
</body>
</html>
1과 2 코드의 기능은 동일하다.
2주 가량의 프론트엔드 맛보기 수업이 끝이 났다. 쌩초보 상태로 듣는 HTML/CSS, Python과 비슷한 것 같으면서도 다른 JavaScript까지.. 어렵고 어렵고 어려운 과정이었던 것 같다. 그렇지만 포기하지 않고 달려온 내 자신을 한번 칭찬해주면서 이어서 써보겠다.
이제 설날이 시작되는데 오랜만에 만나는 친척들과 도란도란 얘기를 나누겠지만! 다음주 금요일(16일)까지 자유주제로 하는 개인 프로젝트를 제출해야 하기에 마냥 즐기기는 힘들 것 같다. 너무 빡빡하게 하지는 않겠지만.
ChatGpt API를 활용하여 웹페이지를 만들고 정보를 받는 건데 어떻게 할지 생각해 봐야겠다.