Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[4주차 기본/심화과제] ☀️승경씨의 날씨 예보☀️ #9

Open
wants to merge 39 commits into
base: main
Choose a base branch
from

Conversation

pinktopaz
Copy link
Contributor

@pinktopaz pinktopaz commented May 12, 2023

✨ 구현 기능 명세

✅ 기상 카드

  1. 날씨에 따른 이미지
  2. 온도
  3. 체감온도
  4. 최저,최고
  5. 구름 %

$\rightarrow$ api를 통해 get 해온 데이터를 렌더링하여 보여주었습니다.

✅ 일간 / 주간 + 지역(영어)검색

  1. 날씨 검색 타입
    1. 오늘 → 하루
    2. 주간 → 5일 예보
// 📍 typescript를 사용하여 가져온 데이터와 렌더링할 데이터 타입을 선언했습니다. 
// 📍 겹치는 속성이 많아서 extends를 사용하여 인터페이스 간 상속을 하였습니다. 
export interface CommonWeatherInfo {
  name?: string; // 지역이름
  main: {
    temp: number; // 현재 온도
    feels_like: number; // 체감기온
    temp_min: number; // 최저
    temp_max: number; // 최고
  };
  clouds: {
    all: number; // 구름 %
  };
  dt_txt?: string; // 날짜
}

export interface WeatherInfo extends CommonWeatherInfo {
  weather: [
    {
      description: string; // 날씨 설명
    }
  ];
}

export interface WeatherInfoToRender extends CommonWeatherInfo {
  weatherImg: string;
}

export interface GetFiveDayWeatherInfo {
  list: WeatherInfo[];
}
  1. 검색 기능
    1. /day/:area or /week/:area 와 같이 params로 검색한 지역을 넘겨줍니다. :: hint) useParams
    2. 이를 가져와 오늘/ 주간 날씨를 렌더링
// 📍 검색 컴포넌트에서 ref로 검색어를 받아 다음과 같이 navigate를 해준 후 
const navigate = useNavigate();
const inputRef = useRef<HTMLInputElement>(null);
const [isDailyInfo, setIsDailyInfo] = useState<boolean>(true);

const handleNavigate = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const area = inputRef.current?.value;
    const type = isDailyInfo ? "day" : "week";
    navigate(`/${type}/${area}`);
  };
// 📍 날씨 카드 컴포넌트에서 useParams로 값을 받아 데이터 요청함수 호출을 해주었습니다. 
const { type, area } = useParams();
  const [detailWeatherInfo, setDetailWeatherInfo] = useState<
    WeatherInfoToRender | WeatherInfoToRender[]
  >();

  const { dailyInfo, weeklyInfo, isLoading, isError } = useGetWeatherData(
    type as string,
    area as string
  );

✅ Outlet + Route

  1. Outlet과 Route를 활용하여 페이지 내의 컴포넌트를 변경해주세요!
    1. 헤더는 고정
📍 헤더와 검색창은 고정하고 카드들만 경로에 따라 바꿔주었습니다. 
const WeatherPage = () => {
  return (
    <div>
      <Header />
      <SearchInput />
      <Outlet />
    </div>
  );
};
  1. 기상 정보 카드들만 경로에 따라 변경됩니다.
  2. 에러페이지 처리도 필수!
📍 라우터 구조입니다!
const Router = () => {
 return (
   <>
     <BrowserRouter>
       <Routes>
         <Route path="/" element={<WeatherPage />}>
           <Route path="/:type/:area" element={<WeatherSection />} />
         </Route>
         <Route path="*" element={<Error404 />} />  // 📍 에러 페이지 처리
       </Routes>
     </BrowserRouter>
   </>
 );
};

🌈 심화과제

✅ 스켈레톤 UI

  • 기상 정보를 불러올때, 사용자에게 스켈레톤 UI를 보이게 합니다!
//📍 커스텀 훅에서 isLoading을 받아서 로딩 중일 경우 스켈레톤 UI를 렌더링했습니다. 
if (isLoading)
    return <LoadingSection isDaily={type === "day" ? true : false} />;

---

const LoadingSection = (props: LoadingSectionProps) => {
  const { isDaily } = props;

  if (isDaily) {
    return (
      <>
        <StFrameWrapper>
          <WeatherCardSkeleton />
        </StFrameWrapper>
      </>
    );
  } else {
    return (
      <StFrameWrapper>
        {Array(5)
          .fill(true)
          .map((_, idx) => (
            <WeatherCardSkeleton key={idx} />
          ))}
      </StFrameWrapper>
    );
  }
};
// 📍 애니메이션은 따로 파일로 분리했고 import 받아 사용했습니다!
export const loading = keyframes`
  0% {
        background-color: rgba(165, 165, 165, 0.1);
    }
    50% {
        background-color: rgba(165, 165, 165, 0.3);
    }

    100% {
        background-color: rgba(165, 165, 165, 0.1);
    }
`;

✅ 커스텀훅으로 데이터를 받아옵시다!

// 📍 커스텀 훅을 통해 일간 날씨 정보, 주간 날씨 정보, 로딩 정보, 에러 정보를 한 번에 받아왔습니다!
export const useGetWeatherData = (type: string, area: string) => {
  const [isLoading, setIsLoading] = useState<boolean>(false);
  const [isError, setIsError] = useState<boolean>(false);
  const [dailyInfo, setDailyInfo] = useState<WeatherInfo>();
  const [weeklyInfo, setWeeklyInfo] = useState<GetFiveDayWeatherInfo>();

  const apiAddress =
    type === "day"
      ? `/weather?q=${area}&appid=${
          import.meta.env.VITE_APP_WEATHER
        }&units=metric`
      : `/forecast?q=${area}&appid=${
          import.meta.env.VITE_APP_WEATHER
        }&units=metric`;

  //getData는 비동기 함수를 "실행하는 함수"이다.
  //따라서 내부 실행은 비동기적으로 되겠지만 만약 여기서 받아온 값을 변수에 넣어줘야한다면 getData 앞에도 await를 붙여야한다.
  //붙이지 않으면 비동기함수가 실행되고 있는 와중에 다른 코드를 실행해버리기 때문에 제대로 get 함수가 리턴하는 값을 받아올 수 없다.
  const getData = async (type: string) => {
    setIsLoading(true);
    try {
      const { data } = await client.get(apiAddress);
      type === "day" ? setDailyInfo(data) : setWeeklyInfo(data);
    } catch (error) {
      setIsError(true);
    } finally {
      setIsLoading(false);
    }
  };

  useEffect(() => {
    setDailyInfo(undefined);
    setWeeklyInfo(undefined);
    getData(type);
  }, [apiAddress]);

  return { dailyInfo, weeklyInfo, isLoading, isError };
};

🌼 PR Point

  • 주간, 일간 커스텀 훅과 컴포넌트를 각각 하나씩만 사용하도록 했는데 그러다 보니 내부 로직이 조금 깔끔하지 못한 건 아닌가,,싶네요 이 부분 중점적으로 봐주시면 감사하겠습니다!

🥺 소요 시간, 어려웠던 점

  • 8h
  • 타입스크립트로 짜다보니까 + 일간/주간 날씨 정보를 한 컴포넌트를 사용해 렌더링하다보니 타입 에러가 발생해서 이걸 처리하는데 가장 시간이 걸렸습니다! isArray 사용해서 주간 날씨 정보를 가져온 경우를 잡을 수 있도록 처리했어요.

🌈 구현 결과물

2023-05-13.12.04.05.mov

pinktopaz added 30 commits May 10, 2023 12:39
@pinktopaz pinktopaz self-assigned this May 12, 2023
@pinktopaz pinktopaz changed the title Week4/weather [4주차 기본/심화과제] ☀️승경씨의 날씨 예보☀️ May 12, 2023
Copy link

@Geun-Oh Geun-Oh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고했어요오~~~ 몸조리 잘해야해!!!!!

Comment on lines +13 to +14
"react": "^18.2.0",
"react-dom": "^18.2.0"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

으이잉??? react-router-dom이랑 styled-components 어디 갔지..?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 그러게! 신기하다👀

Comment on lines +7 to +11
<div>
<Header />
<SearchInput />
<Outlet />
</div>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 여기 div 태그로 감싸져있는 이유가 뭔가요옹

</StPeriodOption>

<input placeholder="영어로 도시명 ex) seoul " ref={inputRef} />
<button type="submit">날씨 검색</button>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어차피 눌렀을 때 실제 form 을 submit 하는게 아니고 추가적인 로직들을 수행하도록 되어있는 거니까!

type='button'으로 설정해두면 e.preventDefault를 할 필요도 없을 것 같아!!

사실 둘 중 뭐가 나은지는 모르겠는데...그런 방법도 있다! 는거죵~

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

하 형근아 혹시 녹음으로 코리했어?

Comment on lines +8 to +11
const StErrorMsg = styled.h2`
margin-left: 3rem;
font-size: 3rem;
`;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오옹 이거어는 이렇게 스타일을 지정해준 이유가 있나요??

h2를 쓰고 거기에 마진과 사이즈를 재조정...특이한 스타일링이라 그냥 궁금해서!

} = props;
let [month, date]: [string, string] = ["", ""];
if (dt_txt) {
[month, date] = dt_txt.slice(5, 10).split("-");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 짱 맘에 들어

},
isDaily,
} = props;
let [month, date]: [string, string] = ["", ""];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요오기도 타입이 string이지만 보여지는 값은 숫자니까..! 이왕이면 요기 타입을 number로 지정하면 다른 이상한 문자열이 들어오는 경우를 방지할 수 있지 않을까! 그렇게 해두고 나중에 실제 값이 사용될 때 string으로 형변환을 해주면 되니까!

타스를 쓰니까 이왕이면 좀 더 좁고 엄격한(narrow) 타입을 사용해보면 좋을 것 같아용

Comment on lines +20 to +21
type as string,
area as string
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오잉 요기는 커스텀 훅 만드는 과정에서 지정해줬으니 여기서까지 타입 단언을 하지 않아도 괜찮지 않을까?
요기서 타입 단언 해준 이유가 궁금쓰합니당~~

<StWrapper>
<WeatherCard
isDaily={true}
weatherCardInfo={detailWeatherInfo as WeatherInfoToRender}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요기도! 타입 단언을 통해서 타입을 다시 명시해주는 이유가 궁금합니당~~

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 궁금해요 👀👀👀

Comment on lines +9 to +10
<div></div>
<img />
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요오기는 이왕이면 어떤 역할을 하는지 클래스나 아이디로라도 적어주면 좋을 것 같아용

Comment on lines +3 to +8
const client = axios.create({
baseURL: import.meta.env.VITE_APP_IP,
headers: {
"Content-type": "application/json",
},
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

axios 인스턴스 생성한거 조아요~

Copy link

@Geun-Oh Geun-Oh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고했어요오~~~ 몸조리 잘해야해!!!!!

Comment on lines +13 to +14
"react": "^18.2.0",
"react-dom": "^18.2.0"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 그러게! 신기하다👀

<BrowserRouter>
<Routes>
<Route path="/" element={<WeatherPage />}>
<Route path="/:type/:area" element={<WeatherSection />} />
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 한 번에 라우터 설정할 수 있었구나! 나는 더 어렵게 생각해서 라우팅 설정에서 시간 많이 썼었는데 배워가요!

Copy link
Member

@eunbeann eunbeann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

역쉬 성경이 므찌다 므찌당!!!!
타스 시작 전에 봤으면 하나도 몰랐을텐데
고거 조금 했다고 엄청 흥미롭다!!!!

코드 잘 보고 갔습니다!!
질문 단 것도 답변 해주세요!!

수고해써 쪽쪽


function App() {
const [count, setCount] = useState(0);
// const { weatherInfo } = useGetWeatherData("Seoul");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

주석아 내가 한 번 사라져볼게 얍!

<BrowserRouter>
<Routes>
<Route path="/" element={<WeatherPage />}>
<Route path="/:type/:area" element={<WeatherSection />} />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

나 요렇게 쓰고 싶던거 못 썼는데 성경이 코드 얌지게 보고 배워가야지 ✨✨✨✨


const SearchInput = () => {
const navigate = useNavigate();
const inputRef = useRef<HTMLInputElement>(null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

타스로햇고나...!!

Comment on lines +15 to +21
const handleNavigate = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const area = inputRef.current?.value;
const type = isDailyInfo ? "day" : "week";
console.log(area, type);
navigate(`/${type}/${area}`);
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 깔끔해 내가 이 코드를 못 맹글어가지고....

</StPeriodOption>

<input placeholder="영어로 도시명 ex) seoul " ref={inputRef} />
<button type="submit">날씨 검색</button>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

하 형근아 혹시 녹음으로 코리했어?

<StWrapper>
<WeatherCard
isDaily={true}
weatherCardInfo={detailWeatherInfo as WeatherInfoToRender}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 궁금해요 👀👀👀

Comment on lines +8 to +9
const LoadingSection = (props: LoadingSectionProps) => {
const { isDaily } = props;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

나나 궁금한거

Suggested change
const LoadingSection = (props: LoadingSectionProps) => {
const { isDaily } = props;
const LoadingSection = ({ isDaily }: LoadingSectionProps) => {

이렇게 적지 않는 이유는 무엇인가요?????

@@ -0,0 +1,14 @@
import { keyframes } from "styled-components";

export const loading = keyframes`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요걸 따로 뺴줬구나...!! 조은데?

@@ -0,0 +1,68 @@
export const WEATHER_TYPE = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

왜 두개이죠?!?!?!

Comment on lines +11 to +20
const apiAddress =
type === "day"
? `/weather?q=${area}&appid=${
import.meta.env.VITE_APP_WEATHER
}&units=metric`
: `/forecast?q=${area}&appid=${
import.meta.env.VITE_APP_WEATHER
}&units=metric`;
console.log(type, area);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제 바이블로 할게요.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants