원형 회전 카드 배너 만들기 - API 요청하기 (뜻밖의 수확)

ChoiYongHyeun·2024년 1월 19일
0

망가뜨린 장난감들

목록 보기
9/19
post-thumbnail

지난번에 디자인까지 만들어뒀다.

디자인도 만들고, 이벤트 핸들러도 구현해놨으니 API 요청을 보내서 파라미터를 만들고

컴포넌트들은 페이지에 뿌려주기만 하면 된다.

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 요청을 보낼 URLcreateURL 을 이용해서 배열 형태로 만들어주자


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();


서버에서 요청을 받아오는데까지 걸리는 시간이 대략 800 1200ms800 ~ 1200ms 사이니 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 를 이용해주자

awaitasync 가 선언된 함수에서만 선언 될 수 있기 때문이다.

비동기 함수를 호출 할 때 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 선언문 뒤에 존재하는 Promisesettled 될 때 까지 기다린다.

...
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 에 대한 개념이 부족했던 터라 오래걸렸다.

그래도 오래 걸렸더라도 얻은게 있으니 오히려 더 좋았다.

내일 마저 렌더링 하는 함수까지만 완성하고 마무리해야지 !

profile
빨리 가는 유일한 방법은 제대로 가는 것이다

0개의 댓글