지난번에 디자인까지 만들어뒀다.
디자인도 만들고, 이벤트 핸들러도 구현해놨으니 API
요청을 보내서 파라미터를 만들고
컴포넌트들은 페이지에 뿌려주기만 하면 된다.
API
요청 가보자고
사용하려는API
의 내용들은 이전에 포스트 해둔 원형 회전 날씨 카드 배너 만들기 - API 찾기 , 카드배너 디자인하기에 있다.
이틀 전에 API KEY
도 모두 발급 받았으니 API
를 신청 할 때 사용할 파라미터들을 준비해야 한다.
사용할 파라미터는 nx , ny
로 발급 받을 지역을 지정해줘야 하고
basedate , basetime
을 이용해 기상청에서 업데이트 된 시각에 맞춰 쿼리 요청을 보내면 된다.
API
사용 전 가공하기class LocateFetch {
constructor(apikey) {
this.locateArr = [
{
address: 'Seoul',
longitude: 126.9816417,
latitude: 37.57037778,
},
...
},
{
address: 'Gwangju',
longitude: 126.8513898,
latitude: 35.1768206,
},
];
this.params = {
dateType: 'JSON',
APIKEY: apikey,
};
this.updateArr = [
'0200',
'0500',
'0800',
'1100',
'1400',
'1700',
'2000',
'2300',
];
this.options = {
timeZone: 'Asia/Seoul',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
};
}
setParams() {
// basedate, basetime 설정하기
const currentDate = new Date();
const koreanTime = new Intl.DateTimeFormat('en-US', this.options)
.format(currentDate)
.split(','); // 예시 [01/19/2024, 17:59:33]
const [month, day, year] = koreanTime[0].trim().split('/');
const [hour, minute] = koreanTime[1].trim().split(':');
const fullBaseDate = `${year}${month}${day}`;
const fullBaseTime = `${hour}${minute}`;
// 기상청API 는 02시부터 23시까지 3시간 간격으로 업데이트 되며
// 매 정각이 아닌 10분 후에 업데이트 되기 때문에
// basetime 을 계산해줘야함
const timeIndex = Math.max(Math.floor((fullBaseTime - 210) / 300), 0);
this.params.baseDate = fullBaseDate;
this.params.baseTime = this.updateArr[timeIndex];
}
init() {
this.setParams();
// 위도 경도 데이터를 nx , ny 데이터로 변경하기
this.locateArr.map((locate) => {
const _locate = locate;
const { latitude, longitude } = _locate;
const { x, y } = dfs_xy_conv('toXY', latitude, longitude);
_locate.nx = x;
_locate.ny = y;
});
}
createUrl() {
const { locateArr, params } = this;
return locateArr.map((locate) => {
const { nx, ny } = locate;
const { dateType, APIKEY, baseDate, baseTime } = params;
return `https://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getVilageFcst?serviceKey=${APIKEY}&pageNo=1&numOfRows=1000&dataType=${dateType}&base_date=${baseDate}&base_time=${baseTime}&nx=${nx}&ny=${ny}`;
});
}
}
파라미터로 들어갈 nx , ny , basedate , basetime
를 추출해주기 위해 전처리들을 좀 해주었다.
그래서 홈페이지에 들어온 시간대로부터 2시간 10분
을 빼준 후 3시간 간격으로 나눠준다.
그렇게 하면 02 시부터 3시간 간격으로 업데이트 되는 API
중 가장 최근에 업데이트 된 정보를 호출 할 수 있다.
나눠진 정수 몫이 인덱스 역할을 한다.
API
업데이트 배열이[02 , 05 , 08 , 11 ... 23]
이렇게 생겼을 때
현재 시각이10시30분
이라면1030
형태로 취급한다.
2시간 10분을 빼주고 300으로 나눠주면 나오는 값은830 / 300 => 2
, 업데이트 배열의 2번째 값인08
을 이용하여API
요청을 보낸다.하지만 이 방법에는 치명적인 문제가 존재한다. 그건 나중에 기능 업데이트 할 때 이야기 해야겠다.
이후 GET
요청을 보낼 URL
을 createURL
을 이용해서 배열 형태로 만들어주자
const api = new LocateFetch(apiKey);
api.init();
console.log(api.createUrl());
사이트 링크에 접속도 잘 되고 , 배열 형태도 잘 만들어진다 .
이제 해당 링크에 GET
요청을 보낸 후 JSON
객체로 받아오기만 하면 된다.
GET
요청 보내기async/await
를 쓸까 말까사실 이번 토이프로젝트를 하기로 했던 가장 큰 이유는
async/await
를 더 체득 하기 위함이였다.
그런데 생각해보면 내가 얻는 GET
요청들은 각 독립적인 지역들의 기상 정보를 받아오는 것이기 때문에
오히려 async/await
를 사용하면 비동기적인 처리들이 동기적인 것마냥
직렬적으로 수행되기 때문에 더 안좋다.
...
async getAllData() {
const urlArray = this.createUrl();
await fetch(urlArray[0])
.then((res) => res.json())
.then(console.log);
await fetch(urlArray[1])
.then((res) => res.json())
.then(console.log);
await fetch(urlArray[2])
.then((res) => res.json())
.then(console.log);
await fetch(urlArray[3])
.then((res) => res.json())
.then(console.log);
await fetch(urlArray[4])
.then((res) => res.json())
.then(console.log);
await fetch(urlArray[5])
.then((res) => res.json())
.then(console.log);
}
}
const api = new LocateFetch(apiKey);
api.init();
api.getAllData();
서버에서 요청을 받아오는데까지 걸리는 시간이 대략 사이니 6개의 요청을 직렬적으로 보내는데 6.5초 정도 걸린다.
async/await
쳐내 ~!
promise.All
을 써서 GET
요청 병렬 처리 하기
...
getAllData() {
const urlArray = this.createUrl();
const allPromises = Promise.all(
urlArray.map((url) => fetch(url).then((res) => res.json())),
)
.then(console.log('비동기 처리 완료!'))
.catch(console.error);
return allPromises;
}
}
const api = new LocateFetch(apiKey);
api.init();
const result = api.getAllData();
console.log(result);
Promise.all
을 사용하니 병렬적으로 GET
요청을 하기 때문에 가장 settled
되는데 오래 걸리는 요청인 1.6초 만에 모든 요청이 완료된 모습을 볼 수 있다.
async/await
를 이용했을 때와 비교해보면 속도가 얼마나 빠른지 더 짐작 할 수 있다.
async/await
promise.all
다만 위에서 GET
요청을 보낸 후 결과값이 result
로 잘 받아졌냐고 묻는다면
아니다.
promise.all
또한 다양한 요청들이 모두 settled
된 상태에서 다음 비동기 함수를 실행 시킬 수 있을 뿐 return allPromises
마저 settled
된 상태에서 실행 하는 것이 아니다.
그렇기 때문에 그저 동기적으로 호출시킨 getAllData()
함수의 반환값은 아직 settled
되지 않은 프로미스 객체를 반환한다.
다만 나는 비동기 처리 완료라는 로그가
promise.all
이 모두settled
된 이후에 로그 되도록 하였는데 개발자 도구에서는 먼저 나온다.
ms
단위로 보면 두개가 거의 같은 시간 혹은 로그가 먼저 뜬다.
이유를 잘 모르겠다.
settled
되지 않은 상태에서 반환되었다면 비동기 처리 완료 로그가 더 마지막에 떠야 할텐데 말이다.오마이갓 완전 바보같았다.
const allPromises = Promise.all( urlArray.map((url) => fetch(url).then((res) => res.json())), ) .then(console.log('비동기 처리 완료!')) .catch(console.error);
이 부분에서
then
이후에서 콜백 함수 내에서 로그한게 아니라 그냥 콘솔 로그만 해버렸다.
저 부분을.then((res) => { console.log('비동기 처리 완료!'); return res; }) .catch(console.error);
이런식으로 바꿔주니 원래 예상했던 대로 반환이 먼저 되고 비동기 처리가 완료된다.
그러니 반환 하기 전 promise.all
비동기 함수에서 기다릴 수 있도록 async/await
를 이용해 함수를 선언해주자
이후 함수를 호출 할 때도 await
를 써야 하기 때문에 호출 할 때도 async/await
를 이용해주자
await
는async
가 선언된 함수에서만 선언 될 수 있기 때문이다.
async / await
를 사용해보자내가 30분동안 계속 머리를 싸맸던 문제가 있었다.
그건 바로 내가 생각했던 것 처럼 함수를 선언 할 때에도 async/await
를 써야 하고 호출 할 때에도 async/await
써야 하는걸로 알고 있었는데
이번에 호출 할 때에만 async/await
를 사용해도 마치 비동기 함수가 동기 함수처럼 쓰이는 것처럼 되더라
getAllData() {
const urlArray = this.createUrl();
const allPromises = Promise.all(
urlArray.map((url) => fetch(url).then((res) => res.json())),
)
.then((res) => {
console.log('비동기 처리 완료!');
return res;
})
.catch(console.error);
return allPromises;
}
}
const api = new LocateFetch(apiKey);
api.init();
const result = (async () => {
const preResult = await api.getAllData();
console.log(preResult);
return preResult;
})();
처음엔 이걸 보고 왜지 ? 함수 선언 할 때 안해도 호출 할 때 async/await
만 쓰면 알아서 비동기 처리들에 모두 await
를 일률적으로 적용시키나 ? 이렇게 생각했다.
하지만 이유를 깨닫고 나서는 내가 async/await
에 대해서 제대로 이해 못하고 있었음을 깨달았다.
async/await
는 선언문이다 .await
선언문 이후 존재하는 비동기 처리가 있을 경우
해당 비동기 처리가 완료 됐을 때 다음 코드 블록으로 넘어간다.
await Promise 객체 ..
와 같은 문이 있을 경우엔 await
선언문 뒤에 존재하는 Promise
가 settled
될 때 까지 기다린다.
...
const api = new LocateFetch(apiKey);
api.init();
const result = (async () => {
const preResult = await api.getAllData();
// 처음 평가된 식은 pending 상태의 Promise 객체
// await 는 해당 Promise 객체가 settled 될 때 까지 기다림
// settled 되면 그 다음 코드 실행
console.log(preResult);
return preResult;
})();
그러니 await api.getAllData()
표현식은 getAllData()
메소드 내부 코드와는 전~혀 상관 없이 그저 pending
상태의 Promise
객체를 받고 기다렸을 뿐이다.
해당 로직을 네트워크 요청을 통해 제대로 알아보자
async/await
하지 않고 호출 할 때만 async/await
했을 때함수를 선언 할 때에는 async/await
를 선언해주지 않고 호출 할 때에만 await
를 하였다.
좀 더 명확한 비교를 위해 똑같은 비동기 처리하는 함수를 2번 실행했다.
GET
요청이 날라가는 것을 보면 5개의 GET
요청을 보내는 2번의 Promise.all
들이 이전 요청이 모두 완료 된 후 진행 되는 것이 아니라
비동기적으로 요청이 날아가는 모습을 볼 수 있다.
async/await
를 사용했을 때이번엔 선언 할 때에도 비동기 처리인 Promise.all
에게 async/await
를 적용해주었더니
이제는 제대로 동기적인 함수처럼 진행 되는 것을 볼 수 있다.
완~전~
async/await
에 대해 잘못 알고 있었구만
그래도 이번 기회에 알 수 있어서 다행이다.
async getAllData() {
const urlArray = this.createUrl();
const results = [];
const allPromises = await Promise.all(
urlArray.map((url) => fetch(url).then((res) => res.json())),
)
.then((allResponse) => allResponse.forEach((res) => results.push(res)))
.catch(console.error);
return results;
}
}
const api = new LocateFetch(apiKey);
api.init();
const result = (async () => {
const preResult = await api.getAllData();
console.log(preResult);
return preResult;
})();
그래서 ~! async/await
를 이용해서 메소드를 완성 시켜주었다.
GET
요청이 모두 잘 일어나는 모습을 볼 수 있다.
이제 파싱한 결과를 이용해 컴포넌트화 시켜놨던 html
에다가 값들을 넣어 동적으로 렌더링 해야 하니
렌더링 하기 편하게 파싱한 결과들을 재가공하자
{
'Seoul' : {
1 : {
상태명 : 상태값 ,
상태명 : 상태값
}
},
2 : {
상태명 : 상태값 ,
상태명 : 상태값
}
}
이런 식의 구조로 가공해두려고 한다.
가장 첫 번째 프로퍼티는 지역 별로 , 두 번쨰 프로퍼티는 일별로 , 나머지는 상태명과 상태값을 값으로 가지도록 말이다 .
이렇게 가공해두면 나중에 렌더링 할 때 디스트럭처링을 이용해서 이쁘게 짤 수 있을 것 같았다.
받아지는 결과값들이 다음처럼 생겼으니 상태명 : 상태값
에 들어가는 것은 해당 일의 category : fcstValue
이런식으로 가공해둬야지
async parseResponse() {
const { locateArr } = this;
const allResponse = await this.getAllData();
const parseResult = {};
locateArr.forEach((loc, index) => {
const { address } = loc;
const preParse = {};
const {
response: {
body: { items: item },
},
} = allResponse[index];
item.item.forEach((data) => {
const { fcstTime, category, fcstValue } = data;
if (preParse[fcstTime]) {
preParse[fcstTime][category] = fcstValue;
} else {
preParse[fcstTime] = { [category]: fcstValue };
}
});
parseResult[address] = preParse;
});
return parseResult;
}
}
const api = new LocateFetch(apiKey);
api.init();
const result = (async () => {
const preResult = await api.parseResponse();
console.log(preResult);
return preResult;
})();
객체 디스트럭처링과 이중 반복문을 이용해서 원하는 형태로 가공해서 저장해줬다.
밤 늦게까지 연습하다보니 위에서 대충 말헀던
이 부분에 대한 문제가 생겼다.
문제가 뭐였냐면 오전 12시 ~ 오전 2시 사이까지는 다음날 오후 11시꺼를 요청하게 됐기 때문에NO DATA
오류 메시지가 발생했다.
하지만 ~ 코드를 업데이트 해주었다.setParams() { // basedate, basetime 설정하기 const currentDate = new Date(); let koreanTime; // 오전 12시 ~ 02시 사이에는 전일 23시 데이터를 가져와야 함 if (currentDate.getHours() < 2) { koreanTime = currentDate.setDate(currentDate.getDate() - 1); }
이런식으로 하게되면 현재가 오전 12시 ~ 02시 사이더라도
파라미터를 계산할 때는 아무리 늦어도 전일 11시59분으로 계산되기 때문에 문제없이 파싱이 잘 된다. 키키킥
오늘은 하루종일 API
만 연습했던 것 같다 .
생각보다 금방 끝날줄 알았는데 내가 async/await
에 대한 개념이 부족했던 터라 오래걸렸다.
그래도 오래 걸렸더라도 얻은게 있으니 오히려 더 좋았다.
내일 마저 렌더링 하는 함수까지만 완성하고 마무리해야지 !