Skip to content

Commit

Permalink
feat: add skeleton ui and tanstack-query
Browse files Browse the repository at this point in the history
- tanstack-query 사용
- Skeleton UI 사용
- next dynamic 활용하여 동적 로딩 적용
  • Loading branch information
rarlala committed Sep 30, 2024
1 parent 848ee55 commit dfbc6ce
Show file tree
Hide file tree
Showing 14 changed files with 212 additions and 27 deletions.
34 changes: 34 additions & 0 deletions .pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
},
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@tanstack/react-query": "^5.56.2",
"classnames": "^2.5.1",
"immer": "^10.1.1",
"next": "14.2.9",
Expand Down
46 changes: 31 additions & 15 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,44 @@
'use client';

import { useEffect, useState } from 'react';
import BannerSection from '@/components/main/bannerSection';
import BoxSection from '@/components/main/boxSection';
import dynamic from 'next/dynamic';
import { useQuery } from '@tanstack/react-query';
import { BannerSectionSkeleton } from '@/components/main/bannerSection';
import { BoxSectionSkeleton } from '@/components/main/boxSection';
import ViewMoreButton from '@/components/main/viewMoreButton';
import { BannerType, BoxBannerType } from '@/types/main';
import { fetchMainData } from '@/fetch/main';
import { MainDataType } from '@/types/main';

const BannerSection = dynamic(() => import('@/components/main/bannerSection'), {
loading: () => <BannerSectionSkeleton />,
ssr: false,
});

const BoxSection = dynamic(() => import('@/components/main/boxSection'), {
loading: () => <BoxSectionSkeleton />,
ssr: false,
});

export default function Home() {
const [data, setData] = useState<{
banners: BannerType[];
boxes: BoxBannerType[];
} | null>(null);
const { data } = useQuery<MainDataType>({
queryKey: ['main'],
queryFn: fetchMainData,
staleTime: 60000,
});

useEffect(() => {
fetch('/main')
.then((response) => response.json())
.then((data) => setData(data));
}, []);
if (!data) {
return (
<div>
<BannerSectionSkeleton />
<BoxSectionSkeleton />
</div>
);
}

return (
<div className="flex-1 font-pretendard">
<div className="flex flex-col md:flex-row">
<BannerSection data={data?.banners} />
<BoxSection data={data?.boxes} />
<BannerSection data={data.banners} />
<BoxSection data={data.boxes} />
</div>
<ViewMoreButton />
</div>
Expand Down
18 changes: 18 additions & 0 deletions src/components/common/skeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import cn from 'classnames';

type Props = {
width?: string | number;
height?: string | number;
style?: string;
};

const Skeleton = ({ width, height, style }: Props) => {
return (
<div
className={cn('skeleton bg-gray-200 animate-pulse', style)}
style={{ width, height }}
/>
);
};

export default Skeleton;
9 changes: 9 additions & 0 deletions src/components/main/banner.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Image from 'next/image';
import Link from 'next/link';
import cn from 'classnames';
import Skeleton from '../common/skeleton';
import { BannerType } from '@/types/main';

type Props = {
Expand All @@ -9,6 +10,10 @@ type Props = {
};

const Banner = ({ data, style }: Props) => {
if (!data) {
return null;
}

return (
<Link href={data.link} className={cn('relative w-full h-auto', style)}>
<Image
Expand All @@ -24,4 +29,8 @@ const Banner = ({ data, style }: Props) => {
);
};

export const BannerSkeleton = () => {
return <Skeleton style="w-full aspect-[5/9] desktop-only" />;
};

export default Banner;
20 changes: 19 additions & 1 deletion src/components/main/bannerSection.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import MobileBannerSlider from './mobileBannerSlider';
import MobileBannerSlider, {
MobileBannerSliderSkeleton,
} from './mobileBannerSlider';
import Banner from './banner';
import Skeleton from '../common/skeleton';
import { BannerType } from '@/types/main';

type Props = {
Expand All @@ -19,4 +22,19 @@ const BannerSection = ({ data }: Props) => {
);
};

export const BannerSectionSkeleton = () => {
return (
<section className="flex flex-col w-full md:w-[44%] md:pr-[2%]">
<div className="flex flex-col gap-4 desktop-only">
{[...Array(3)].map((_, index) => (
<Skeleton key={index} style="w-full aspect-[5/6]" />
))}
</div>
<div className="mobile-only">
<MobileBannerSliderSkeleton />
</div>
</section>
);
};

export default BannerSection;
16 changes: 15 additions & 1 deletion src/components/main/boxBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import Image from 'next/image';
import Link from 'next/link';
import BoxBannerItem from './boxBannerItem';
import BoxBannerItem, { BoxBannerItemSkeleton } from './boxBannerItem';
import { BoxBannerType, ProductType } from '@/types/main';
import Skeleton from '../common/skeleton';

type Props = {
data: BoxBannerType;
Expand Down Expand Up @@ -31,4 +32,17 @@ const BoxBanner = ({ data }: Props) => {
);
};

export const BoxBannerSkeleton = () => {
return (
<div className="relative flex flex-col gap-3 px-[32px] py-[24px]">
<div className="flex flex-col gap-3">
<Skeleton style="aspect-[1/1]" />
<Skeleton style="h-[22px]" />
<Skeleton style="h-[15px]" />
</div>
<BoxBannerItemSkeleton />
</div>
);
};

export default BoxBanner;
17 changes: 17 additions & 0 deletions src/components/main/boxBannerItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Image from 'next/image';
import Link from 'next/link';
import { ProductType } from '@/types/main';
import { formatLikesToK, formatWithCommas } from '@/utils/format';
import Skeleton from '../common/skeleton';
import { Icons } from '../icons';

type Props = {
Expand Down Expand Up @@ -46,4 +47,20 @@ const BoxBannerItem = ({ product }: Props) => {
);
};

export const BoxBannerItemSkeleton = () => {
return (
<div className="flex justify-between items-center py-[10px] h-[76px] gap-2 border-t-[1px] border-[#ccc] overflow-hidden">
<div className="flex gap-2 min-w-[294px]">
<Skeleton style="w-[54px] h-[54px]" />

<div className="min-w-[200px] flex flex-col gap-2">
<Skeleton style="h-[12px]" />
<Skeleton style="h-[12px]" />
<Skeleton style="h-[10px]" />
</div>
</div>
</div>
);
};

export default BoxBannerItem;
20 changes: 18 additions & 2 deletions src/components/main/boxSection.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
'use client';

import Masonry, { ResponsiveMasonry } from 'react-responsive-masonry';
import Banner from './banner';
import BoxBanner from './boxBanner';
import Banner, { BannerSkeleton } from './banner';
import BoxBanner, { BoxBannerSkeleton } from './boxBanner';
import BoxSlider from './boxSlider';
import {
BannerType,
Expand All @@ -19,6 +19,7 @@ const BoxSection = ({ data }: Props) => {
if (data === undefined) {
return null;
}

return (
<section className="relative w-full md:w-[56%]">
<ResponsiveMasonry columnsCountBreakPoints={{ 300: 1, 719: 2 }}>
Expand All @@ -45,4 +46,19 @@ const BoxSection = ({ data }: Props) => {
);
};

export const BoxSectionSkeleton = () => {
return (
<section className="relative w-full md:w-[56%]">
<ResponsiveMasonry columnsCountBreakPoints={{ 300: 1, 719: 2 }}>
<Masonry>
<BannerSkeleton />
<BannerSkeleton />
<BoxBannerSkeleton />
<BoxBannerSkeleton />
</Masonry>
</ResponsiveMasonry>
</section>
);
};

export default BoxSection;
11 changes: 10 additions & 1 deletion src/components/main/mobileBannerSlider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
import { Autoplay, Pagination } from 'swiper/modules';
import { BannerType } from '@/types/main';
import Banner from './banner';
import Skeleton from '../common/skeleton';
import { BannerType } from '@/types/main';
import { Icons } from '../icons';

type Props = {
Expand Down Expand Up @@ -75,4 +76,12 @@ const MobileBannerSlider = ({ data }: Props) => {
);
};

export const MobileBannerSliderSkeleton = () => {
return (
<div className="my-2">
<Skeleton style="relative left-[5vw] w-[90vw] aspect-[5/6] rounded-lg" />
</div>
);
};

export default MobileBannerSlider;
5 changes: 5 additions & 0 deletions src/fetch/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { MainDataType } from '@/types/main';

export const fetchMainData = async (): Promise<MainDataType> => {
return fetch('/main').then((response) => response.json());
};
18 changes: 11 additions & 7 deletions src/lib/next-auth/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import { SessionProvider } from 'next-auth/react';
import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

import Header from '@/components/header';
import Footer from '@/components/footer';
Expand All @@ -11,14 +11,18 @@ interface Props {
children: React.ReactNode;
}

const queryClient = new QueryClient();

const AuthProvider = ({ children }: Props) => {
return (
<SessionProvider>
<Header />
{children}
<Footer />
<BottomMenu />
</SessionProvider>
<QueryClientProvider client={queryClient}>
<SessionProvider>
<Header />
{children}
<Footer />
<BottomMenu />
</SessionProvider>
</QueryClientProvider>
);
};

Expand Down
5 changes: 5 additions & 0 deletions src/types/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ export enum MainComponentsType {
BOX_SLIDER = 'BOX_SLIDER',
}

export type MainDataType = {
banners: BannerType[];
boxes: (BannerType | BoxBannerType | BoxSliderType)[] | undefined;
};

export type BannerType = {
type: MainComponentsType;
imgSrc: string;
Expand Down
Loading

0 comments on commit dfbc6ce

Please sign in to comment.