회원가입을 진행하면 api 키가 기본적으로 발급된다.
npx create-react-app weather-app
: 프로젝트 생성하기
(npx create-react-app 앱이름)
Happy hacking!
문구가 나오면 프로젝트 생성이 잘 된 것이다!
프로젝트 열기
기본 폴더 구조에서 필요없는 부분 비우기
App.js
div 안에 내용 제거 / App.css
내용 제거
실행해보기 - > App.js에 아무 문구 입력 -> 터미널창에서 npm start
로 실행 / 결과
- 사용자는 사이트에 접속 시 현재 위치 기반의 날씨를 확인할 수 있다.
( ex. 지역, 온도, 날씨 상태)- 사용자는 다른 도시의 버튼들을 볼 수 있다.
- 사용자는 다른 도시 버튼을 클릭하면 해당 도시의 날씨 정보를 볼 수 있다.
- 사용자는 데이터가 로딩될 때 로딩 스피너를 볼 수 있다.
해야 할 일 ( 앱의 주요 동작 )
앱이 실행되면 현재 위치 기반의 날씨가 보인다 - > 콘솔에 현재위치함수 찍어보기
import { useEffect, useState } from 'react';
리액트에서 제공하는 useEffect와 useState 훅을 가져온다.
useEffect는 함수 컴포넌트 내에서 side effect를 수행할 수 있게 해주는 훅이며, useState는 상태를 관리할 수 있게 해주는 훅이다.
import './App.css';
외부 CSS 파일을 가져와서 현재 컴포넌트에 적용한다.
이것은 컴포넌트에 스타일링을 적용하는 데 사용된다.
☁️ 코드 설명
function App() { ... }
App 컴포넌트를 정의한다.
이 함수형 컴포넌트는 날씨 앱의 기본적인 레이아웃 및 동작을 담당
const getCurrentLocation = () => { console.log('getCurrentLocation!'); };
getCurrentLocation이라는 함수를 정의
이 함수는 현재 위치를 가져오는 로직을 구현할 예정
현재는 단순히 콘솔에 메시지를 출력하는 역할 수행
useEffect(() => { getCurrentLocation(); }, []);
useEffect 훅을 사용하여 컴포넌트가 마운트되었을 때 getCurrentLocation 함수를 호출
빈 배열을 전달하여 의존성 배열을 비웠으므로,
이 효과는 컴포넌트가 처음 렌더링될 때 한 번만 발생하게 된다.
return <div>hi !</div>;
JSX를 반환한다.
현재는 단순히 "hi !" 라는 텍스트를 포함하는<div />
요소를 반환
export default App;
App 컴포넌트를 외부로 내보낸다.
이 컴포넌트는 index.js에서 import하여 사용한다.
🌞 동작 흐름
앱이 실행되면 useEffect 훅이 실행되어 getCurrentLocation 함수를 호출 - >
getCurrentLocation 함수는 현재 위치를 가져오는 동작을 수행 - >
return 구문에서는<div/>
요소를 반환
geolocation 레퍼런스
https://www.w3schools.com/html/html5_geolocation.asp
브라우저의 Geolocation API를 사용하면 사용자의 현재 위치를 가져올 수 있다 !
앱이 실행되면 위치 권한 요청이 옴 - > 허용 - > 콘솔에 현재 위치의 위도, 경도값이 찍힌다.
☁️ 코드 설명
navigator.geolocation.getCurrentPosition((position) => {...});
브라우저의 Geolocation API를 사용하여 현재 위치를 가져오는 비동기 함수인
getCurrentPosition을 호출한다.
이 함수는 사용자의 위치 정보를 매개변수로 받아와서 콜백 함수 내에서 처리한다.
let lat = position.coords.latitude;
현재 위치의 위도 정보를 가져와서 lat 변수에 저장
let lon = position.coords.longitude;
현재 위치의 경도 정보를 가져와서 lon 변수에 저장
JavaScript에서 익명 함수는 함수의 이름이 없이 직접 정의되어 사용되는 함수를 의미
익명 함수는 다른 함수의 매개변수로 전달되거나, 콜백 함수로 사용되는 경우에 주로 쓰인다.
현재 이 코드에서는(position) => {...}
부분이 익명 함수로서 사용됨.
이 함수는 getCurrentPosition 함수의 콜백 함수로 사용되었으며, 해당 함수는 position 매개변수를 받아와서 처리한다.
이 익명 함수는 getCurrentPosition 함수가 호출되면서 자동으로 실행되고, getCurrentPosition 함수가 위치 정보를 성공적으로 가져오면 이 익명 함수가 호출되어 위치 정보를 처리하게 되는 것.
https://openweathermap.org/api 참고
openweathermap API 호출
function App() {
const getCurrentLocation = () => {
// console.log('getCurrentLocation!');
navigator.geolocation.getCurrentPosition((position) => {
let lat = position.coords.latitude;
let lon = position.coords.longitude;
//console.log('현재 위치', lat, lon);
getWeatherByCurrentLocation(lat, lon);
});
};
const getWeatherByCurrentLocation = async (lat, lon) => {
let url = `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${API_KEY}`;
let response = await fetch(url);
let data = await response.json();
console.log('data', data);
};
☁️ 코드 설명
const getWeatherByCurrentLocation = async (lat, lon) => { ... };
사용자의 현재 위치에 대한 날씨 정보를 가져오는 함수를 정의
이 함수는 async 키워드를 사용하여 비동기 함수임을 나타내고, 위도(lat)와 경도(lon)를 매개변수로 받는다.
let url = https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${API_KEY}`;
OpenWeatherMap API의 현재 날씨 정보를 가져오기 위한 요청 URL 생성
이 URL에는 위도와 경도 정보(lat, lon), API 키(appid)도 함께 전달된다.
let response = await fetch(url);
생성한 URL을 사용하여 API에 요청을 보내고, 응답을 기다린다.
fetch 함수를 사용하여 비동기적으로 HTTP 요청을 수행한다.
let data = await response.json();
API 요청에 대한 응답을 JSON 형식으로 파싱한다.
response.json() 메서드를 사용하여 응답 본문을 JSON 형태로 변환하고,
이를 변수 data에 저장한다.
+++ 추가 설명
response.json() 메서드는 Promise를 반환하며, 이를 await 키워드를 사용하여 비동기적으로 처리한다.
이 메서드는 HTTP 응답의 본문을 읽고 JSON 형식으로 변환한 후에 Promise를 완료(resolve)한다.
이후에는 해당 JSON 데이터가 JavaScript 객체로 파싱되어 변수 data에 저장되는 구조
프로미스 ? 이 객체에 대해선 좀 더 공부가 필요할 것 같다. 헷갈려 빙빙 !
https://velog.io/@kimbangul/fetch%EC%99%80-asyncawait%EB%A1%9C-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0 참고하기
👀 콘솔 결과 확인
결과 - > 객체 형태 - > 현재 위치 기반의 날씨 정보를 콘솔에서 확인할 수 있다.
src 폴더 안에 config.js 파일 생성 - > API 키 저장
app.js에서 API 키 불러오기 - > config.js 파일에서 API 키를 불러와서 사용
이제 app.js에서는 apiKey 변수를 가지고 api키를 필요로 하는 곳에 사용하면 된다 !
프론트단에선 우선 이렇게 처리하고 추후에 서버쪽에 대해 더 알게 되면 서버단에서
API키를 관리하는 것도 배우자 !
큰 틀은 2개
1. 날씨 정보 나타낼 영역
2. 버튼 들어갈 영역
배경 고르다가 시간 다 감 ㅎㅎ
나중에 어울리는 걸로 바꾸자
일단 내가 좋아하는 gif 스타일로 넣어보았다.
WeatherBox 컴포넌트 하나 만들어서 지역, 온도, 간단한 날씨 설명을 넣은 뼈대만 넣고
확인한 결과
버튼은 리액트 부트스트랩 사용해보기
https://react-bootstrap.netlify.app/
사용할 컴포넌트에 import 하기
app.js에 부트스트랩 css import 하기
리액트 부트스트랩 사이트에서 components 메뉴 들어가면 다양하게 있음 !
하나 들고 와서 적용해보고 확인 !
app.js에서 div를 하나 더 생성해서 날씨정보박스, 버튼영역 컴포넌트를 묶고
className
으로 클래스명 설정
app.css에서 스타일 설정하고 중앙으로 이동을 시도해보지만
결과를 확인하면 가로는 중앙으로 갔지만 수직중앙은 안 된 상태로 나온다.
원인은 container 자체의 높이가 작아서 !!
컨테이너에 height를 100vh로 줘서 뷰포트 높이(viewport height)의 100%를 주면
세로 중앙으로 배치가 된다.
정보영역과 버튼영역이 붙어 있는 이유는 display:flex 때문
flex 속성을 쓰면
같은 영역 안에 있는 아이템들을 세로가 아닌 가로정렬로 바꿈 !
이를 세로로 바꾸고싶으면 flex-direction: column;
속성을 쓰면 된다.
날씨정보 영역 윤곽선만 잡아주고 기능 완성 후에 스타일 좀 더 입혀보자 👀
useState 함수를 사용해 상태 생성
const [weather, setWeather] = useState(null);
useState 훅은 첫 번째 요소로 현재 상태 값을, 두 번째 요소로 상태 값을 업데이트하는 함수를 반환한다.
weather : 현재의 날씨 상태를 저장하는 상태 값
setWeather: useState 훅을 통해 생성한 상태 값을 업데이트하는 함수
(setWeather 함수를 호출하면 해당 상태 값이 변경되고, 컴포넌트가 다시 렌더링된다.)
useState(null): useState 훅을 호출하여 초기 상태 값 설정
여기서는 weather 상태의 초기값은 null로 두었다.
(날씨 정보를 받아오기 전에는 초기값이 존재하지 않음을 의미)
현재위치날씨 함수안에서 setWeather 함수에 현재위치날씨 데이터 담기
props 전달
weather 정보를 props로서 WeatherBox에 보내기
WeatherBox 컴포넌트에서 props 받기
이 정보들을 이제 화면에 보여주자 !
서울 - > {weather.name}
를 하고 확인해보면 에러가 난다.
이 에러의 원인은
useEffect !
useEffect가 호출되는 때가 첫번째 ui 렌더 후임
처음에 ui 그릴 때 당시에는 weather의 값이 null임
그래서 weather.name은 null.name 과 같아져서
존재하지 않음 !!
해결 방법
조건부 렌더링 or 삼항연산식 활용
삼항연산식으로 하면 더 간단하게 가능하다.
{weather?.name}
weather가 있으면 name 보여줘 ! 라는 뜻
이제 온도를 나타내주기 위해 데이터를 확인해보니
섭씨 온도로 나타난 값이 없었다.
공식 문서에서 섭씨 온도 검색
이 쿼리를 추가해주면 된다요
요청 url에 & 붙이고 추가해주면 temp의 단위가 바뀌어서 값이 바뀐 걸 확인할 수 있다.
weather가 있다면 main 안에 temp를 보여주라는 뜻 ! 실행하면 잘 나타나고 있다.
화씨는 구하는 공식을 추가하면 된다.
(섭씨°C × 9/5) + 32 = 화씨 °F
날씨 정보는 weather 안에 0번째 데이터에 description을 가져오기 !
{weather?.weather[0].description}
도시별 날씨를 가져오기 이전에 도시버튼을 정리해주자 !!
도시 몇개를 정해서 배열로 만들고 이 배열을
WeatherBtn 컴포넌트에 props로 넘긴다 !
WeatherBtn에서 받아온 props인 cities를 콘솔에 찍어보기
array 함수 활용 !! map 함수 활용해서 배열안에 있는 item들 긁어와서 화면에 보여주기 (array 함수도 연습 더 필요해 ....연습만이 살 길이다 👀)
버튼을 하나씩 넣어주는 것보다
아이템이 몇개든 이런 방식으로 데려오는 게 더 효율적인 것 같다.
WeatherBtn 컴포넌트에는 setWeather를 쓰려고 하면
현재 사용할 수가 없다. 왜냐면 setWeather는 Weather Info(내 폴더에서는 WeatherBox) 컴포넌트에서 만든 업데이트 함수니까 !!
WeatherBtn은 Weather Info에도, 부모인 App에도 정보를 주지 못하는 상태인 것
리액트는 단방향 소통을 한다.
부모에서 자식으로만 데이터를 전달 할 수 있다.
이러면 데이터 흐름을 추적하기는 쉬워지지만
자식이 부모에겐 전달을 못하고
형제끼리도 데이터를 주고받기는 힘들어진다.
해결 방법
app(부모)은 모든 걸 가지고 있고!
필요한 정보는 자식들에게 넘겨주자
자식들은 app이 준 정보로만 작업을 하고
app이 그 정보들을 받아다가 필요로 하는 자식에게 넘겨주자
🌌 컴포넌트를 나누는 또 다른 방식
즉 state를 들고 있는 부모 컴포넌트는 stateful 컴포넌트가 되고
부모가 주는 데이터만 받는 컴포넌트는 stateless 컴포넌트가 되는 것 !
장점 : state가 없는 stateless 컴포넌트는 재사용이 쉽고
데이터를 부모에게서 받아서 쓰기만 하면 됨 !
모든 중요한 데이터들은 부모인 stateful 컴포넌트에 있으니까 유지보수가 쉽다.
stateful 컴포넌트만 주의하고 관리해주면 되므로 !!
결론 : 최대한 모든 컴포넌트를 stateless로 만들고 몇개의 stateful 컴포넌트에서
데이터를 관리하는 구조가 되게끔 구성하기
weatherBtn 컴포넌트에 작성했던 city관련 state를 App.js로 옮기고
props로 setCity 함수를 WeatherBtn에 보내주기 !
(함수도 props로 보내줄 수 있다 !!!)
WeatherBtn에서는 버튼에 onClick 이벤트를 줘서 받아온 setCity함수를 사용 !
이때 이벤트로 인해 바뀌는건 App.js에 있는 city가 바뀌는것 !
WeatherBtn은 어떠한 state도 가지고 있지 않다
그렇다면
city값이 바뀐것이 App에 있는 city state 값을 바꾼게 맞는지 확인해보기
useEffect 함수 생성해서 콘솔 찍어보기
App.js
이제 클릭된 시티의 날씨 데이터를 가져와보자 !
공식문서에서 city name 검색 후 호출 url을 긁어온다.
시티별 날씨 데이터를 불러올 함수를 호출하고
함수 내용 작성
테스트
클릭한 도시버튼의 날씨 정보를 가져오고 있다 !
이제 ui에 이 데이터를 뿌려주면 된다.
setWeather() 함수를 호출해주고 데이터 넣어주기
에러가 난다 ! 이 원인도 useEffect !
getWeatherByCity 함수 안에서는 city를 기준으로 weather를 가지고 오는데
city의 처음 값은 빈 스트링임 !
해결 방안
초기에 useEffect를 두번 부르지 않게 해야 함
-- > 앱이 실행됐을 때 1번 useEffect만 실행되고 2번은 실행되지 않게 해야 함 !
-- > useEffect를 하나로 합쳐주기
💡 상황에 따라 유동적으로 호출되게 하기
처음에 렌더될 땐 시티버튼 누르기 전이니까 city값 없을 때임 !!
이땐 현재위치 날씨 함수만 호출되게 하고 city 값이 생기면(시티버튼누르면)
클릭한 도시 날씨 함수 호출되게 하는 것 !!
결과 - > 클릭한 도시별로 날씨 정보가 잘 나타나고 있다.
api와 api 호출 사이 사이에 공백이 생기는 부분에 로딩 스피너를 넣어주자
(데이터가 도착하기 전! 공백 시간)
https://www.npmjs.com/package/react-spinners
리액트 스피너 활용
스피너 설치
npm install --save react-spinners
사용 예제 참고하기
import ClipLoader from "react-spinners/ClipLoader";
import 추가 + ClipLoader 코드 들고 오기
스피너 state 생성
<ClipLoader color="#ffffff" loading={loading} size={150} />
필요한 부분만 남기고 실행해보니 오류가 났다.
ESLint 오류는 모듈의 본문 안에 import 문이 있고, 이를 파일 맨 위로 옮기라고 권장한다 - > App.js 파일에서 모든 import 문을 파일 맨 위로 옮기니 해결됐다.
스피너의 상태를 true로 두면 스피너가 처음부터 계속 나타나고
false로 두면 안 보이게 된다.
스피너를 보여줘야 할 때만 보여주게 처리하자
이 경우 데이터를 fetch 할 때 ! false - > true가 되게 했다가
데이터가 도착했으면 다시 true - > false 처리
fetch 할 땐 true로 data가 온 뒤엔 false로 처리 / 마찬가지로 도시별날씨함수에도 추가
결과 - > 스피너가 공백 사이에 나타나고 있다.
근데 이러면 너무 박스가 움직이니까 스피너가 나타날 땐
박스를 안 보이게 처리하기
js 삼항연산식을 사용하여 loading이 true라면 로딩스피너만 보이게 false라면 그 외 ui가 보이게 설정 - > 결과
++ 이렇게 너무 긴 소수점자리까지 나오는 경우가 있음 !!
JavaScript의 내장 함수인 toFixed()를 사용해서
화씨 온도를 소수점 두 자리까지 반올림하여 표시하기
현재 위치 버튼을 클릭하면 다시 원래의 내 위치기반 날씨가 보이지 않는데
이는 WeatherBtn 컴포넌트에서 현재 위치 버튼에 대한 onClick 핸들러가 정의되어 있지 않았기 때문에 아무 동작을 하지 않는 것 !
onClick 핸들러를 추가하여 현재 위치 버튼을 클릭했을 때 getCurrentLocation 함수를 호출 - > App.js 파일에서 WeatherBtn 컴포넌트를 렌더링할 때 getCurrentLocation 함수를 props로 전달하기 !!
이러면 현재 위치 버튼을 클릭했을 때 현재 위치에 대한 날씨 정보 데이터를 불러온다.
현재 클릭한 버튼을 구분해주기 !
App 컴포넌트에서 상태를 관리하고, 선택된 버튼의 정보를 WeatherBtn 컴포넌트로 props로 전달하게끔 해보기
이를 통해 WeatherBtn은 선택된 버튼에 대한 정보를 받아와서 해당 버튼을 구분해줄 수 있다.
WeatherBtn.js
App.js
선택된 도시를 관리하는 상태 추가
const [selectedCity, setSelectedCity] = useState(null);
App 컴포넌트에서 WeatherBtn을 렌더링할 때 선택된 도시를 props로 전달 - > 선택된 도시 정보를 App 컴포넌트에서 관리하고 해당 정보를 WeatherBtn 컴포넌트로 전달하면 WeatherBtn 컴포넌트에서는 state를 관리하지 않고도 선택된 버튼을 표시할 수 있게 된다.
선택된 도시 정보는 App 컴포넌트에서 관리하고 WeatherBtn 컴포넌트로 전달되는 흐름이다 !
결과 - > 선택한 버튼을 구분할 수 있게 된다.
try...catch 블록을 추가하여 API 호출 중 발생하는 오류를 처리하고, 오류가 발생한 경우 화면에 에러 메시지를 표시해보자 !
에러가 발생하지 않은 경우에는 이전과 같이 기존의 코드를 화면에 표시하기
에러가 발생했을 때 자체적인 에러 화면 대신에 사용자가 정의한 에러 메시지가 화면에 표시되게 하려면 - > 상태를 이용하여 에러 상태를 관리하고, 에러가 발생했을 때 해당 상태를 업데이트하여 화면에 에러 메시지를 표시
-> 에러 상태를 추가하고, API 호출 중에 오류가 발생하면 해당 상태를 업데이트하여 에러 메시지를 표시
에러 상태 추가
const [apiError, setApiError] = useState(null);
try catch문 감싸기 - > 화면 보여주는 부분에서 삼항연산식 활용해서 에러가 있을 땐 메세지가 보이게 없을 땐 기존 컴포넌트 보이게 해주기
결과 - > 일부러 url을 틀리게 넣고 확인해보면 화면에 에러 메세지가 잘 나타난다.
netlify를 통한 배포시 이전 가위바위보 프로젝트와는 달리
빌드에서 오류가 났다.
build command를 CI= npm run build
로 바꿔주고도 계속 오류가 났는데
이유는 api키 숨겨둔 config 파일을 내가 gitignore 파일에 작성해뒀는데
이 파일을 찾지 못해서 빌드가 실패했던 것이었나보다.
config 파일을 숨기지 않고 api키를 선언해서 쓰는걸로 바꾸고 푸시한 뒤 다시
시도하니 배포가 잘 되었다 !
근데 api키를 env ? 라는 거에 숨기고 netlify에서도 작성해두면
api키를 숨길 수 있다는데 이것도 시도해봐야겠다.
위에서 배포 오류로 인해 config 파일에 api키 넣어뒀던걸 다시
api키를 노출시키게 되어
이걸 해결해줄 env 파일을 작성해보자 !
최상위 폴더에 .env 파일 생성
API KEY 입력하기
반드시 REACT_APP이라는 접두사를 사용해주어야 react앱이 인식한다고 함.
.gitignore에 .env 추가하기
API KEY 사용하기
리액트만 쓸 때는 따로 .env 파일을 import해서 사용하지 않아도 된다고 한다.
이후 netlify에서도 동일하게 환경변수를 추가해주어야 함 !
근데 이러고도 배포링크에선 api키를 인식못하길래 리포지토리에서 아무거나 입력하고
푸시를 다시 해보니까 빌드 하면서 업데이트되어 잘 작동되고 있는 걸 확인할 수 있었다 !!
전에 구글맵 활용했던 것도 이런식으로 하면 키를 직접적으로 하드코딩하지 않고 보호할 수 있을 것 같다는 생각이 들었다. 굿굿
참고
https://velog.io/@rkio/React-API-KEY-%EA%B4%80%EB%A6%AC-%EB%B0%A9%EB%B2%95
스타일 다듬으면 큰 사이클 한 바퀴 돌았다 !
1차 (기능 완성 후)
2차
최종
openweathermap api 공식문서
https://openweathermap.org/api
https://velog.io/@realzu/OpenWeather-%EB%82%A0%EC%94%A8-API-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0