AccuWeather API와 Next.js로 사용자 위치 기반 날씨 정보 구현하기

루리·2024년 11월 1일
0

Next.js

목록 보기
2/2
post-thumbnail

과정

AccuWeather 사이트에서 앱 정보 등록하기

  1. 회원가입, 로그인 한다.
  2. My Apps에서 Add a new App
  3. 필요한 정보를 입력하고 CREATE APP을 해준다.
  4. API Key를 확인하고, .env 파일에 넣어준다.

    NEXT_PUBLIC_ACCUWEATHER_KEY=*******************

브라우저에서 현재 위치 받아오기

  1. 사용자의 현재 위치를 받아오는 코드를 작성한다.
'use client'

import { useEffect, useState } from "react";

interface Location {
  latitude: number | null;
  longitude: number | null;
}

export default function Home() {
  const [location, setLocation] = useState<Location>({ latitude: null, longitude: null });
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const getCurrentLocation = () => {
      if (navigator.geolocation) {
        navigator.geolocation.getCurrentPosition(
          (position) => {
            // 성공적으로 위치를 가져온 경우
            const { latitude, longitude } = position.coords;
            setLocation({ latitude, longitude });
          },
          (err) => {
            // 위치 가져오기 실패 시 오류 메시지 저장
            setError(err.message);
          }
        );
      } else {
        // Geolocation을 지원하지 않는 브라우저의 경우 오류 메시지 저장
        setError("이 브라우저는 Geolocation을 지원하지 않습니다.");
      }
    };

    getCurrentLocation();
  }, []); // 컴포넌트가 처음 렌더링될 때 한 번만 실행

  return (
    <>
      <div className="flex flex-col items-center justify-center w-full h-full">
        {error ? (
          <div className="text-red-500">오류: {error}</div>
        ) : (
          <div>
            <div className="font-bold">사용자의 현재 위치</div>
            <div>{location.latitude} / {location.longitude}</div>
          </div>
        )}
      </div>
    </>
  );
}


사용자가 브라우저에서 위치 권한을 허용하면

이렇게 현재 위치를 가져올 수 있다.

만약 위치 권한을 차단한다면 이렇게 오류 메시지가 나온다.

CORS 에러 임시 우회

※ API를 받아오기 전에,
accuweather의 api는 CORS 에러를 발생시켰습니다.
필요하신 분은 임시로 아래 방법으로 CORS 에러를 우회하시기 바랍니다.
cors-anywhere

  • 홈페이지에 접속하여 버튼을 누른다.
  • 호출할 API 앞에 https://cors-anywhere.herokuapp.com 주소를 붙인다.

    저도 이 사이트를 이번에 알게 되었는데 앞으로 테스트 용도로 요긴하게 사용할 것 같네요 😀

위도, 경도로 지역 정보 받아오기

  1. 사용자의 위도, 경도를 이용해 AccuWeather API로 지역 정보를 받아오는 코드를 작성한다.
    weatherApi.ts
interface Location {
    key: number;
    city: string;
    localizedName: string;
}

export async function getLocation(lat: number, lng: number) {
    const response = await fetch(
      // CORS error를 임시로 우회
        `https://cors-anywhere.herokuapp.com/http://dataservice.accuweather.com/locations/v1/cities/geoposition/search?apikey=${process.env.NEXT_PUBLIC_ACCUWEATHER_KEY}&q=${lat}%2C${lng}&language=ko-kr`,
        {
            method: 'GET',
            headers: {
                'Content-Type': 'application/json',
            },
        }
    );
    const result = await response.json();

    if (result) {
        const location: Location = {
            key: result.ParentCity.Key, // 날씨 api에 사용할 지역 키
            city: result.AdministrativeArea.LocalizedName, // 화면에 보여줄 지역 정보
            localizedName: result.ParentCity.LocalizedName, // 화면에 보여줄 지역 정보
        };
        return location;
    } else {
        throw new Error(result);
    }
}
  • 쿼리 스트링 정보

  • 응답 json 데이터

{
    "Version": 1,
    "Key": "3429991", // 도시의 고유 키
    "Type": "City", // 데이터 타입
    "Rank": 55, // 도시의 순위
    "LocalizedName": "**동", // 지역의 한국어 이름 (마스킹 처리)
    "EnglishName": "**-dong" // 지역의 영어 이름 (마스킹 처리)
    "PrimaryPostalCode": "", // 우편번호 (nullable)
    "Region": {
        "ID": "ASI",
        "LocalizedName": "아시아",
        "EnglishName": "Asia"
    },
    "Country": {
        "ID": "KR",
        "LocalizedName": "대한민국",
        "EnglishName": "South Korea"
    },
    "AdministrativeArea": {
        "ID": "11",
        "LocalizedName": "서울시",
        "EnglishName": "Seoul",
        "Level": 1,
        "LocalizedType": "특별시",
        "EnglishType": "Special City",
        "CountryID": "KR"
    },
    "TimeZone": {
        "Code": "KST",
        "Name": "Asia/Seoul",
        "GmtOffset": 9.0,
        "IsDaylightSaving": false,
        "NextOffsetChange": null
    },
    "GeoPosition": {
        "Latitude": 37.***, // 위도 (마스킹 처리)
        "Longitude": 126.***, // 경도 (마스킹 처리)
        "Elevation": {
            "Metric": {
                "Value": 18.0, // 해발 고도 (미터)
                "Unit": "m", // 단위 (미터)
                "UnitType": 5 // 단위 유형
            },
            "Imperial": {
                "Value": 59.0, // 해발 고도 (피트)
                "Unit": "ft", // 단위 (피트)
                "UnitType": 0 // 단위 유형
            }
        }
    },
    "IsAlias": false,
    "ParentCity": {
        "Key": "2330435",
        "LocalizedName": "강서구",
        "EnglishName": "Gangseo-gu"
    },
    "SupplementalAdminAreas": [
        {
            "Level": 2,
            "LocalizedName": "강서구",
            "EnglishName": "Gangseo-gu"
        }
    ],
    "DataSets": [
        "AirQuality",
        "AirQuality-Regional",
        "AirQualityCurrentConditions",
        "AirQualityForecasts",
        "Alerts",
        "DailyLocalIndices",
        "ForecastConfidence",
        "FutureRadar",
        "HourlyLocalIndices",
        "MinuteCast",
        "PremiumAirQuality",
        "Radar",
        "TidalForecast"
    ]
}
  • page.tsx
'use client'

import { getLocation, getWeather } from "@/api/weatherApi";
import { useEffect, useState } from "react";

interface Location {
  latitude: number | null;
  longitude: number | null;
}
interface LocationData {
  key: number;
  city: string;
  localizedName: string;
}

export default function Home() {
  const [location, setLocation] = useState<Location>({ latitude: null, longitude: null });
  const [error, setError] = useState<string | null>(null);
  const [loading, setLoading] = useState<boolean>(false);
  const [locationData, setLocationData] = useState<LocationData>();
  const [weatherData, setWeatherData] = useState<Weather>();

  const fetchLocationData = async () => {
    if (location.latitude !== null && location.longitude !== null) {
      setLoading(true);
      try {
        const result = await getLocation(location.latitude, location.longitude);
        setLocationData(result);
      } catch (error) {
        console.error("Error fetching location data:", error);
        setError("위치 정보를 가져오는 중 오류가 발생했습니다.");
      } finally {
        setLoading(false);
      }
    }
  };

  useEffect(() => {
    const getCurrentLocation = () => {
      if (navigator.geolocation) {
        navigator.geolocation.getCurrentPosition(
          (position) => {
            // 성공적으로 위치를 가져온 경우
            const { latitude, longitude } = position.coords;
            setLocation({ latitude, longitude });
            fetchLocationData();
          },
          (err) => {
            // 위치 가져오기 실패 시 오류 메시지 저장
            setError(err.message);
          }
        );
      } else {
        // Geolocation을 지원하지 않는 브라우저의 경우 오류 메시지 저장
        setError("이 브라우저는 Geolocation을 지원하지 않습니다.");
      }
    };
    getCurrentLocation();
  }, []); // 컴포넌트가 처음 렌더링될 때 한 번만 실행

  useEffect(() => {
    if (location.latitude !== null && location.longitude !== null) {
      fetchLocationData();
    }
  }, [location]);

  return (
    <>
      {locationData && (
        <div className="mt-4 text-center">
          <div className="font-bold">위치 정보</div>
          <div>도시: {locationData.city}</div>
          <div>지역명: {locationData.localizedName}</div>
          <div>위치 키: {locationData.key}</div>
        </div>
      )}
    </>
  );
}

결과

날씨 정보 받아오기

  1. 날씨 정보를 받아오는 코드 작성한다.
    먼저, 날씨 아이콘들을 assets에 추가한다. AccuWeather Weather Icons
  • weatherApi.ts
interface Weather {
    weatherText: string;
    weatherIcon: number;
    temperature: number;
}

export async function getWeather(key: number) {
    const response = await fetch(
      	// CORS error를 임시로 우회
        `https://cors-anywhere.herokuapp.com/http://dataservice.accuweather.com/currentconditions/v1/${key}?apikey=${process.env.NEXT_PUBLIC_ACCUWEATHER_KEY}&language=ko-kr`,
        {
            method: 'GET',
            headers: {
                'Content-Type': 'application/json',
            },
        }
    );
    const result = await response.json();

    if (result) {
        const weather: Weather = {
            weatherText: result[0].WeatherText, // 날씨 상태
            weatherIcon: result[0].WeatherIcon, // 날씨 아이콘 코드
            temperature: result[0].Temperature.Metric.Value, // 기온 (섭씨)
        };
        return weather;
    } else {
        throw new Error(result);
    }
}
  • 응답 json 데이터
[
    {
        "LocalObservationDateTime": "2024-11-01T09:41:00+09:00", // 관측 시간
        "EpochTime": 1730421660,
        "WeatherText": "흐림", // 날씨 상태
        "WeatherIcon": 7, // 날씨 아이콘 코드
        "HasPrecipitation": false, // 강수 여부
        "PrecipitationType": null, // 강수 유형 (없으면 null)
        "IsDayTime": true, // 낮 여부 (true: 낮, false: 밤)
        "Temperature": {
            "Metric": {
                "Value": 14.5, // 섭씨 온도
                "Unit": "C", // 단위 (섭씨)
                "UnitType": 17 // 단위 유형
            },
            "Imperial": {
                "Value": 58.0, // 화씨 온도
                "Unit": "F", // 단위 (화씨)
                "UnitType": 18 // 단위 유형
            }
        },
        "MobileLink": "http://www.accuweather.com/ko/kr/gangseo-gu/2330435/current-weather/2330435",
        "Link": "http://www.accuweather.com/ko/kr/gangseo-gu/2330435/current-weather/2330435"
    }
]
  • page.tsx
'use client'

import { getLocation, getWeather } from "@/api/weatherApi";
import { useEffect, useState } from "react";

interface Location {
  latitude: number | null;
  longitude: number | null;
}
interface LocationData {
  key: number;
  city: string;
  localizedName: string;
}

interface Weather {
  weatherText: string;
  weatherIcon: number;
  temperature: number;
}

export default function Home() {
  const [location, setLocation] = useState<Location>({ latitude: null, longitude: null });
  const [error, setError] = useState<string | null>(null);
  const [loading, setLoading] = useState<boolean>(false);
  const [locationData, setLocationData] = useState<LocationData>();
  const [weatherData, setWeatherData] = useState<Weather>();

  const fetchLocationData = async () => {
    if (location.latitude !== null && location.longitude !== null) {
      setLoading(true);
      try {
        const result = await getLocation(location.latitude, location.longitude);
        setLocationData(result);
        await fetchWeatherData(result.key);
      } catch (error) {
        console.error("Error fetching location data:", error);
        setError("위치 정보를 가져오는 중 오류가 발생했습니다.");
      } finally {
        setLoading(false);
      }
    }
  };

  const fetchWeatherData = async (locationKey: number) => {
    try {
      const result = await getWeather(locationKey);
      setWeatherData(result);
    } catch (error) {
      console.error("Error fetching weather data:", error);
      setError("날씨 정보를 가져오는 중 오류가 발생했습니다.");
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    const getCurrentLocation = () => {
      if (navigator.geolocation) {
        navigator.geolocation.getCurrentPosition(
          (position) => {
            // 성공적으로 위치를 가져온 경우
            const { latitude, longitude } = position.coords;
            setLocation({ latitude, longitude });
            fetchLocationData();
          },
          (err) => {
            // 위치 가져오기 실패 시 오류 메시지 저장
            setError(err.message);
          }
        );
      } else {
        // Geolocation을 지원하지 않는 브라우저의 경우 오류 메시지 저장
        setError("이 브라우저는 Geolocation을 지원하지 않습니다.");
      }
    };
    getCurrentLocation();
  }, []); // 컴포넌트가 처음 렌더링될 때 한 번만 실행

  useEffect(() => {
    if (location.latitude !== null && location.longitude !== null) {
      fetchLocationData();
    }
  }, [location]);

  return (
    <>
      <div className="flex flex-col items-center justify-center w-full h-full">
        {error && <div>Error: {error}</div>}
        {loading ? (
          <div className="flex items-center">
            Loading...
          </div>
        ) : (
          locationData && weatherData && (
            <div className="flex items-center w-full justify-center">
              <img src={`/assets/weather/${weatherData.weatherIcon}-s.png`} alt="Weather Icon" className="w-1/3 h-auto" />
              <div className="text-left">
                <div className="text-3xl font-bold">{weatherData.temperature} °C</div>
                <div className="font-bold">{weatherData.weatherText}</div>
                <div className="text-slate-500">{locationData.city} {locationData.localizedName}</div>
              </div>
            </div>
          )
        )}
      </div>
    </>
  );
}

결과

전체 코드

GitHub

References

profile
안녕하세요

0개의 댓글