|
콜백 지옥 (Callback hell)
: 비동기 처리 로직을 위해 콜백 함수를 연속해서 사용할 때 발생하는 문제다.
아래 코드를 보자.
$.get('url', function(response) {
parseValue(response, function(id) {
auth(id, function(result) {
display(result, function(text) {
console.log(text);
});
});
});
});
웹 서비스를 개발하다 보면 서버에서 데이터를 받아와 화면에 표시하기까지 인코딩, 사용자 인증 등을 처리해야 하는 경우가 있다. 만약 이 모든 과정을 비동기로 처리해야 한다고 하면 위와 같이 콜백 안에 콜백을 계속 이어가는 형식으로 코딩을 하게 된다.
이러한 코드 구조는 가독성이 떨어지고 로직을 변경하기도 어렵다. 이와 같은 코드 구조를 콜백 지옥이라고 한다.
일반적으로 콜백 지옥을 해결하는 방법에는 Promise나 Async를 사용하는 방법이 있다. 만약 코딩 패턴으로만 콜백 지옥을 해결하려면 아래와 같이 각 콜백 함수를 분리해주면 된다.
function parseValueDone(id) {
auth(id, authDone);
}
function authDone(result) {
display(result, displayDone);
}
function displayDone(text) {
console.log(text);
}
$.get('url', function(response) {
parseValue(response, parseValueDone);
});
위 코드는 앞의 콜백 지옥 예시를 개선한 코드다. 중첩해서 선언했던 콜백 익명 함수를 각각의 함수로 구분하였다.
코드를 간단하게 살펴보겠다.
- 먼저 ajax 통신으로 받은 데이터를 parseValue() 메서드로 파싱한다.
- parseValueDone()에 파싱 한 결과값인 id가 전달되고 auth() 메서드가 실행된다.
- auth() 메서드로 인증을 거치고 나면 콜백 함수 authDone()이 실행된다.
- 인증 결과 값인 result로 display()를 호출하면 마지막으로 displayDone() 메서드가 수행되면서 text가 콘솔에 출력된다.
위와 같은 코딩 패턴으로도 콜백 지옥을 해결할 수 있지만 Promise나 Async를 이용하면 더 편하게 구현할 수 있다.
Promise란?
“A promise is an object that may produce a single value some time in the future”
Promise는 자바스크립트 비동기 처리에 사용되는 객체다. 자바스크립트의 비동기 처리란 ‘특정 코드의 실행이 완료될 때까지 대기하지 않고 다음 코드를 먼저 수행하는 자바스크립트의 특성’을 의미한다.
Promise는 주로 서버에서 받아온 데이터를 화면에 표시할 때 사용한다. 일반적으로 웹 애플리케이션을 구현할 때 서버에서 데이터를 요청하고 받아오기 위해 아래와 같은 API를 사용한다.
$.get('url 주소/member/1', function(response) {
// ...
});
위 API가 실행되면 서버에 ‘데이터를 보내줘’ 라는 요청을 한다. 그런데 여기서 데이터를 받아오기도 전에 마치 데이터를 다 받아온 것 마냥 화면에 데이터를 표시하려고 하면 오류가 발생하거나 빈 화면이 뜬다. 이런 문제를 해결하기 위한 방법 중 하나가 Promise다. 이제 Promise의 동작을 이해하기 위해 예제 코드를 보자. 아래 코드는 간단한 ajax 통신 코드다.
function getData(callbackFunc) {
$.get('url 주소/member/1', function(response) {
callbackFunc(response); // 서버에서 받은 데이터 response를 callbackFunc() 함수에 넘겨줌
});
}
getData(function(tableData) {
console.log(tableData); // $.get()의 response 값이 tableData에 전달됨
});
위 코드는 jQuery의 ajax 통신 API를 이용하여 지정된 url에서 1번 회원 데이터를 요청하는 코드로 비동기 처리를 위해 콜백 함수를 사용했다. 위 코드에 Promise 객체를 적용하면 아래와 같이 작성할 수 있다.
function getData(callback) {
// new Promise() 추가
return new Promise(function(resolve, reject) {
$.get('url 주소/member/1', function(response) {
// 데이터를 받으면 resolve() 호출
resolve(response);
});
});
}
// getData()의 실행이 끝나면 호출되는 then()
getData().then(function(tableData) {
// resolve()의 결과 값이 여기로 전달됨
console.log(tableData); // $.get()의 reponse 값이 tableData에 전달됨
});
콜백 함수로 처리하던 구조에서 new Promise(), resolve(), then()와 같은 프로미스 API를 사용한 구조로 바뀌었다.
async와 await란?
async와 await는 자바스크립트의 비동기 처리 패턴 중 최근에 나온 문법으로 기존의 비동기 처리 방식인 콜백 함수와 프로미스의 단점을 보완하고 개발자가 읽기 좋은 코드를 작성할 수 있게 도와준다. async await의 기본 문법을 보자.
async function 함수명() {
await 비동기_처리_메서드_명();
}
함수의 앞에 async 라는 예약어를 적는다. 그런 다음 함수의 내부 로직 중 HTTP 통신을 하는 비동기 처리 코드 앞에 await를 붙인다. 여기서 주의할 점은 비동기 처리 메서드가 반드시 Promise 객체를 반환해야 await가 의도한 대로 동작한다.
일반적으로 await의 대상이 되는 비동기 처리 코드는 Axios 등 Promise를 반환하는 API 호출 함수다.
참조 페이지 : https://joshua1988.github.io/web-development/javascript/js-async-await/
자바스크립트 Promise 쉽게 이해하기
Ajax 처리 연습 해보기 ------------------------------------
<!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>
<script src="https://code.jquery.com/jquery-2.2.1.min.js"></script>
<script>
const jsonURL = "http://openapi.seoul.go.kr:8088/sample/json/SeoulLibraryTime/1/5/";
$(function() {
$("#btn1").on("click", function() {
getDataAjax(jsonURL);
});
$("#btn2").on("click", function() {
aa();
});
$("#btn3").bind("click", function() {
bb();
});
});
//방법 1 : 전통적
const getDataAjax = url => {
const xhr = new XMLHttpRequest();
xhr.open("get", url, true);
xhr.responseType = "json";
xhr.onreadystatechange = () => {
if(xhr.readyState === 4) { // 통신에 이상이 없다면...
if(xhr.status === 200) { // 요청에 대한 반응이 정상인 경우
for(let key in xhr.response) { // 받아온 json 데이터의 키와 값을 출력.
if(xhr.response.hasOwnProperty(key))
alert(`${key}: ${xhr.response[key]}`);
}
} else { // 통신 상에 오류가 있다면 오류 출력.
alert(`http status code: ${xhr.status}`);
}
}
};
xhr.send();
};
//방법 2 fetch API는 XMLHttpRequest를 대신하기 위한 방안 중 하나 : async ~ await를ajax 처리에 최적화 시킨 명령
function aa(){
const getDataAjaxFetch = url => (
fetch(url).then(res => res.json())
);
getDataAjaxFetch(jsonURL).then(data => {
for(let key in data) { // 받아온 json 데이터의 키와 값을 출력.
if(data.hasOwnProperty(key))
alert(`방법2 ${key}: ${data[key]}`);
}
}).catch(err => alert(err));
}
//방법 3 async & await (ES2017)
function bb(){
const getDataFetch = url => (
fetch(url).then(res => res.json())
);
const res = data => { // 성공 콜백함수는 공통 함수로 뺌
for(let key in data) {
if(data.hasOwnProperty(key))
alert(`방법3 ${key}: ${data[key]}`);
}
};
// async 함수 안에서 비동기 코드 앞에 await를 붙여주면 된다.
// 안타깝게도 async '함수'라서 호출을 위해 즉시 실행함수를 사용했다.
(async () => {
try {
await getDataFetch(jsonURL).then(data => res(data));
} catch(err) {
alert(err);
}
})();
}
</script>
<body>
Ajax 처리<br>
<button id="btn1">버튼 클릭1</button><br>
<button id="btn2">버튼 클릭2</button><br>
<button id="btn3">버튼 클릭3</button>
</body>
</html>
// "GET" 방식으로 자료를 서버로 전달하려면
fetch(url, {method:"GET", head:JSON.stringify({aaa:'111'})})
.then(res =>{ // fetch함수는 Promise객체를 리턴
if(res.status === 200){
return res.json();
}else{
console.error(`HTTP error! status:${res.status}`);
}
}).then(jsonData => {
//console.log(jsonData);
processJsonFunc2(jsonData);
}).catch(err => {
console.log(err);
});
// "POST" 방식으로 자료를 서버로 전달하려면
fetch(url, {
method: "POST",
body: JSON.stringify({
email: id,
password: pw,
}),
})
.then((response) => response.json())
.then((result) => console.log(result));
👍Axios 라이브러리
fetch then : 네트워크 통신을 할 때 유용한 api이긴 하지만 몇 가지 문제점이 있다.
1) response를 받아올 때마다 json으로 변환해줘야 함.
2) 네트워크 통신에 문제가 있거나, socket이 타임아웃 되었거나, offline이거나 할 때 에러가 던져져서 catch로 에러를 핸들링 할 수 있지만 백엔드에서 뭔가 잘못되었다고 반응을 해줘도 전부 성공적인 case로 간주하기 때문에 then으로 들어온다. 따라서 then 안에서 throw를 이용해서 에러를 직접 수동적으로 던져줘야 한다.
이러한 문제점들을 해결해 줄 수 있는 라이브러리가 바로 axios이다!
Axios 설치 및 사용 : Axios는 브라우저, Node.js를 위한 Promise API를 활용하는 HTTP 비동기 통신 라이브러리다.
https://inpa.tistory.com/entry/AXIOS-%F0%9F%93%9A-%EC%84%A4%EC%B9%98-%EC%82%AC%EC%9A%A9
실습 하나더 ------ 자바스크립트 여러 방법으로 라이브러리로 Ajax 처리
my.json
{"sangpum":
[
{"code":"100","sang":"glass"},
{"code":"110","sang":"backpack"}
]
}
aa.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<!-- axios CDN -->
<script type="text/javascript">
window.xxxxonload = function(){
document.querySelector("#btn1").xxxxonclick=funcJs;
document.querySelector("#btn2").xxxxonclick=funcFetch;
document.querySelector("#btn3").xxxxonclick=funcAsync;
document.querySelector("#btn4").xxxxonclick=funcAxios;
}
let xhr;
function funcJs(){
//alert(1);
xhr = new XMLHttpRequest();
xhr.open("get", "my.json", true);
xhr.onreadystatechange = function(){
//alert(xhr.readyState);
if(xhr.readyState === 4){
//alert(xhr.status);
processFunc();
}
}
xhr.send(null);
}
let processFunc = () => { // function processFunc(){ }
let data = xhr.responseText;
//alert(data);
let parseData = JSON.parse(data);
let str = "";
for(let i=0; i < parseData.sangpum.length; i++){
str += parseData.sangpum[i].code + " " + parseData.sangpum[i].sang + "<br/>";
}
document.querySelector("#show1").innerHTML = str;
}
/*
Fetch : 브라우저에 내장된 라이브러리. Promise 타입 객체 반환. 반환 객체는 API 호출이 성공하면 response를 resolve하고, 실패했을 때에는 error를 reject을 반환함
GET 요청 방식 ----------------------
fetch(url) // fetch는 get 방식이 기본으로 작동, options 생략 가능
.then((response) => {
return response.json(); //응답을 JSON 형태로 파싱한 결과를 반환: .text(), .formData()
})
.then((data) => {
console.log(data); // 결과 출력
})
.catch((error) => {
console.error('오류 발생:', error);
});
// 보낼 데이터
const dataToSend = {
title: '제목', body: '내용', userId: 1,
};
// POST 요청 보내기 ----------------------
fetch('https://korea.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(dataToSend),
// js 객체를 json으로 변경
// body의 데이터 유형은 반드시 "Content-Type" 헤더와 일치해야 함
})
.then((response) => {
return response.json(); // 응답을 JSON 형태로 파싱한 결과로 반환
})
.then((data) => {
console.log(data); // 결과 출력
})
.catch((error) => {
console.error('오류 발생:', error);
});
*/
function funcFetch(){ //https://msm1307.tistory.com/127
//alert(2);
const url = "my.json";
fetch(url, {method:"GET"}).then(response => { // {method:"GET"} 생략 가능
//alert(res.status);
if(response.status === 200){
return response.json();
}else{
console.log(`HTTP error! status=${response.status}`);
}
}).then(jsonData => {
//alert(jsonData); //JSON.parse()가 된 상태임
let str = "";
for(let i=0; i < jsonData.sangpum.length; i++){
str += jsonData.sangpum[i].code + " " +
jsonData.sangpum[i].sang + "<br/>";
}
document.querySelector("#show2").innerHTML = str;
}).catch(err => {
console.error(err);
});
}
async function funcAsync(){
//alert(3);
const url = "my.json";
const response = await fetch(url);
const jsonData = await response.json();
//alert(jsonData); //JSON.parse()가 된 상태임
let str = "";
for(let i=0; i < jsonData.sangpum.length; i++){
str += jsonData.sangpum[i].code + " " +
jsonData.sangpum[i].sang + "<br/>";
}
document.querySelector("#show3").innerHTML = str;
}
function funcAxios(){ //https://msm1307.tistory.com/127
// Axios : 비동기 HTTP 통신으로 클라이언트에서 서버로 송수신 처리 라이브러리
// 브라우저 호환성이 뛰어남, Promise 기반, 브라우저와 nodejs 환경에서 사용 가능
//alert(4);
const url = "my.json";
axios.get(url).then((response) => {
console.log(response.data); // 서버가 제공하는 응답(데이터)
console.log(response.status); // HTTP 상태 코드
console.log(response.statusText); // HTTP 상태 메시지
console.log(response.headers); // HTTP 헤더
console.log(response.config); // 요청을 위해 'Axios'가 제공하는 구성
return response.data;
}).then((data) => {
//alert(data); //JSON.parse()가 된 상태임
let str = "";
for(let i=0; i < data.sangpum.length; i++){
str += data.sangpum[i].code + " " +
data.sangpum[i].sang + "<br/>";
}
document.querySelector("#show4").innerHTML = str;
}).catch((error) => {
console.error('오류 발생:', error);
});
}
}
</script>
</head>
<body>
<h2>Ajax 처리</h2>
<button id="btn1">기본적인 방법</button><br/>
<button id="btn2">fetch 방법</button><br/>
<button id="btn3">async 방법</button><br/>
<button id="btn4">axios 방법</button><br/>
<br/>
<hr>
<div id="show1"></div>
<div id="show2"></div>
<div id="show3"></div>
<div id="show4"></div>
</body>
</html>
|