diff --git a/frontend/src/components/pages/main/MainPage.tsx b/frontend/src/components/pages/main/MainPage.tsx
index 65884814..aaba6701 100644
--- a/frontend/src/components/pages/main/MainPage.tsx
+++ b/frontend/src/components/pages/main/MainPage.tsx
@@ -8,8 +8,10 @@ import TravelogueCard from "@components/pages/main/TravelogueCard/TravelogueCard
import useIntersectionObserver from "@hooks/useIntersectionObserver";
import * as S from "./MainPage.styled";
+import TravelogueCardSkeleton from "./TravelogueCard/skeleton/TravelogueCardSkeleton";
const MainPage = () => {
+ const SKELETON_COUNT = 5;
const { travelogues, status, fetchNextPage } = useInfiniteTravelogues();
const { lastElementRef } = useIntersectionObserver(fetchNextPage);
@@ -20,7 +22,13 @@ const MainPage = () => {
지금 뜨고 있는 여행기
다른 이들의 여행을 한 번 구경해보세요.
- {status === "pending" && <>로딩 ...>}
+ {status === "pending" && (
+
+ {Array.from({ length: SKELETON_COUNT }, (_, index) => (
+
+ ))}
+
+ )}
{travelogues.map(({ userAvatar, id, title, thumbnail, likes }) => (
{
+ return (
+
+
+
+ );
+ },
+ ],
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/frontend/src/components/pages/main/TravelogueCard/skeleton/TravelogueCardSkeleton.styled.ts b/frontend/src/components/pages/main/TravelogueCard/skeleton/TravelogueCardSkeleton.styled.ts
new file mode 100644
index 00000000..5bddedfa
--- /dev/null
+++ b/frontend/src/components/pages/main/TravelogueCard/skeleton/TravelogueCardSkeleton.styled.ts
@@ -0,0 +1,67 @@
+import { keyframes } from "@emotion/react";
+import styled from "@emotion/styled";
+
+const skeletonAnimation = keyframes`
+ 0% {
+ background-position: -100% 0;
+ }
+ 100% {
+ background-position: 100% 0;
+ }
+`;
+
+const SkeletonBase = styled.div`
+ background-color: ${(props) => props.theme.colors.skeleton.base};
+ background-image: linear-gradient(
+ 90deg,
+ ${(props) => props.theme.colors.skeleton.base} 6rem,
+ ${(props) => props.theme.colors.skeleton.highlight} 12rem,
+ ${(props) => props.theme.colors.skeleton.base} 18rem
+ );
+ background-size: 200% 200%;
+ background-repeat: no-repeat;
+
+ animation: ${skeletonAnimation} 1.2s ease-in-out infinite;
+`;
+
+export const Layout = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${(props) => props.theme.spacing.m};
+`;
+
+export const ThumbnailCard = styled(SkeletonBase)`
+ width: 100%;
+ height: 25rem;
+ border-radius: 10px;
+`;
+
+export const BottomBarContainer = styled.div`
+ display: flex;
+ justify-content: space-between;
+ width: 100%;
+ padding: 0 ${(props) => props.theme.spacing.m};
+`;
+
+export const TitleContainer = styled.div`
+ display: flex;
+ gap: ${(props) => props.theme.spacing.s};
+`;
+
+export const ProfileSkeleton = styled(SkeletonBase)`
+ width: 2.2rem;
+ height: 2.2rem;
+ border-radius: 50%;
+`;
+
+export const TitleSkeleton = styled(SkeletonBase)`
+ width: 13rem;
+ height: 2.2rem;
+ border-radius: 10px;
+`;
+
+export const LikesSkeleton = styled(SkeletonBase)`
+ width: 5rem;
+ height: 2.2rem;
+ border-radius: 10px;
+`;
diff --git a/frontend/src/components/pages/main/TravelogueCard/skeleton/TravelogueCardSkeleton.tsx b/frontend/src/components/pages/main/TravelogueCard/skeleton/TravelogueCardSkeleton.tsx
new file mode 100644
index 00000000..8495c273
--- /dev/null
+++ b/frontend/src/components/pages/main/TravelogueCard/skeleton/TravelogueCardSkeleton.tsx
@@ -0,0 +1,20 @@
+import * as S from "./TravelogueCardSkeleton.styled";
+
+const TravelogueCardSkeleton = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default TravelogueCardSkeleton;
diff --git a/frontend/src/styles/tokens/colors.ts b/frontend/src/styles/tokens/colors.ts
index 8d5355ea..19ba37a1 100644
--- a/frontend/src/styles/tokens/colors.ts
+++ b/frontend/src/styles/tokens/colors.ts
@@ -42,4 +42,8 @@ export const SEMANTIC_COLORS = {
danger: "#ea0000",
kakao: "#f9e007",
dimmed: "#0000004d",
+ skeleton: {
+ base: PRIMITIVE_COLORS.gray[200],
+ highlight: PRIMITIVE_COLORS.gray[100],
+ },
} as const;