[개발일지 41일차] React로 날씨 앱 만들기

MSJ·2022년 6월 30일
0

WEB

목록 보기
41/41
post-thumbnail

2022-06-30

날씨 앱 만들기

구현 기능

  1. 앱이 실행되자마자 현재 위치 기반의 날씨가 보이게 한다
  2. 날씨 정보에는 도시, 섭씨, 화씨, 날씨 상태를 보여준다
  3. 5개의 버튼이 있다. (1개: 현재 위치, 4개: 다른 도시)
  4. 도시 버튼을 클릭할 때 도시별 날시가 나온다
  5. 현재 위치 버튼을 누르면 다시 현재 위치 기반 날씨가 나온다
  6. 데이터를 들고오는 동안 로딩 스피너가 돈다

동기와 비동기

동기

  • task(작업)들이 순차적으로 이루어지는 것
  • 다른 작업들을 blocking

비동기

  • task들이 순차적으로 이루어지지 않음
  • 다른 작업들 non-blocking
  • Javascript를 사용한 비동기 통신 방식을 Ajax 라고 부름
  • http 요청(GET, POST), 이벤트 핸들러(click, over...), setTimeout, setInterval 등이 있음

비동기

Callback

  • 콜백 함수 : 인자로 들어오는 함수
  • 비동기 통신을 할 수 있는 한 패턴
  • 문제점 : 콜백 헬로 인한 에러처리 불가, 가독성 hell

Promise

  • ES6에 나온 비동기 패턴
  • 비동기 통신의 결과상태를 저장하는 객체다
  • 후속처리 메서드로 then(성공), catch(에러), finally(무조건)가 있다

async/await

  • Promise의 복잡성으로 인해 ES8에서 나온 비동기 패턴
  • Promise를 기반으로 하며, 완전히 같지는 않으나 사용하기 편함

구현 해보기

App.css

body {
    background: url(https://t4.ftcdn.net/jpg/04/97/80/99/360_F_497809944_FMo3DO6j7XSlb9rZKOlnqaaWoJhuZXBm.jpg);
    height : 100vh;
    background-repeat: no-repeat;  
    background-size: cover;
}

.weather-card{
    background-color: rgba(52,52,52,.7);
    padding: 50px;
    border: 2px solid #fff;
    border-radius: 20px;
    max-width: 700px;
    width:100%;
    height: 300px;
  }
  .main-container{
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 100%;
    
  }
  .menu-container{
    display: flex;
    justify-content: center;
    background-color: #000;
    border-radius: 60px;
    max-width: 700px;
    width:100%;
    padding: 30px;
    margin-top: 30px;
  
  }
  .menu-container Button{
    margin-right:30px;
  }
  
  .menu-container Button:hover{
    background-color: #ffc107;
  }

App.js

import React, { useState, useEffect } from 'react';
import 'bootstrap/dist/css/bootstrap.min.css';
import './App.css';
import { Container } from 'react-bootstrap';
import WeatherButton from './components/WeatherButton';
import WeatherBox from './components/WeatherBox';
import { ClipLoader } from 'react-spinners';

const cities = ['paris', 'new york', 'tokyo', 'seoul'];
const API_KEY = 'b4a0b63fb0a27cc709f6ea5ecd5f5d7d';

const App = () => {
  const [loading, setLoading] = useState(false);
  const [city, setCity] = useState(null);
  const [weather, setWeather] = useState(null);
  const [apiError, setAPIError] = useState('');

  const getWeatherByCurrentLocation = async (lat, lon) => {
    console.log('현재 위치', lat, lon);
    //비동기 처리
    try {
      let url = //units=metric 캘빈을 섭씨로
        // let url = `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${API_KEY}`;
        // `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${API_KEY}`;
        'http://api.openweathermap.org/data/2.5/weather?lat=35.87222&lon=128.60250&appid=b4a0b63fb0a27cc709f6ea5ecd5f5d7d';
      //&units=metric
      const res = await fetch(url);
      const data = await res.json();
      setWeather(data);
      setLoading(false);
    } catch (err) {
      setAPIError(err.message);
      setLoading(false);
    }
  };

  const getCurrentLocation = () => {
    navigator.geolocation.getCurrentPosition((position) => {
      const { latitude, longitude } = position.coords;
      getWeatherByCurrentLocation(latitude, longitude);
      // console.log('현재 위치', lat, lon);
    });
  };

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

      setWeather(data);
      setLoading(false);
    } catch (err) {
      console.log(err);
      setAPIError(err.message);
      setLoading(false);
    }
  };

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

  const handleCityChange = (city) => {
    if (city === 'current') {
      setCity(null);
    } else {
      setCity(city);
    }
  };

  return (
    <Container className="vh-100">
      {loading ? (
        <div className="w-100 vh-100 d-flex justify-content-center align-items-center">
          <ClipLoader color="#f86c6b" size={150} loading={loading} />
        </div>
      ) : !apiError ? (
        <div class="main-container">
          <WeatherBox weather={weather} />
          <WeatherButton
            cities={cities}
            handleCityChange={handleCityChange}
            selectedCity={city}
          />
        </div>
      ) : (
        apiError
      )}
    </Container>
  );
};

export default App;

WeatherBox.js

import React from 'react';
import { Card } from 'react-bootstrap';

const WeatherBox = ({ weather }) => {
  const temperatureC =
    weather && weather.main ? (weather.main.temp - 273.15).toFixed(2) : '';
  const temperatureF =
    weather && weather.main
      ? (((weather.main.temp - 273.15) * 9) / 5 + 32).toFixed(2)
      : '';
  return (
    <Card className="weather-card">
      {/* <Card.Img src="holder.js/100px270" alt="Card image" /> */}
      <Card.ImgOverlay className="d-flex flex-column justify-content-center text-center">
        <Card.Title>{weather?.name}</Card.Title>
        <Card.Text className="text-success h1">
          {`${temperatureC} °C / ${temperatureF} °F`}
        </Card.Text>
        <Card.Text className="text-info text-uppercase h2">
          {weather && weather.weather[0]?.description}
        </Card.Text>
      </Card.ImgOverlay>
    </Card>
  );
};

export default WeatherBox;

WeatherButton.js

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

const WeatherButton = ({ cities, selectedCity, handleCityChange }) => {
  return (
    <div class="menu-container">
      <Button
        variant={`${selectedCity === null ? 'outline-warning' : 'warning'}`}
        onClick={() => handleCityChange('current')}
      >
        Current Location
      </Button>

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

export default WeatherButton;

PublicNavbar.js

import React from 'react';
import { Navbar, Nav } from 'react-bootstrap';

const PublicNavbar = () => {
  return (
    <Navbar bg="light" expand="lg" className="position-fixed navbar-fixed">
      <Navbar.Brand></Navbar.Brand>
      <Nav className="mr-auto"></Nav>
      <Nav>
        <a
          href="https://github.com/dhminh1024/cs_weather_app"
          target="_blank"
          rel="noreferrer"
        ></a>
      </Nav>
    </Navbar>
  );
};

export default PublicNavbar;

App.test

import { render, screen } from '@testing-library/react';
import App from './App';

test('renders learn react link', () => {
  render(<App />);
  const linkElement = screen.getByText(/learn react/i);
  expect(linkElement).toBeInTheDocument();
});

index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

index.css

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}

reportWebVitals

const reportWebVitals = onPerfEntry => {
  if (onPerfEntry && onPerfEntry instanceof Function) {
    import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
      getCLS(onPerfEntry);
      getFID(onPerfEntry);
      getFCP(onPerfEntry);
      getLCP(onPerfEntry);
      getTTFB(onPerfEntry);
    });
  }
};

export default reportWebVitals;

setupTests

// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

어려운 점

기본 메커니즘에 대한 이해가 더 필요해보임

해결 방법

해당 관련해서 많이 서술된 블로그가 있다

소감

예제로 했던 것 보다 갑자기 난이도가 뛴 기분이다

profile
제로부터 시작하는 프로그래밍&코딩

0개의 댓글