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주차 기본/심화 과제 ] 4주차 과제 제출 #7

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

Conversation

Geun-Oh
Copy link
Contributor

@Geun-Oh Geun-Oh commented May 11, 2023

✨ 구현 기능 명세

기본 과제

✅ 기상 카드

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

=> 구현 완료했습니다!

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

  1. 날씨 검색 타입
    1. 오늘 → 하루
    2. 주간 → 5일 예보
  2. 검색 기능
    1. /day/:area or /week/:area 와 같이 params로 검색한 지역을 넘겨줍니다. :: hint) useParams
    2. 이를 가져와 오늘/ 주간 날씨를 렌더링

=> useParams를 활용해 렌더링하도록 구현했습니다!

✅ Outlet + Route

  1. Outlet과 Route를 활용하여 페이지 내의 컴포넌트를 변경해주세요!
    1. 헤더는 고정
    2. 기상 정보 카드들만 경로에 따라 변경됩니다.
  2. 에러페이지 처리도 필수!

=> 구현 완료했습니다! 에러 페이지 처리 완료했습니다! 추가적으로 라우터 단위에서 에러 페이지를 표시하도록 했습니다.

심화 과제

✅ 스켈레톤 UI

  • 기상 정보를 불러올때, 사용자에게 스켈레톤 UI를 보이게 합니다!

=> 구현하고 Shimmer 이펙트 적용했습니다!

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

  • 저 같은 경우에는 isError, isLoading, data를 커스텀훅의 반환값으로 만들어서 이를 스켈레톤 UI랑 연결지었습니다!!!

=> 커스텀 훅 구현하였습니다!


🌼 PR Point

커스텀 훅

src/api/hooks/weatherInfo.ts

const useGetWeatherInfo = (type: string, area: string) => {
  const [dailyData, setDailyData] = useState<WeatherInfoProps>();
  const [weeklyData, setWeeklyData] = useState<GetFiveDayWeatherInfoProps>();
  const [isLoading, setIsLoading] = useState<boolean>(false);
  const [isError, setIsError] = useState<boolean>(false);
  const [error, setError] = useState<unknown>();

  const url =
    type === "weekly"
      ? `https://api.openweathermap.org/data/2.5/forecast?q=${area}&appid=${import.meta.env.VITE_APP_WEATHER}&units=metric`
      : `https://api.openweathermap.org/data/2.5/weather?q=${area}&appid=${import.meta.env.VITE_APP_WEATHER}&units=metric`;

  const getWeatherInfo = useCallback(async () => {
    setIsLoading(true);
    try {
      axios
        .get(url)
        .then((res) => {
          type === "daily" ? setDailyData(res.data) : setWeeklyData(res.data);
        })
        .finally(() => {
          setIsLoading(false);
        });
    } catch (err: unknown) {
      setIsError(true);
      setError(err);
    }
  }, [url]) ;

  useEffect(() => {
    setDailyData(undefined);
    setWeeklyData(undefined);
    getWeatherInfo();
  }, [url]);

  return { dailyData, weeklyData, isLoading, isError, error };
};

커스텀 훅을 구현하여 훅 내부에서 비동기 함수의 실행 상태에 따라 data, loading, error 를 표시해줄 수 있도록 하였습니다.
추가적으로 비동기 함수에 useCallback을 적용하여 url이 바뀌는 경우에만 함수를 리프레쉬하도록 해주었습니다.

예외처리

src/components/atom/Error404.tsx

const Error404 = ({ error }: { error: string }) => {
    return (
        <St.InfoCardSectionWrapper>
                <h1>날씨 정보를 불러올 수 없어요...</h1>
                <strong>에러 내용 : {error}</strong>
        </St.InfoCardSectionWrapper>
    )
}

export default Error404;

기본적인 에러 표시 컴포넌트를 만든 뒤,

src/components/template/InfoCardSection.tsx

const InfoCardSection = () => {
    const { type, area } = useParams();
    const { dailyData, weeklyData, isError, isLoading, error } = useGetWeatherInfo(type!, area!);

    if (isLoading) return type === 'daily' ? <DailyShimmer /> : <WeeklyShimmer />;

    if (isError) return <Error404 error={String(error)} />;
        
    if (dailyData) {
        const imgUrl = WEATHER_TYPE.find(x => x.description === dailyData.weather[0].description)
        return (
            <St.InfoCardSectionWrapper>
                <p>현재 도시 : {dailyData.name}</p>
                <St.InfoCardWrapper>
                    {imgUrl && <MemoizedInfoCard date={getMonthDate()} imgUrl={imgUrl.imgURL} temp={dailyData.main.temp} feels_like={dailyData.main.feels_like} temp_min={dailyData.main.temp_min} temp_max={dailyData.main.temp_max} clouds_all={dailyData.clouds.all} />}
                </St.InfoCardWrapper>
            </St.InfoCardSectionWrapper>
        )
    } else if (weeklyData) {
        const newData = weeklyData?.list.filter(x => x.dt_txt?.substring(11, 13) === "06").splice(0, 5)
        return (
            <St.InfoCardSectionWrapper>
                <p>현재 도시 : {weeklyData?.city.name}</p>
                <St.InfoCardWrapper>
                    {newData && newData.map((data, index) => <MemoizedInfoCard date={data.dt_txt!.substring(5, 10)} key={index} imgUrl={WEATHER_TYPE.find(x => x.description === data.weather[0].description) && WEATHER_TYPE.find(x => x.description === data.weather[0].description)!.imgURL} temp={data.main.temp} feels_like={data.main.feels_like} temp_min={data.main.temp_min} temp_max={data.main.temp_max} clouds_all={data.clouds.all} />)}
                </St.InfoCardWrapper>
            </St.InfoCardSectionWrapper>
        )
    } else {
        return <Error404 error="데이터를 불러오지 못했습니다..." />
    }
}

맨 마지막에 데이터가 존재하지 않는 경우 에러를 반환하도록 해주었습니다.

src/pages/Router.tsx

const Router = () => {
    return (
        <BrowserRouter>
            <Suspense>
                <Routes>
                    <Route path="/" element={<MainPage />}>
                        <Route path="/:type" element={<Error404 error="도시 이름을 입력해주세요!" />} />
                        <Route path="/:type/:area" element={<InfoCardSection />} />
                    </Route>
                </Routes>
            </Suspense>
        </BrowserRouter>
    )
}

이때 인풋 값에 아무것도 없는 경우를 처리해주기 위해
라우터 단에서 만일 area정보가 없는 상태로 라우팅이 되면 에러 컴포넌트가 반환되도록 해주었습니다.

현재 도시를 나타내도록 레이아웃 재구성

src/components/template/InfoCardSectin.tsx

            <St.InfoCardSectionWrapper>
                <p>현재 도시 : {dailyData.name}</p>
                <St.InfoCardWrapper>
                    {imgUrl && <MemoizedInfoCard date={getMonthDate()} imgUrl={imgUrl.imgURL} temp={dailyData.main.temp} feels_like={dailyData.main.feels_like} temp_min={dailyData.main.temp_min} temp_max={dailyData.main.temp_max} clouds_all={dailyData.clouds.all} />}
                </St.InfoCardWrapper>
            </St.InfoCardSectionWrapper>

일간 / 주간 관계없이 사용자가 검색한 현재 위치를 표시해주도록 '현재 도시' 를 따로 정의하여 넣어주었습니다!

렌더링 최적화

src/components/organism/InfoCard.tsx

const InfoCard = ({ date, imgUrl, temp, feels_like, temp_min, temp_max, clouds_all }: InfoCardProps) => {
    return (
        <St.InfoCardWrapper>
            <p>{date}</p>
            {imgUrl ? <img src={imgUrl} alt={imgUrl} /> : <St.ThereIsNoImg src="/assets/no_img.png" alt="NO IMG" />}
            <InfoText description="온도" value={temp} />
            <InfoText description="체감 온도" value={feels_like} />
            <InfoText description="최저/최고" value={`${temp_min}/${temp_max}`} />
            <InfoText description="구름" value={`${clouds_all}%`} />
        </St.InfoCardWrapper>
    )
}
...
const MemoizedInfoCard = React.memo(InfoCard);

export default MemoizedInfoCard;

앞서 언급한 useCallback과 더불어 props에 동일한 데이터가 내려오는 경우 InfoCard의 리렌더링이 발생하지 않도록 메모이제이션 해주었습니다.

Fallback shimmer 적용

src/components/atom/Shimmer.tsx

const Shimmer = ({ width, height }: { width: string, height: string }) => {
    return <St.Shimmer width={width} height={height} >
    </St.Shimmer>
}

const St = {
    Shimmer: styled.div<{ width: string, height: string }>`
    width: ${props => props.width};
        height: ${props => props.height};

        border-radius: 20px;

        background: #f6f7f8;
        background-image: linear-gradient(to right, #f6f7f8 0%, #edeef1 20%, #f6f7f8 40%, #f6f7f8 100%);
        background-repeat: no-repeat;
        background-size: 800px 104px; 
        display: inline-block;
        position: relative; 
        
        animation-duration: 1s;
        animation-fill-mode: forwards; 
        animation-iteration-count: infinite;
        animation-name: placeholderShimmer;
        animation-timing-function: linear;

        @keyframes placeholderShimmer {
            0% {
              background-position: -468px 0;
            }
            
            100% {
              background-position: 468px 0; 
            }
        }
    `
}

export default Shimmer;

높이와 너비를 입력받는 쉬머 스켈레톤 컴포넌트를 만들었고,

src/components/template/InfoCardSection.tsx

    const { dailyData, weeklyData, isError, isLoading, error } = useGetWeatherInfo(type!, area!);

    if (isLoading) return type === 'daily' ? <DailyShimmer /> : <WeeklyShimmer />;

    if (isError) return <Error404 error={String(error)} />;

Loading 중일 때 각 상황에 맞는 쉬머 컴포넌트를 렌더링하도록 해주었습니다.


🥺 소요 시간, 어려웠던 점

  • 8h
  • 비동기 함수를 호출해서 객체를 다뤄야하는 부분은 자잘한 타입 오류가 발생하기 쉽다고 생각해 타입스크립트를 적용했습니다.
  • 그래도 제일 오래 걸린 건 역시 비동기 데이터를 불러서 뷰에 맞추는 부분인 것 같아요. 타입 관련 처리를 하는 데 꽤 시간을 먹은 것 같습니다.
  • 사실 에러 상태코드가 꽤 다양했는데, 이것까지 분기처리하기에는 부대 짐 정리를 해야해서^^
  • 이제 보니 placeholder를 신경쓰지 못했네요. 좀 더 사용자 경험을 생각하는 개발자가 되도록 노력하겠습니다...

파트장님 짱~


🌈 구현 결과물

☀️웨비들의 기상예보☀️

@Geun-Oh Geun-Oh self-assigned this May 11, 2023
Copy link

@gunom gunom left a comment

Choose a reason for hiding this comment

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

수고했습니당


const useGetWeatherInfo = (type: string, area: string) => {
const [dailyData, setDailyData] = useState<WeatherInfoProps>();
const [weeklyData, setWeeklyData] = useState<GetFiveDayWeatherInfoProps>();
Copy link

Choose a reason for hiding this comment

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

props를 확인해봐야하긴 할것 같은데 union type으로 dailyData와 weeklyData를 통합할 수 있을 것 같기두?!

<Suspense>
<Routes>
<Route path="/" element={<MainPage />}>
<Route path="/:type" element={<Error404 error="도시 이름을 입력해주세요!" />} />
Copy link

Choose a reason for hiding this comment

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

진짜 별거 아니구 area는 어떤 값이 올지 몰라서 QueryParam이나 QueryString으로 써도 되는데 보통 type으로 daily와 weekly로만 구분되는 url이면 QueryParam을 사용안하고 /daily 또는 /weekly로 고정 url을 쓰는게 맞는거 같습니다!

type === "weekly"
? `https://api.openweathermap.org/data/2.5/forecast?q=${area}&appid=${import.meta.env.VITE_APP_WEATHER}&units=metric`
: `https://api.openweathermap.org/data/2.5/weather?q=${area}&appid=${import.meta.env.VITE_APP_WEATHER}&units=metric`;

Choose a reason for hiding this comment

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

요렇게 type값에 따라서 url 생성한거 좋다!


interface InfoCardProps {
date: string;
imgUrl?: string;

Choose a reason for hiding this comment

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

선택적 매개변수로 표시해줬구나 👍👍

<select onChange={(e) => setType(e.target.value)}>
<option value='daily'>오늘</option>
<option value="weekly">주간</option>
</select>

Choose a reason for hiding this comment

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

select option button 다 넘 시맨틱해!!

Copy link

@Yeonseo-Jo Yeonseo-Jo left a comment

Choose a reason for hiding this comment

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

형근이 수고 했엉 ! 타스까지 완벽하당 배워가용 ~!

axios
.get(url)
.then((res) => {
type === "daily" ? setDailyData(res.data) : setWeeklyData(res.data);

Choose a reason for hiding this comment

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

조건 처리 해준거 좋당 ! 굿굿

@@ -0,0 +1,15 @@
import { St } from "../template/InfoCardSection"

Choose a reason for hiding this comment

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

오 이런식으로 재사용할수 있구낭 배워가용

clouds_all: number;
}

const InfoCard = ({ date, imgUrl, temp, feels_like, temp_min, temp_max, clouds_all }: InfoCardProps) => {

Choose a reason for hiding this comment

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

구조 분해 할당으로 props 넘겨주는 방법 좋다!

`,
InfoCardWrapper: styled.section`
width: 80vw;
padding-top: 20px;

Choose a reason for hiding this comment

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

padding-top만 px로 지정한 이유는 먼가용 ?!

const [type, setType] = useState<string>('daily');

const onClick = () => {
// 나중에 예외처리 합시당

Choose a reason for hiding this comment

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

if (area && type) 정도로 예외처리 해줘도 좋을것 같앙 !

}

const St = {
SearchInputForm: styled.article`

Choose a reason for hiding this comment

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

form이니까 article도 좋지만 form 태그를 사용해도 좋을것 같아!

Comment on lines +3 to +13
weather: [
{
description: string; // 날씨 설명
}
];
main: {
temp: number; // 현재 온도
feels_like: number; // 체감기온
temp_min: number; // 최저
temp_max: number; // 최고
};

Choose a reason for hiding this comment

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

주석까지 넘 깔끔합니당!

Comment on lines +1 to +6
export const getMonthDate = () => {
const getDate = new Date();
const nowMonth = getDate.getMonth() < 10 ? "0" + (getDate.getMonth() + 1) : getDate.getMonth() + 1;
const nowDate = getDate.getDate() < 10 ? "0" + getDate.getDate() : getDate.getDate();
return `${nowMonth}-${nowDate}`;
};

Choose a reason for hiding this comment

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

date 값을 이렇게 만들어줘도 좋지만 daily data에 있는 값을 가져와서 써도 좋을것 같아!

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

Successfully merging this pull request may close these issues.

4 participants