우선 이전 포스팅에서 사용자가 위치 동의를 거부하거나, 위도와 경도를 제대로 받아오지 못했을 때를 대비해서 recoil 기본값으로 서울 강남 좌표를 설정했었는데요.
// 개선 전 코드
import { atom } from "recoil";
export const locationState = atom({
key: "locationState",
default: { lat: 37.5145, lon: 127.0495 },
});
이렇게 하니 처음 페이지가 열릴 때 무조건 서울 강남 정보가 렌더링됐다가, 나중에 현재 위치로 바뀌는 상황이 있었습니다.
위도와 경도를 성공적으로 받아왔을 때도, 서울 강남이 먼저 표시되고 나서야 현재 위치로 변경되는 부분이 조금 부자연스러웠습니다.
그래서 이 부분을 위도와 경도를 먼저 불러온 후, 실패할 경우에만 서울 강남을 렌더링하도록 수정했습니다.
이 부분에 대해 구체적으로 어떻게 개선했는지는 포스팅에서 설명드릴 예정입니다.
그리고 오늘의 주요 정보를 보여주는 Highlights 컴포넌트도 추가했는데, 이 컴포넌트는 HTML 과 CSS의 비중이 더 크기 때문에, 특별히 다룰 내용은 많지 않습니다.
locationState default값 변경
위치 정보를 불러온 후에만 값이 설정되도록 하기 위해 초기 lat, lon 값을 null로 설정하였습니다.
import { atom } from "recoil";
export const locationState = atom<{ lat: number | null; lon: number | null }>({
key: "locationState",
default: { lat: null, lon: null },
});
Header 컴포넌트 수정
기존에는 Header 컴포넌트에서 Geolocation API를 통해 위도와 경도 정보를 성공적으로 받아오면 이를 locationState에 업데이트하는 역할만 했습니다.
이 기능은 그대로 유지하면서, 이제 위치 정보 불러오기에 실패했을 경우에는 기본값(defaultLocation)을 사용해 locationState를 업데이트하는 로직으로 변경했습니다.
const defaultLocation = { lat: 37.5145, lon: 127.0495 };
const getLocation = (showAlert = false) => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude } = position.coords;
setLocation({ lat: latitude, lon: longitude }); // 위치 정보를 받아오면 그대로 업데이트
},
(error) => {
if (error.code === error.PERMISSION_DENIED && showAlert) {
alert("위치 정보 접근이 차단되었습니다. 브라우저 설정을 확인해주세요.");
}
console.error("위치 정보를 가져오는 데 실패했습니다.", error);
setLocation(defaultLocation); // 실패 시 기본값 설정
}
);
} else {
console.error("이 브라우저는 Geolocation을 지원하지 않습니다.");
setLocation(defaultLocation); // Geolocation을 지원하지 않으면 기본값 설정
}
};
MainContent컴포넌트 수정
기존의 useQueries에 location.lat와 location.lon이 모두 있을 때만 쿼리가 활성화되도록 enabled 옵션을 추가했습니다.
enabled: !!location.lat && !!location.lon,
이렇게 하면 위도와 경도 값이 없을 경우 쿼리가 실행되지 않게 됩니다.
그러나 TypeScript에서는 여전히 location.lat와 location.lon이 null일 수 있다고 경고를 띄우기 때문에, 이를 해결하기 위해 queryFn 내부에서도 조건문을 추가했습니다.
queryFn: () =>
location.lat && location.lon
? WeatherApi.getCurrentWeather(location.lat, location.lon)
: Promise.reject(new Error("위치 정보 없음")),
const [location] = useRecoilState(locationState);
const results = useQueries({
queries: [
{
queryKey: ["currentWeather", location.lat, location.lon],
queryFn: () =>
location.lat && location.lon
? WeatherApi.getCurrentWeather(location.lat, location.lon)
: Promise.reject(new Error("위치 정보 없음")),
staleTime: 60000,
gcTime: 1000 * 60 * 10,
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
enabled: !!location.lat && !!location.lon,
},
{/* 생략 */}
],
});
Weather Api
이번에 Highlights 컴포넌트를 구현하면서, 대기 오염 지수도 함께 표시하려고 Weather API에 새로운 메서드를 추가했습니다.
이 메서드는 위도와 경도를 받아서 대기 오염 데이터를 가져오는 역할을 합니다.
async getAirPollution(lat: number, lon: number) {
const response = await this.httpClient.get("air_pollution", {
params: {
lat: lat,
lon: lon,
}
});
return response.data;
}
Highlight 컴포넌트
Highlights 컴포넌트에서는 이 API로 받아온 대기 오염 데이터를 활용해 미세먼지, 이산화황(SO₂), 이산화질소(NO₂), 오존(O₃) 등 주요 대기 오염 정보를 표시합니다.
기존의 날씨 정보도 함께 구성하여 오늘의 주요 정보 섹션을 만들었습니다.
interface HighlightsProps {
airPollution: IAirPollution;
currentWeather: ICurrentWeather;
}
export default function Highlights({airPollution, currentWeather}: HighlightsProps) {
const {
main: { aqi },
components: { pm2_5, so2, no2, o3 },
} = airPollution.list[0];
const {
main: { humidity, pressure, sea_level, feels_like },
sys: { sunrise, sunset },
visibility,
} = currentWeather;
console.log(pm2_5,so2, no2, o3 );
return (
<section className={styles.section}>
<Card size="large">
<h2 className={styles.title}>오늘의 주요 정보</h2>
<div className={styles.grid}>
<Card size="small" className={`${styles["highlight-card"]} ${styles["one"]}`}>
<h3 className={styles.heading}>대기 오염 지수</h3>
<div className={styles.wrapper}>
<span className={styles.icon}>
<MdAir />
</span>
<ul className={styles.list}>
<li className={styles.item}>
<p className={styles.label}>미세먼지</p>
<p className={styles.title}>{pm2_5.toFixed(2)}</p>
</li>
<li className={styles.item}>
<p className={styles.label}>이산화황</p>
<p className={styles.title}>{so2.toFixed(2)}</p>
</li>
<li className={styles.item}>
<p className={styles.label}>이산화질소</p>
<p className={styles.title}>{no2.toFixed(2)}</p>
</li>
<li className={styles.item}>
<p className={styles.label}>오존</p>
<p className={styles.title}>{o3.toFixed(2)}</p>
</li>
</ul>
</div>
</Card>
<Card size="small" className={`${styles["highlight-card"]} ${styles["two"]}`}>
<h3 className={styles.heading}>일출 & 일몰</h3>
<div className={styles.list}>
<div className={styles.item}>
<span className={styles.icon}>
<FiSunrise />
</span>
<div>
<p className={styles.label}>일출</p>
<p className={styles.title}>{getTime(sunrise)}</p>
</div>
</div>
<div className={styles.item}>
<span className={styles.icon}>
<FiSunset />
</span>
<div>
<p className={styles.label}>일몰</p>
<p className={styles.title}>{getTime(sunset)}</p>
</div>
</div>
</div>
</Card>
{/* 나머지 카드 생략 */}
</div>
</Card>
</section>
);
}
일출 및 일몰 시간 처리
날씨 정보를 표시하는 Highlights 컴포넌트에서, 일출과 일몰 시간을 표시하기 위해 시간을 변환하는 getTime 함수를 추가했습니다.
이전 포스팅에서 작성한 getDate 함수와 원리는 비슷합니다.
timeUnix를 1000으로 나눠 밀리초 단위로 변환한 후, Date 객체를 생성하여 원하는 형태의 문자열로 출력하는 방식입니다
export const getTime = function (timeUnix: number): string {
const date = new Date(timeUnix * 1000);
const hours = date.getHours();
const minutes = date.getMinutes();
const period = hours >= 12 ? "PM" : "AM";
const formattedHours = hours % 12 === 0 ? 12 : hours % 12;
return `${formattedHours}:${minutes} ${period}`;
};
'React > Weather' 카테고리의 다른 글
OpenWeatherMap API로 날씨 Web 만들기 - 마무리 (0) | 2024.09.13 |
---|---|
OpenWeatherMap API로 날씨 Web 만들기 - 3 (1) | 2024.09.11 |
OpenWeatherMap API로 날씨 Web 만들기 - 1 (0) | 2024.09.09 |