Studying/개인 프로젝트

개인 프로젝트 - 날씨 앱 (3) 특정 도시 날씨 (메인페이지구성)

creamymood 2025. 5. 29. 21:09

 

 

개인 프로젝트 - 날씨 앱 (2) 현재 위치의 날씨 받아오기 (메인페이지구성)

개인 프로젝트 - 날씨 앱 (1) 현재 위치 받아오기날씨앱 메인페이지에서 구현 할 내용이, 내가 있는 위치의 날씨 정보 이므로, 현재 위치를 우선 알아야 한다.그 과정에 필요한 내용.날씨 앱 말고

creamymood.tistory.com

앞 게시글에서 이어집니다.

 


앞 게시글에서, 메인페이지에서의 현재 지역 위치 날씨 칸은 구현 완료 했다.

 

 

해당 게시글에서의 구현 목표는

1.(7)~(9)번 특정 나라의 온도 API를 받아온 뒤 렌더링 한다.

2. 예쁘지 않은 css..니까, 더 자주 쳐다보고 싶게 조금은 디자인적 요소를 추가해본다.

3. (11)번의 도시 검색창을 만들어준다.


1. 특정 나라의 온도 API를 받아온 뒤 렌더링

우선, OpenWeather 사이트에서, 특정 도시의 날씨 정보를 받아올 수 있었다.

 

두가지 방법이 있었는데 ❶ 도시 이름을 직접 넣어주기 ❷ 도시 ID 사용하기 (파일을 제공한다.).

 

우선 ❶번 방법으로, 테스트를 해본 뒤, 추후 검색 기능에서 ❷ 사용 해봐야겠다.


 

 

❶ 도시 이름으로 요청하기

 

위의 사진을 참고해서, api 호출을 하여 화면에 렌더링 해본다.

도쿄로 도시 이름에 넣어줘서 호출하니, 아래처럼 떴다.

 

 

코드는 다음과 같다.

더보기
 //도시 이름으로, 날씨 호출하기 함수
 const [tokyoWeather, setTokyoWeather] = useState(null)
 
 
    const cityWeather = async() => {
        const key = import.meta.env.VITE_WEATHER_KEY
        const url = `https://api.openweathermap.org/data/2.5/weather?q=Tokyo&appid=${key}`

        try{
          const response = await fetch(url)
          const data = await response.json()
          console.log(data)
          setTokyoWeather(data)

        }
        catch (error){
            console.error('도시 날씨 데이터를 불러오지 못했습니다:', error);
        }
    }

    //
    useEffect(() => {
      cityWeather()
    }, []);

 

return (
 	{tokyoWeather ? (
                <p>{`도쿄의 현재 날씨는 ${tokyoWeather.weather[0].description}`}</p>
            ) : (
                <p>도쿄 날씨 정보를 불러오는 중...</p>
                
            )}
)

 

 


 

2. 세 나라의 온도 API를 받아온 뒤 렌더링 (도시 공통 컴포넌트 방법)

나의 UI 목표는 3개의 나라를 구성 보여주는 것 이었으니까, 3개 다 이렇게 하나 하나 만들 수 있긴 하지만..

다른 블로그에서 배열로써 관리하는 방법을 참고 해서 한번 해보겠다.

원하는 나라 3개를 배열로써 만들어주고 map으로 돌려보겠음!

 

→ 해당 방식은 어떻게 우여곡절이 많아서 조교님이 추천해주신 방식으로 해보기로 했다

 

도시 공통 컴포넌트를 만들고, 배열의 도시를 프롭스(?매개변수)로 내려줘서, 배열을 맵돌리면

배열에 도시마다 도시 컴포넌트가 생기고 (예를 들어 : <City/> <City/> <City/>), 공통 컴포넌트 그 쪽에서 API를 부르는 방식으로!

 

2-1. 공통 도시 컴포넌트 만들기

도시 배열은 

    const cities = ['Tokyo', 'Busan', 'Toronto']

 

이렇게 만들어 두었고, 

내부에 도시정보가 들어오면, API를 호출 할 수 있게 호출 함수 그대로 사용

function Cities ({city}) {
    const [cityWeatherData, setCityWeatherData] = useState(null)

 //도시 이름으로, 날씨 호출하기 함수
    const cityWeather = async() => {
        const key = import.meta.env.VITE_WEATHER_KEY
        const url = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${key}`

        try{
          const response = await fetch(url)
          const data = await response.json()
          console.log(data)
          setCityWeatherData(data)

        }
        catch (error){
            console.error('도시 날씨 데이터를 불러오지 못했습니다:', error);
        }
    }

    
    useEffect(() => {
      cityWeather()
    }, []);


    return(
        
        <div>
            {cityWeatherData? (
            <>
            <p>{`현재 위치는 : ${cityWeatherData.name}`}</p>
            <p>{`현재 날씨는 : ${cityWeatherData.weather[0].description}`}</p> 
            </>
            ) : (
                <p>날씨 정보를 불러오는 중...</p>
                
            )}
           
        </div>
    )

}

export default Cities

 

2-2. Home.jsx에서 배열을 map으로 돌리기

그리고, home.jsx에서는

           	{cities.map((city,index)=>(
                <Cities key={index} city={city}/>
            ))}

이렇게 배열을 맵으로 돌려서 <Cities />  <Cities />  <Cities /> 이렇게 세개가 뜰겁니다:)

 

간단했어!

 

이렇게 되었네요 !

 

 


디자인 적 요소를 조금은 가꿔야 할 것 같아서, 한번 다듬어 본다.


3. 특정 도시 날씨를 검색할 수 있는 검색창 만들기

도시 검색을 할 수 있는 검색창도 만들어줍니다.

 

3-1 Input창을 만들어서 버튼을 누르면 -> 디테일 페이지로 이동 하게 한다.

 

  ❶ return 안에, 이렇게 넣어줬고

<input placeholder="어느 도시가 궁금하세요?"/>

 

❷ 버튼을 달아서, detail로 넘어가게 했다.

그런데 버튼을 버튼 대신에, 리액트 라이브러리 중 하나인 아이콘을 사용해서 활용해봤다.

 

사용법은 다음과 같고,

 <input placeholder="어느 도시가 궁금하세요?"/> <Link to ="/detail"><IoIosSearch /></Link>

 

난 링크를 달아줬다!

 

*추가 수정 + 다음 단계에서 수정 했습니다.

더보기

해당 부분은, 인풋창에 값으로 버튼을 눌렀을 때, 그 값으로 url을 이동하려고 하는거기 때문에..

Link가 아닌 Onclick으로 바꿔줬다.

 

설명은 아래와 같음

상황 정리

지금 하고 싶은 건:

사용자가 입력창에 도시 이름을 입력하면,
그 값(/detail/입력값)으로 이동하고 싶다.
이동은 버튼(아이콘)을 눌렀을 때 일어나야 한다.

 Link를 쓰지 않고 navigate를 쓰는 이유

<Link>는 고정된 경로로 이동할 때 유용하다.

  • 예: <Link to="/about">About</Link>
  • 근데 사용자가 입력한 값에 따라 URL이 달라질 경우, <Link>의 to 속성에 동적으로 값 넣기 어렵거나 복잡해짐.
  • 그래서 onClick으로 navigate()를 호출해서 동적으로 이동시키는 게 자연스러움.

코드 확인

const inputChange = (e) => {
  setSearchInputValue(e.target.value)
}

const buttonChange = () => {
  navigate(`/detail/${searchInputValue}`)
}

이건 아주 자연스러운 React 방식이야.
searchInputValue는 상태로 관리되고 있고,
navigate()는 react-router-dom의 훅이니까 페이지 전환에 딱 맞음.

추가 팁

혹시 사용자가 엔터를 눌러도 이동되게 하고 싶다면:

<input
  placeholder="도시 이름을 영어로 입력해보세요!"
  onChange={inputChange}
  onKeyDown={(e) => {
    if (e.key === 'Enter') buttonChange()
  }}
/>

 

 

그런데 링크를 다니까 문제점...

하이퍼링크 처럼 아이콘의 색이 약간 바뀌었는데, 이걸 좀 고칠 수 있는 법은?

더보기

<Link>는 기본적으로 브라우저에서 하이퍼링크처럼 동작해서 파란색 텍스트 + 밑줄 스타일이 적용될 수 있다.

특히 <a> 태그로 렌더링되기 때문에 그렇다

아이콘에 그런 스타일이 적용되면 어색하니까, 스타일을 직접 없애주는 게 좋다.


 해결법: Link에 스타일 초기화 주기

예를 들어:

import { Link } from 'react-router-dom';
import { Search } from 'lucide-react';

function MyComponent() {
  return (
    <Link to="/detail" style={{ textDecoration: 'none', color: 'inherit' }}>
      <Search className="cursor-pointer" />
    </Link>
  );
}

 

💡 textDecoration: 'none' → 밑줄 제거
💡 color: 'inherit' → 부모 텍스트 색상 그대로 사용 (아이콘에도 영향 있음)


Tailwind CSS 쓸 경우

만약 Tailwind CSS를 쓰고 있다면 이렇게 쓸 수 있음:

<Link to="/detail" className="no-underline text-inherit">
  <Search className="cursor-pointer" />
</Link>

만약 아이콘 색도 바뀌었다면?

<Link> 안의 아이콘이 파랗게 되었다면, 아마 부모 스타일 영향을 받는 것.
이럴 땐 아이콘에 color 스타일을 직접 지정해줘도 됌

<Search style={{ color: 'black' }} />

혹은 Tailwind에서:

<Search className="text-black" />

 

그러면 이렇게 검색 창도 만들어졌다.

 

 


다음 게시글에선,

1. 검색 창에서 특정 도시를 검색 한 뒤 버튼을 누르면 해당 값으로 api를 호출하고 -> 디테일을 표시해주는 작업을

2. 로딩 창을 구현

3. 추천 옷, 노래 컴포넌트 만들기

4. 한국어 지원으로 변경하기