본문 바로가기
React

[React] API로 날씨 앱 만들기 3(OpenWeather API 사용)

by IT 정복가 2023. 3. 1.
728x90

https://conquer-it.tistory.com/195

API로 날씨 앱 만들기 2(OpenWeather API 사용)

 

[React] API로 날씨 앱 만들기 2(OpenWeather API 사용)

https://conquer-it.tistory.com/194 API로 날씨 앱 만들기 1(OpenWeather API 사용) 4. 현재 위치한 날씨 데이터 보여주기 저번까지 동작은 하지 않는 UI까지 만들었다. 이제 API를 통해 이 UI가 동작하게 만들 차례

conquer-it.tistory.com


6. 도시별 날씨 가져오기

우선 여러 도시의 날씨를 불러오기 위해

App.js에서 city라는 state를 만든다.

const [city, setCity] = useState("");

그 후에 setCity를 <WeatherButton />의 props로 보낸다.

  return (
    <>
      <div className="container">
        <WeatherBox weather={weather} />
        <WeatherButton cities={cities} setCity={setCity} /> //props로 보낸다.
      </div>
    </>
  );

그 후 <WeatherButton />의 도시 버튼에 onClick 이벤트를 주어 setCity를 통해 아이템을 읽을 수 있도록 한다.

import React from "react";
import { Button } from "react-bootstrap";

const WeatherButton = ({ cities, setCity }) => {
  console.log(cities);
  return (
    <div className="weather-btn">
      <Button variant="warning" className="btn">
        Current Location
      </Button>

      {cities.map((item) => (
        <Button variant="warning" className="btn" onClick={() => setCity(item)}>
          {item}
        </Button>
      ))}
    </div>
  );
};

export default WeatherButton;

과연 잘 읽어오는지 useEffect로 확인을 해보자

App.js로 와서 useEffect를 만들자.

  useEffect(() => {
    console.log("city", city);
  }, [city]);

각 도시별 버튼을 눌렀을 때 도시명이 잘 뜨는 것을 확인할 수 있다.

이제 이것을 통해 도시별 기온을 화면에 뿌려주기만 하면 된다.


화면에 정보를 뿌려주기 위해 위의 useEffect의 콘솔 부분을 지워주고 

getWeatherByCity()라는 함수를 실행시켜주자.

이 함수에서는 지역별 날씨를 api를 통해 부르는 역할을 한다.

  const getWeatherByCity = async () => {
    let url = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=07c8f80150954d942a79882827366bc7&units=metric`;
    let response = await fetch(url);
    let data = await response.json();
    setWeather(data);
  };

  useEffect(() => {
    if (city == "") {
      getCurrentLocation();
    } else {
      getWeatherByCity();
    }
  }, [city]);

여기까지 작성하고 실행을 해보면

클릭한 도시의 날씨 정보가 잘 나오는 것을 확인할 수 있다.


7. 로딩 스피너 만들기

우리가 다른 지역의 버튼을 클릭할 때마다 api를 불러야 하기때문에 약간의 시간이 걸리는데

이때 로딩 스피너가 있다면 유저 입장에서는 인내심을 가지고 기다려 줄 수 있을 것이다.

(로딩 스피너가 나오는 타이밍은 데이터가 도착하기 전이 될 것이다.)

 

리액트 로딩 스피너 사이트

https://www.npmjs.com/package/react-loader-spinner

 

react-loader-spinner

react-spinner-loader provides simple React.js spinner component which can be implemented for async wait operation before data load to the view.. Latest version: 5.3.4, last published: 6 months ago. Start using react-loader-spinner in your project by runnin

www.npmjs.com

우선 터미널을 켜서 npm을 설치해준다.

그 후에 자신이 원하는 로딩 스피너를 골라서 코드에 복붙하면 된다.

만약 이것이 마음에 든다고 한다면

App.js로 가서 import를 추가 작성해준다.

import { ThreeDots } from "react-loader-spinner";

그 후에 위의 코드를 App()의 return 부분에 냅다 붙여넣기를 하면 

아래와 같이 로딩 스피너가 계속 보여질 것이다.

 return (
    <>
      <div className="container">
        <ThreeDots
          height="80"
          width="80"
          radius="9"
          color="#4fa94d"
          ariaLabel="three-dots-loading"
          visible={true}
        />
        <WeatherBox weather={weather} />
        <WeatherButton cities={cities} setCity={setCity} />
      </div>
    </>
  );

이 문제를 어떻게 해결할 수 있을까?

 

로딩 스피너가 화면에 나올 수 있는 유일한 시간은 api 데이터를 가져오는 시간 뿐이다.

그렇기 때문에 데이터를 가져오는 동안에는 visible이 true이다가 데이터가 도착을 했으면 false로 바뀌어야 한다.

 

visible이라는 state를 만들어서 초기 값으로 false를 준다.

그리고 getWeatehrByCurrentLocation()와 getWeatherByCity()에서 데이터를 부를때

visible의 값을 true로 바꾸고 데이터가 도착을 하면 다시 false로 바꿔준다.

const [visible, setVisible] = useState(false);

//생략

  const getWeatherByCurrentLocation = async (lat, lon) => {
    let url = `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=07c8f80150954d942a79882827366bc7&units=metric`;
    setVisible(true);
    let response = await fetch(url);
    let data = await response.json();
    setWeather(data);
    setVisible(false);
  };

  const getWeatherByCity = async () => {
    let url = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=07c8f80150954d942a79882827366bc7&units=metric`;
    setVisible(true);
    let response = await fetch(url);
    let data = await response.json();
    setWeather(data);
    setVisible(false);
  };
  
  //생략

 

 그 후에 return부분을 아래와 같이 바꿔주면 로딩 스피너까지 구현에 성공했다.

return (
    <>
      {visible ? (
        <div className="container">
          <ThreeDots
            height="80"
            width="80"
            radius="9"
            color="#4fa94d"
            ariaLabel="three-dots-loading"
            visible={visible}
          />
        </div>
      ) : (
        <div className="container">
          <WeatherBox weather={weather} />
          <WeatherButton cities={cities} setCity={setCity} />
        </div>
      )}
    </>
  );

8. Current Location 버튼을 누르면 현재 위치의 날씨 보여주기

현재 다른 지역의 버튼을 누르고 다시 Current Location 버튼을 누르면 아무 일도 일어나지 않는다.

다시 현재 위치의 날씨를 보여주기 위해서는 

Current Location 버튼을 눌렀을 때 getCurrentLocation() 함수가 실행되어야 한다.

그렇기 때문에 getCurrentLocation()을 <WeatherButton/>의 props로 보낸다.

 

App.js

<WeatherButton cities={cities} setCity={setCity} getCurrentLocation={getCurrentLocation}/>

 

그러면 <WeatherButton />에서 이 함수를 받아 onClick 이벤트로 실행시켜주면

현재 위치의 날씨가 잘 출력된다.

WeatherButton.js

import React from "react";
import { Button } from "react-bootstrap";

const WeatherButton = ({ cities, setCity, getCurrentLocation }) => {
  return (
    <div className="weather-btn">
      <Button variant="warning" className="btn" onClick={getCurrentLocation}>
        Current Location
      </Button>

     // 생략
    </div>
  );
};

export default WeatherButton;


전체코드

App.js

import { useEffect,useState } from 'react';
import './App.css';
import "bootstrap/dist/css/bootstrap.min.css";
import WeatherBox from "./component/WeatherBox";
import WeatherButton from "./component/WeatherButton";
import { ThreeDots } from "react-loader-spinner";

function App() {
  const [weather, setWeather] = useState(null);
  const cities = ["Paris", "New York", "London", "Busan"];
  const [city, setCity] = useState("");
  const [visible, setVisible] = useState(false);

  //위치 가져오기
  const getCurrentLocation = () => {
    navigator.geolocation.getCurrentPosition((position) => {
      let lat = position.coords.latitude;
      let lon = position.coords.longitude;
      getWeatherByCurrentLocation(lat, lon);
    });
  };

  const getWeatherByCurrentLocation = async (lat, lon) => {
    let url = `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=07c8f80150954d942a79882827366bc7&units=metric`;
    setVisible(true);
    let response = await fetch(url);
    let data = await response.json();
    setWeather(data);
    setVisible(false);
  };

  const getWeatherByCity = async () => {
    let url = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=07c8f80150954d942a79882827366bc7&units=metric`;
    setVisible(true);
    let response = await fetch(url);
    let data = await response.json();
    setWeather(data);
    setVisible(false);
  };

  useEffect(() => {
    if (city == "") {
      getCurrentLocation();
    } else {
      getWeatherByCity();
    }
  }, [city]);

  return (
    <>
      {visible ? (
        <div className="container">
          <ThreeDots
            height="80"
            width="80"
            radius="9"
            color="lightblue"
            ariaLabel="three-dots-loading"
            visible={visible}
          />
        </div>
      ) : (
        <div className="container">
          <WeatherBox weather={weather} />
          <WeatherButton
            cities={cities}
            setCity={setCity}
            getCurrentLocation={getCurrentLocation}
            selectedCity={city}
          />
        </div>
      )}
    </>
  );
}

export default App;

WeatherBox.js

import React from "react";

const WeatherBox = ({ weather }) => {
  console.log(weather);

  return (
    <div className="weather-box">
      <div>{weather?.name}</div>
      <h2>
        {weather?.main.temp}℃ / {(weather?.main.temp * 1.8 + 32).toFixed(2)}℉
      </h2>
      <h3>{weather?.weather[0].description}</h3>
    </div>
  );
};

export default WeatherBox;

WeatherButton.js

import React from "react";
import { Button } from "react-bootstrap";

const WeatherButton = ({
  cities,
  setCity,
  getCurrentLocation,
  selectedCity,
}) => {
  return (
    <div className="weather-btn">
      <Button
        variant={`${selectedCity == null ? "outline-warning" : "warning"}`}
        className="btn"
        onClick={getCurrentLocation}
      >
        Current Location
      </Button>

      {cities.map((city) => (
        <Button
          variant={`${selectedCity == city ? "outline-warning" : "warning"}`}
          className="btn"
          onClick={() => setCity(city)}
        >
          {city}
        </Button>
      ))}
    </div>
  );
};

export default WeatherButton;

App.css

body{
    background-image: url(https://free4kwallpapers.com/uploads/originals/2015/09/16/weather-live-wallpaper-icon.jpg);
    height: 100vh;
    background-position: center;
}

.container{
    display: flex;
    justify-content: center;
    flex-direction: column;
    align-items: center;
    height: 100vh;
}

.weather-box{
    border: 3px solid #ffc107;
    padding: 30px 50px;
    border-radius: 10px;
    margin-bottom: 20px;
    width: 650px;
    background-color: #48aac09d;
}

.weather-btn{
    border: 3px solid black;
    padding: 20px 50px;
    border-radius: 10px;
    background-color: black;
    width: 650px;
    display: flex;
    justify-content: center;
}
.btn:last-child{
    margin: 0;
}
.btn{
    margin-right: 15px;
}

.btn:hover{
    background-color: red;
    color:red;
}

728x90