[React] DAY 19 - OpenWeatherMap API 활용

URECA - TIL/React 라이브러리
profile jugang , 2025

💡 무엇을 배울까

1. React 개발을 위한 VS Code 설정
2. 파일 구조
3. 프로젝트 전체 동작 흐름
4. OpenWeatherMap API 가져오는 방법
5. WeatherPage에서 화면에 날씨 정보 표시하기

 


📖 수업 내용

1️⃣ React 개발을 위한 VS Code 설정

프로젝트를 시작하기 앞서 먼저 VS Code를 설정하자.

VSCode 설정 참고

 

React 개발을 위한 VS Code 설정

⚙ VS Code 설정1️⃣ VS Code 설치 Visual Studio Code - Code Editing. RedefinedVisual Studio Code redefines AI-powered coding with GitHub Copilot for building and debugging modern web and cloud applications. Visual Studio Code is free and available o

jugang.tistory.com

 

필요한 패키지까지 설치하면 (아래 코드 참고)

# 라우팅
npm install react-router-dom

# API 호출
npm install axios

npm install @tanstack/react-query

npm install @tanstack/react-query-devtools

 

초기 설정 완료!!

 

 

 


2️⃣ 파일 구조

/src                  
  ├── assets                 
  │   ├── index.css ------------------> # 전역 CSS 스타일 파일
  │   └── react.svg          
  │
  ├── layout -------------------------> # 공통 레이아웃 컴포넌트 모음
  │   ├── MainLayout.jsx -------------> # 전체 페이지 공통 레이아웃 컴포넌트
  │   ├── MainLayout.module.css ------> # MainLayout 전용 모듈 CSS
  │   ├── MenuList.jsx ---------------> # 메뉴 리스트 컴포넌트
  │   └── MenuList.module.css --------> # MenuList 전용 모듈 CSS
  │
  ├── router -------------------------> # 라우터 설정 폴더
  │   └── index.jsx ------------------> # React Router 설정 파일
  │
  ├── weather ------------------------> # 날씨 관련 기능 담당 폴더
  │   ├── Button.jsx -----------------> # 버튼 컴포넌트
  │   ├── Button.module.css ----------> # Button 전용 모듈 CSS
  │   ├── useWeatherApi.js -----------> # 날씨 API 호출용 커스텀 훅 (API 통신)
  │   ├── WeatherPage.jsx ------------> # 날씨 페이지 컴포넌트
  │   └── WeatherPage.module.css -----> # WeatherPage 전용 모듈 CSS
  │
  └── main.jsx -----------------------> # React 앱 진입점 (루트 파일)

/.env.local --------------------------> # 환경변수 파일 (로컬 전용)

 

 

 


3️⃣ 프로젝트 전체 동작 흐름

1. 진입: `main.jsx`

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import '@/assets/index.css'
import { RouterProvider } from 'react-router-dom'
import { router } from './router'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <RouterProvider router={router} />
  </StrictMode>
)
  • React 앱을 root에 마운트한다.
  • `RouterProvider`를 통해 라우팅 시스템을 설정한다.

 

 


2. 라우팅 설정: router > `index.jsx`

import { createBrowserRouter } from 'react-router-dom'
import MainLayout from '../layout/MainLayout'
import WeatherPage from '../weather/WeatherPage'

export const router = createBrowserRouter([
  {
    path: '/',
    element: <MainLayout />,
    errorElement: <div>에러</div>,
    children: [
      {
        index: true,
        element: <WeatherPage />,
      },
    ],
  },
])
  • `/` 경로에 `MainLayout.jsx`를 보여준다.
  • `MainLayout` 내부에서 `WeatherPage.jsx`를 렌더링한다.

 

 


3. 공통 레이아웃: layout > `MainLayout.jsx`

import React from 'react'
import MenuList from './MenuList'
import { Outlet } from 'react-router-dom'
import css from './MainLayout.module.css'

const MainLayout = () => {
  return (
    <div className={css.layout}>
      <MenuList />
      <Outlet />
    </div>
  )
}

export default MainLayout
  • 좌측 메뉴와 메인 콘텐츠 영역을 분리해서 구성
  • `<MenuList />`: 사이드 메뉴 (네비게이션)
  • `<Outlet />`: 현재 경로에 맞는 하위 컴포넌트 (WeatherPage) 렌더링

 

 


4. 메뉴 리스트: layout > `MenuList.jsx`

import React from 'react'
import { NavLink } from 'react-router-dom'
import css from './MenuList.module.css'

const MenuList = () => {
  return (
    <>
      <ul>
        <li>
          <NavLink to={'/'} className={isActive => (isActive ? `${css.active}` : '')}>
            날씨 API 활용
          </NavLink>
        </li>
      </ul>
    </>
  )
}

export default MenuList
  • 사이드 메뉴에 `NavLink`를 사용하여 `/` 경로로 이동하는 메뉴 제공
  • 현재는 "날씨 API" 한 개만 등록

 


5.  날씨 페이지: weather > `WeatherPage.jsx`

import React, { useEffect, useState } from 'react'
import css from './WeatherPage.module.css'
import { getCountryData, getCurrentData } from './useWeatherApi'
import Button from './Button'
import { useSearchParams } from 'react-router-dom'

const WeatherPage = () => {
  const [searchParams, setSearchParams] = useSearchParams()
  const city = searchParams.get('city')

  const [weatherData, setWeatherData] = useState(null)

  const cityButtons = [
    { id: 'current', label: '현재 위치' },
    { id: 'seoul', label: '서울' },
    { id: 'hongkong', label: '홍콩' },
    { id: 'new york', label: '뉴욕' },
    { id: 'paris', label: '파리' },
  ]

  useEffect(() => {
    const fetchWeatherData = async () => {
      try {
        let data
        if (city) {
          data = await getCountryData(city)
        } else {
          data = await getCurrentData()
        }
        setWeatherData(data)
      } catch (err) {
        console.log('날씨 데이터 가져오기 실패----', err)
      }
    }
    fetchWeatherData()
  }, [city])

  const handleChangeCity = city => {
    if (city === 'current') {
      setSearchParams({})
    } else {
      setSearchParams({ city })
    }
  }

  return (
    <main className={css.main}>
      <section className={css.weatherCard}>
        <h2 className={css.title}>날씨</h2>
        <p className={css.location}>
          {weatherData?.sys.country} / {weatherData?.name}
        </p>
        <div className={css.temperature}>
          <p>{weatherData?.main.temp.toFixed(1)}&#8451;</p>
          <img
            src={`https://openweathermap.org/img/wn/${weatherData?.weather[0].icon}@2x.png`}
            alt="weather-icon"
            className={css.weatherIcon}
          />
        </div>
      </section>

      <div className={css.btnList}>
        {cityButtons.map(button => (
          <Button
            key={button.id}
            city={button.id}
            label={button.label}
            onClick={handleChangeCity}
          />
        ))}
      </div>
    </main>
  )
}

export default WeatherPage
  • 현재 위치 또는 도시 이름으로 날씨 데이터를 가져온다.
  • 버튼을 클릭하면 `useSearchParams`로 URL을 바꾸고, 그에 따라 다시 날씨 데이터를 불러온다.

📌 핵심 동작:

  1. `useEffect`로 URL 변경 감지
  2. `getCurrentData` (현재 위치) 또는 `getCountryData` (도시명)로 API 요청
  3. 받아온 데이터 화면에 표시

 

 


6. 버튼: weather > `Button.jsx`

import React from 'react'
import css from './Button.module.css'

const Button = ({ city, label, onClick }) => {
  return (
    <button className={css.button} onClick={() => onClick(city)}>
      {label}
    </button>
  )
}

export default Button

 

 


7. API 함수: weather > `useWeatherApi.js`

import axios from 'axios'

const API_KEY = import.meta.env.VITE_WEATHER_API_KEY
const BASE_URL = 'https://api.openweathermap.org/data/2.5/weather'

// 좌표로 날씨 정보 가져오기
export const getWeatherByCurrentLocation = async (lat, lon) => {
  try {
    const res = await axios.get(
      `${BASE_URL}?lat=${lat}&lon=${lon}&appid=${API_KEY}&lang=kr&units=metric`
    )

    return res.data
  } catch (err) {
    console.log('좌표로 날씨 정보 가져오기 실패----', err)
  }
}

// 현재 위치 날씨 정보 가져오기
// 1. 현재 좌표 가져오기
// 2. getWeatherByCurrentLocation(위도, 경도)
export const getCurrentData = async () => {
  return new Promise((resolve, reject) => {
    navigator.geolocation.getCurrentPosition(
      async position => {
        try {
          const { latitude, longitude } = position.coords
          const res = await getWeatherByCurrentLocation(latitude, longitude)

          resolve(res)
        } catch (err) {
          console.log('좌표로 날씨 정보 가져오기 실패', err)
          reject(err)
        }
      },
      err => {
        console.log('좌표 가져오기 실패', err)
        reject(err)
      }
    )
  })
}

// 도시명으로 날씨 정보 가져오기
export const getCountryData = async city => {
  try {
    const res = await axios.get(`${BASE_URL}?q=${city}&appid=${API_KEY}&lang=kr&units=metric`)

    return res.data
  } catch (err) {
    console.log('좌표로 날씨 정보 가져오기 실패----', err)
  }
}
  • 날씨 데이터 가져오는 API 통신 모듈
  • `getCurrentData()` : 현재 위치 기준 날씨
  • `getCountryData(city)` : 도시명 기준 날씨

 

 

📊 전체 연결 흐름

index.html
  ↓
main.jsx (React 앱 시작)
  ↓
RouterProvider
  ↓
index.jsx (라우팅 설정)
  ↓
MainLayout.jsx (레이아웃)
  ├─ MenuList.jsx (사이드 메뉴)
  └─ Outlet → WeatherPage.jsx (메인 콘텐츠)
        ├─ useWeatherApi.js로 API 호출
        └─ Button.jsx 클릭 → 도시 변경

 

 

 

 


🛠 OpenWeatherMap API 가져오는 방법

1. OpenWeatherMap API 가입 및 API Key 발급

 

-> 먼저 OpenWeatherMap 사이트에 접속한다.

 

Current weather and forecast - OpenWeatherMap

Access current weather data for any location on Earth including over 200,000 cities! The data is frequently updated based on the global and local weather models, satellites, radars and a vast network of weather stations. how to obtain APIs (subscriptions w

openweathermap.org

 

-> 그 다음 회원가입 후 이메일 인증을 한 뒤 로그인한다.

 

-> 그럼 내 계정에 My API keys 메뉴에서 새 API 키를 발급받을 수 있다.

 

-> 발급받은 API 키를 `.env.local` 파일에 저장하자.

VITE_WEATHER_API_KEY=발급받은_API_KEY

※ 주의 :

  • `.env.local` 파일은 GitHub에 업로드하지 말고 로컬에서만 사용해야 한다.
  • `VITE_`로 시작해야 Vite 환경에서 환경변수를 읽을 수 있다.

 

🤔 `.env` 파일이 궁금하다면 (더보기 참고)

더보기

`.env` 파일은

프로젝트 안에서 사용하는 환경 변수(Environment Variables)를 저장해놓는 파일
  • 비밀번호, API 키, 서버 주소처럼 외부에 노출되면 안되는 값을 관리할 때 사용
  • GitHub 같은 공개 저장소에는 절대 올리지 않고 로컬(내 컴퓨터)에만 저장
  • 코드에 민감한 정보를 하드코딩(직접 적는 것)하지 않고,
    별도의 파일로 관리해서 보안적으로 안전

 

Vite, CRA(Create React App), Next.js 같은 현대 프론트엔드 프레임워크들은

환경에 따라 다른 `.env` 파일을 읽을 수 있도록 규칙을 정해놨다.

 

파일명 사용 용도
`.env` 모든 환경(개발, 운영)에 공통으로 적용할 기본 설정
`.env.local` 내 컴퓨터에서만 적용되는 로컬 전용 설정 (공유 X)
`.env.development` 개발 환경에서만 적용할 설정
`.env.production` 배포(운영) 환경에서만 적용할 설정
`.env.test` 테스트 환경에서만 적용할 설정

 

❗ 주의할 점 

`.env.local`은 무조건 `.gitignore`에 추가해야 한다.

 


2. 날씨 API 요청

 

API 요청을 쉽게 하기 위해 Axios를 사용한다. (이미 처음에 설치했기 때문에 설명은 생략)

 

 

📌 weather > `useWeatherApi.js`

# 기본 설정

import axios from 'axios'

const API_KEY = import.meta.env.VITE_WEATHER_API_KEY
const BASE_URL = 'https://api.openweathermap.org/data/2.5/weather'

`API_KEY`는 `.env.local`에서 가져오고,

`BASE_URL`은 OpenWeatherMap의 날씨 데이터 API 기본 URL이다.

 

 

# 현재 위치로 날씨 데이터 가져오기

export const getCurrentData = async () => {
  return new Promise((resolve, reject) => {
    navigator.geolocation.getCurrentPosition(
      async position => {
        try {
          const { latitude, longitude } = position.coords
          const res = await getWeatherByCurrentLocation(latitude, longitude)
          resolve(res)
        } catch (err) {
          reject(err)
        }
      },
      err => {
        reject(err)
      }
    )
  })
}
  • 브라우저의 `navigator.geolocation` API를 사용해 현재 위치(위도, 경도)를 얻는다.
  • 얻은 좌푤르 가지고 `getWeatherByCurrentLocation` 함수를 호출해서 날씨 정보를 가져온다.

 

 

# 위도와 경도로 날씨 데이터 가져오기

export const getWeatherByCurrentLocation = async (lat, lon) => {
  try {
    const res = await axios.get(
      `${BASE_URL}?lat=${lat}&lon=${lon}&appid=${API_KEY}&lang=kr&units=metric`
    )
    return res.data
  } catch (err) {
    console.log('좌표로 날씨 정보 가져오기 실패', err)
  }
}
  • 위도(`lat`)와 경도(`lon`)를 이용해서 OpenWeatherMap API에 요청한다.
  • 한국어(`lang=kr`)로 데이터를 요청하고, 온도는 섭씨(`units=metric`)로 가져온다.

 

 

# 도시명으로 날씨 데이터 가져오기

export const getCountryData = async city => {
  try {
    const res = await axios.get(
      `${BASE_URL}?q=${city}&appid=${API_KEY}&lang=kr&units=metric`
    )
    return res.data
  } catch (err) {
    console.log('도시명으로 날씨 정보 가져오기 실패', err)
  }
}
  • 도시 이름(`city`)을 검색어로 날씨 데이터를 요청한다.

 

 

 


🖥 WeatherPage에서 화면에 날씨 정보 표시하기

 

📌 화면 동작 흐름

  1. URL에 `city` 값이 있으면 -> 해당 도시 날씨 요청 (`getCountryData`)
  2. URL에 `city` 값이 없으면 -> 현재 위치 날씨 요청 (`getCurrentData`)
  3. 날씨 데이터가 받아와지면 -> 화면에 다음 정보 표시
    • 나라/도시명 (`sys.country / name`)
    • 현재 온도 (`main.temp`)
    • 날씨 아이콘 (`weather[0].icon`)

 

 

🧾 주요 코드 설명

useEffect(() => {
  const fetchWeatherData = async () => {
    try {
      let data
      if (city) {
        data = await getCountryData(city)
      } else {
        data = await getCurrentData()
      }
      setWeatherData(data)
    } catch (err) {
      console.log('날씨 데이터 가져오기 실패----', err)
    }
  }
  fetchWeatherData()
}, [city])

-> useEffect로 날씨 데이터 가져오기

 

여기서 날씨 API로부터 받아온 `data`를 콘솔에 출력해보면 다음과 같은 구조를 볼 수 있다.

console.log('날씨 데이터: ', data)

주요 필드 설명
coord 위치 정보 (위도, 경도)
weather 날씨 상태 (맑음, 흐림 등)
base 내부 데이터베이스 종류
main 온도, 습도, 기압 등 주요 날씨 데이터
name 도시 이름
sys 국가 코드, 일출/일몰 시간 등
wind 바람 속도, 바람 방향
visibility 가시거리 (미터 단위)

 

여기서

`.sys.country`(국가 코드), `.name`(도시명), `.main.temp`(현재 온도), `.weather[0].icon`(날씨 아이콘 코드)

를 사용한다.

 

 

    <main className={css.main}>
      <section className={css.weatherCard}>
        <h2 className={css.title}>날씨</h2>
        <p className={css.location}>
          {weatherData?.sys.country} / {weatherData?.name}
        </p>
        <div className={css.temperature}>
          <p>{weatherData?.main.temp}&#8451;</p>
          <img
            src={`https://openweathermap.org/img/wn/${weatherData?.weather[0].icon}@2x.png`}
            alt="weather-icon"
            className={css.weatherIcon}
          />
        </div>
      </section>
    </main>

-> 받아온 데이터를 화면에 표시한다.

 

🖐🏻 여기서 잠깐

🤔 왜 `?`를 사용해야 할까? (Optional Chaining)

 

`weatherData?.sys.country` 처럼 `?`를 사용하는 이유는

아직 `weatherData`가 완전히 로딩되지 않았을 때를 대비해서
에러 없이 안전하게 접근하기 위해

 

💡 상황을 쉽게 이해해보자.

 

처음 화면이 렌더링될 때를 생각해보면 :

const [weatherData, setWeatherData] = useState(null)
  • 처음에는 weatherData가 `null`임 (데이터 없음)
  • 그런데 코드에서 만약 그냥 `weatherData.sys.country`처럼 바로 접근하면?
    ❗ 에러 발생
TypeError: Cannot read properties of null (reading 'sys')

-> 해석 : "null"이라는 값에 대해 'sys' 속성을 읽으려고 해서 에러가 발생했다"

이렇게 접근하면,

  • null은 객체가 아니야
  • null에는 `sys`라는 속성이 없어
  • 그래서 JavaScript가 죽어버리는 거야 ("Cannot read properties of null")

 

그래서 해결 방법은 바로

Optional Chaining (`?`)을 쓰는 것!

{weatherData?.sys.country}
  • weatherData가 null이면 아예 평가를 멈추고(undefined로 처리) 넘어간다.
  • weatherData가 있으면 정상적으로 `.sys.country` 읽어온다.

 

결론적으로,
항상 데이터가 확실히 존재하는지 모를 때는 `?.`로 안전하게 접근하는 습관을 들이자!

 

 

<div className={css.btnList}>
  {cityButtons.map(button => (
    <Button
      key={button.id}
      city={button.id}
      label={button.label}
      onClick={handleChangeCity}
    />
  ))}
</div>

-> 버튼으로 도시 선택하기

  • `cityButtons` 배열을 map으로 돌면서 Button 컴포넌트를 여러 개 만든다.
  • 버튼을 클릭하면 `handleChangeCity`가 호출되어 URL `city` 파라미터를 변경한다.
  • 이 변경을 감지한 `useEffect`가 다시 API를 호출해서 데이터를 업데이트한다.

 

 

const Button = ({ city, label, onClick }) => {
  return (
    <button className={css.button} onClick={() => onClick(city)}>
      {label}
    </button>
  )
}

-> Button 컴포넌트

  • `label`로 버튼 텍스트를 표시
  • 클릭하면 `onClick` 함수를 호출해서 선택한 도시 정보를 전달한다.

 

 

 

✨ 완성~!

 

 

 


🔍 회고

더보기

실습 시작 전에 강사님께서 전 시간에 과제로 내준 용돈기입장 프젝 중

잘했던 사람들을 소개해주셨다.

보면서 난 정말 주어진 대로만 하고 창의적인 생각은 안한다고 느꼈다.

어릴 때부터 뭔갈 창작하고 아이디어 도출하는게 어려웠다.

다양한 방법으로 주어진 예시와 다르게 표현하거나 여러 노력의 흔적을 봤을 때

많은 깨달음이 있었다.

한참 부족한 지식이지만 꾸준히 노력하자...꾸준히.

'URECA - TIL > React 라이브러리' 카테고리의 다른 글

[React] DAY 12 - 쇼핑몰 프로젝트 (4)  (0) 2025.04.21
[React] DAY 11 - 쇼핑몰 프로젝트 (3)  (0) 2025.04.19
[React] DAY 10 - 쇼핑몰 프로젝트 (2)  (2) 2025.04.15
[React] DAY 9 - 쇼핑몰 프로젝트 (1)  (0) 2025.04.15
[React] DAY 8 - React Router  (0) 2025.04.11
'URECA - TIL/React 라이브러리' 카테고리의 다른 글
  • [React] DAY 12 - 쇼핑몰 프로젝트 (4)
  • [React] DAY 11 - 쇼핑몰 프로젝트 (3)
  • [React] DAY 10 - 쇼핑몰 프로젝트 (2)
  • [React] DAY 9 - 쇼핑몰 프로젝트 (1)
Juganggang.dev
Juganggang.dev
Coding. Learning. Growing.
  • Juganggang.dev
    Juganggang.log
    Juganggang.dev
  • GitHub
  • 전체
    오늘
    어제
    • All Categories (21)
      • Frontend (5)
        • React 라이브러리 (3)
      • URECA (5)
        • EXAM (4)
        • Project (1)
      • URECA - TIL (10)
        • React 라이브러리 (10)
      • etc. (1)
        • Customize Tistory (1)
  • 태그

    LOADER
    프로젝트
    skeletonui
    Tanstack Query
    초기설정
    유레카
    LG 유플러스
    쇼핑몰
    axios
    figma
    CX
    부트캠프
    스피너
    React Query
    스켈레톤UI
    YouTube
    Util
    미니프로젝트
    렌더링
    Backend
    웹시큐리티
    useState
    react
    유레카부트캠프
    프록시
    ureca
    json-server
    Spinner
    유레카 부트캠프
    algorithm
  • hELLO· Designed By정상우.v4.10.3
Juganggang.dev
[React] DAY 19 - OpenWeatherMap API 활용
상단으로

티스토리툴바