

지난번에 디자인까지 만들어뒀다.
디자인도 만들고, 이벤트 핸들러도 구현해놨으니 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 에 대한 개념이 부족했던 터라 오래걸렸다.
그래도 오래 걸렸더라도 얻은게 있으니 오히려 더 좋았다.
내일 마저 렌더링 하는 함수까지만 완성하고 마무리해야지 !