
๐ฏ ๋ฆฌ๋ทฐ, ์ ๊ฐ, ๋ฒ ์คํธ์ ๋ฌ, ๋ฐฐ๋ ๋ฑ์ผ๋ก ๋ฉ์ธ ํ๋ฉด์ ์ ์ํ๊ณ , ๋ชจ๋ฐ์ผ ๋์์ ๊ตฌํํฉ๋๋ค.
React์์ ์ฌ์ฉํ ์ ์๋ ์ฌ๋ผ์ด๋(์บ๋ฌ์
) ๋ผ์ด๋ธ๋ฌ๋ฆฌ์
๋๋ค.

npm install react-slick slick-carousel
npm install -D @types/react-slick
react-slick : React ์ฌ๋ผ์ด๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์
๋๋ค.
slick-carousel : ์ฌ๋ผ์ด๋๊ฐ ์ ๋๋ก ๋์ํ๊ณ ๊พธ๋ฉฐ์ง๋๋ก ํ๊ธฐ ์ํ ๊ธฐ๋ณธ ์คํ์ผ(CSS)์ ํฌํจํ๊ณ ์๋ ์์กด์ฑ ํจํค์ง์
๋๋ค. (๋ฐ๋์ ์ค์น ํ์)
@types/react-slick : TypeScript ํ๊ฒฝ์์ react-slick์ ํ์
์ฒดํฌ ๊ธฐ๋ฅ์ ์ ๊ณตํด์ฃผ๋ ํ์
์ ์ฉ ๋ณด์กฐ ํจํค์ง์
๋๋ค.
๐ค ์์กด์ฑ ํจํค์ง์ด์ง๋ง
-D๋ก ์ค์นํ์ง ์๋ ์ด์ ?
devDependencies๋ ๋ณดํต ํ๋ก๋์ ๋น๋์์๋ ์ ์ธ๋๋๋ฐ,slick-carousel์ ์คํ ํ๊ฒฝ์์๋ ํ์ํ CSS๋ฅผ ์ ๊ณตํ๊ธฐ ๋๋ฌธ์ ๊ทธ๋ฅ ์ค์น๋ฅผ ํด์ผํฉ๋๋ค.
import React from "react";
import Slider from "react-slick";
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";
export default function SimpleSlider() {
var settings = {
dots: true,
infinite: true,
speed: 500,
slidesToShow: 1,
slidesToScroll: 1,
};
return (
<Slider {...settings}>
<div>
<h3>1</h3>
</div>
<div>
<h3>2</h3>
</div>
<div>
<h3>3</h3>
</div>
<div>
<h3>4</h3>
</div>
<div>
<h3>5</h3>
</div>
<div>
<h3>6</h3>
</div>
</Slider>
);
}
settings : ์ค์ ๊ฐ์ฒด์
๋๋ค.
dots: true : ํ๋จ์ ํ์ด์ง ํ์ ์ ํ์
infinite: true : ๋ง์ง๋ง ์ฌ๋ผ์ด๋์์ ์ฒ์์ผ๋ก ์ํ
speed: 500 : ์ฌ๋ผ์ด๋ ๋์ด๊ฐ๋ ์๋ (0.5์ด)
slidesToShow: 1 : ํ ๋ฒ์ ๋ณด์ฌ์ค ์ฌ๋ผ์ด๋ 1๊ฐ
slidesToScroll: 1 : ๋๊ธธ ๋ ํ ์ฅ์ฉ ๋๊น
<Slider {...settings}> : ์ค์ ๊ฐ์ฒด๋ฅผ props๋ก ๋๊ฒจ ์ฌ๋ผ์ด๋์ ์ ์ฉํฉ๋๋ค.
export const reviewForMain = http.get('http://localhost:9999/reviews', () => {
return HttpResponse.json(mockReviewData, {
status: 200,
});
});
๋ฉ์ธํ์ด์ง์ฉ ๋ฆฌ๋ทฐ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ง๋ก ์๋ตํด์ฃผ๋ ํจ๋ค๋ฌ๋ฅผ ์์ฑํฉ๋๋ค.
http://localhost:9999/reviews๋ก GET ์์ฒญ์ด ์ค๋ฉด, mockReviewData ๋ฐฐ์ด์ JSON ํ์์ผ๋ก 200 OK๋ก ์๋ตํด์ฃผ๋ ๊ฐ์ง API์
๋๋ค.export const fetchReviewAll = async () => {
return await requestHandler<BookReviewItem[]>('get', `/reviews`);
};
GET /reviews ์์ฒญ์ ๋ณด๋ด BookReviewItem[] ํ์
์ ์ ์ฒด ๋ฆฌ๋ทฐ ๋ฆฌ์คํธ๋ฅผ ๋ฐ์์ค๋ ๋น๋๊ธฐ ํจ์์
๋๋ค.
import { BookReviewItem as IBookReviewItem } from '@/models/book.model';
import styled from 'styled-components';
import BookReviewItem from '../book/BookReviewItem';
import Slider from 'react-slick';
import 'slick-carousel/slick/slick.css';
import 'slick-carousel/slick/slick-theme.css';
import { useMediaQuery } from '@/hooks/useMediaQuery';
interface Props {
reviews: IBookReviewItem[];
}
export default function MainReview({ reviews }: Props) {
const { isMobile } = useMediaQuery();
let sliderSettings = {
dots: true,
infinite: true,
speed: 500,
slidesToShow: isMobile ? 1 : 3,
slidesToScroll: isMobile ? 1 : 3,
gap: 16,
};
return (
<StyledMainReview>
<Slider {...sliderSettings}>
{reviews.map((review) => (
<BookReviewItem key={review.id} review={review} />
))}
</Slider>
</StyledMainReview>
);
}
const StyledMainReview = styled.div`
padding: 0 0 24px 0;
.slick-track {
padding: 12px 0;
}
.slick-slide > div {
margin: 0 12px;
}
.slick-prev:before,
.slick-next:before {
color: #000;
}
@media ${({ theme }) => `screen and ${theme.mediaQuery.mobile}`} {
.slick-prev {
left: 0;
}
.slick-next {
right: 0;
}
}
`;
<Slider {...sliderSettings}> : sliderSettings๋ฅผ ๊ทธ๋๋ก ๋๊ธด react-slider์ ์ฌ๋ผ์ด๋ ์ปดํฌ๋ํธ์
๋๋ค.
reviews ๋ฐฐ์ด์ ์ํํ๋ฉด์, ๊ฐ ๋ฆฌ๋ทฐ๋ฅผ BookReviewItem ์ปดํฌ๋ํธ๋ก ๋ ๋๋งํฉ๋๋ค.
import { Book } from '@/models/book.model';
import styled from 'styled-components';
import BookItem from '../books/BookItem';
interface Props {
books: Book[];
}
export default function MainNewBooks({ books }: Props) {
return (
<StyledMainNewBooks>
{books.map((book) => (
<BookItem key={book.id} book={book} view='grid' />
))}
</StyledMainNewBooks>
);
}
const StyledMainNewBooks = styled.div`
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
@media ${({ theme }) => `screen and ${theme.mediaQuery.mobile}`} {
grid-template-columns: repeat(2, 1fr);
}
`;
books ๋ฐฐ์ด์ ๋๋ฉด์ ๊ฐ๊ฐ์ BookItem ์ปดํฌ๋ํธ๋ก ๋ ๋๋งํฉ๋๋ค.
import { Book } from '@/models/book.model';
import { HttpResponse, http } from 'msw';
import { fakerKO as faker } from '@faker-js/faker';
const bestBooksData: Book[] = Array.from({ length: 10 }).map((_, index) => ({
id: index,
title: faker.lorem.sentence(),
img: faker.number.int({ min: 100, max: 200 }),
category_id: faker.number.int({ min: 0, max: 2 }),
form: '์ข
์ด์ฑ
',
isbn: faker.commerce.isbn(),
summary: faker.lorem.paragraph(),
detail: faker.lorem.paragraph(),
author: faker.person.firstName(),
pages: faker.number.int({ min: 100, max: 500 }),
contents: faker.lorem.paragraph(),
price: faker.number.int({ min: 10000, max: 50000 }),
likes: faker.number.int({ min: 0, max: 100 }),
pubDate: faker.date.past().toISOString(),
}));
export const bestBooks = http.get('http://localhost:9999/books/best', () => {
return HttpResponse.json(bestBooksData, {
status: 200,
});
});
๋ฒ ์คํธ์ ๋ฌ ๋์ ๋ชฉ๋ก์ ๊ฐ์ง๋ก ์๋ตํด์ฃผ๋ ํจ๋ค๋ฌ๋ฅผ ์์ฑํฉ๋๋ค.
http://localhost:9999/books/best๋ก GET ์์ฒญ์ด ์ค๋ฉด, bestBooksData ๋ฐฐ์ด์ JSON ํ์์ผ๋ก 200 OK๋ก ์๋ตํด์ฃผ๋ ๊ฐ์ง API์
๋๋ค.export const fetchBestBooks = async () => {
const response = await httpClient.get<Book[]>(`/books/best`);
return response.data;
};
GET /books/best ์์ฒญ์ ๋ณด๋ด Book[] ํ์
์ ๋ฒ ์คํธ์
๋ฌ ๋ฆฌ์คํธ๋ฅผ ๋ฐ์์ค๋ ๋น๋๊ธฐ ํจ์์
๋๋ค.
import { Book } from '@/models/book.model';
import styled from 'styled-components';
import BookBestItem from '../books/BookBestItem';
interface Props {
books: Book[];
}
export default function MainBest({ books }: Props) {
return (
<StyledMainBest>
{books.map((item, index) => (
<BookBestItem key={item.id} book={item} itemIndex={index} />
))}
</StyledMainBest>
);
}
const StyledMainBest = styled.div`
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 16px;
@media ${({ theme }) => `screen and ${theme.mediaQuery.mobile}`} {
grid-template-columns: repeat(2, 1fr);
}
`;
books ๋ฐฐ์ด์ ๋๋ฉด์ ํ๋์ฉ BookBestItem ์ปดํฌ๋ํธ๋ก ๋ ๋๋งํฉ๋๋ค.import { Book } from '@/models/book.model';
import styled from 'styled-components';
import BookItem, { StyledBookItem } from './BookItem';
import { Theme } from '@/style/theme';
interface Props {
book: Book;
itemIndex: number;
}
export default function BookBestItem({ book, itemIndex }: Props) {
return (
<StyledBookBestItem>
<BookItem book={book} view='grid' />
<div className='rank'>{itemIndex + 1}</div>
</StyledBookBestItem>
);
}
const StyledBookBestItem = styled.div`
${StyledBookItem} {
.summary,
.price,
.likes {
display: none;
}
h2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
}
position: relative;
.rank {
position: absolute;
top: -10px;
left: -10px;
width: 40px;
height: 40px;
background: ${({ theme }) => (theme as Theme).color.primary};
border-radius: 500px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
color: #fff;
font-weight: 700;
font-style: italic;
}
`;
<BookItem> ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฌ์ฉํ์ฌ ์์ ๋ฑ์ง๋ฅผ ์ค๋ฒ๋ ์ด๋ก ๊ฒน์ณ์ ๋ฒ ์คํธ์
๋ฌ๋ฅผ ํํํฉ๋๋ค.
export interface Banner {
id: number;
title: string;
description: string;
image: string;
url: string;
target: string;
}
๋ฐฐ๋ ๋ฐ์ดํฐ ํ์ ์ ์ ์ํฉ๋๋ค.
id : ๋ฐฐ๋ ๊ณ ์ ID
title : ๋ฐฐ๋ ์ ๋ชฉ (ex: '์ ๊ฐ ์ถ์!')
description : ๋ถ์ ๋ชฉ, ์ค๋ช
๋ฌธ๊ตฌ
image : ์ด๋ฏธ์ง URL
url : ๋ฐฐ๋ ํด๋ฆญ ์ ์ด๋ํ ๋งํฌ
target : ๋งํฌ ์ด๊ธฐ ๋ฐฉ์ ('_blank'(์ ํญ์์ ์ด๊ธฐ), '_self'(ํ์ฌ ํญ์์ ์ด๊ธฐ) ๋ฑ)
import { HttpResponse, http } from 'msw';
import { Banner } from '@/models/banner.model';
const bannerData: Banner[] = [
{
id: 1,
title: '์ ๊ฐ ์ถ์: ์์ฃผ ์์ ์ต๊ด์ ํ',
description: '๋ ๋์ ์ต๊ด์ผ๋ก ์ถ์ ๋ฐ๊ฟ๋ณด์ธ์. ์ง๊ธ ์์ํ์ธ์.',
image: 'http://picsum.photos/id/111/1200/400',
url: 'http://some.url',
target: '_blank',
},
{
id: 2,
title: '๋ฌด๋ผ์นด๋ฏธ ํ๋ฃจํค ์ ์',
description: '๋ฌด๋ผ์นด๋ฏธ์ ๋ชฝํ์ ์ธ ์ธ๊ณ๋ฅผ ์ง๊ธ ๋ฐ๋ก ๊ฒฝํํด๋ณด์ธ์.',
image: 'http://picsum.photos/id/222/1200/400',
url: 'http://some.url',
target: '_self',
},
{
id: 3,
title: '๋ด๋ง์ด ๋์ ํ ์ธ!',
description: '์ ํ๋ ์์ค ์ต๋ 40% ํ ์ธ. ๊ธฐ๊ฐ ํ์ !',
image: 'http://picsum.photos/id/33/1200/400',
url: 'http://some.url',
target: '_blank',
},
];
export const banners = http.get('http://localhost:9999/banners', () => {
return HttpResponse.json(bannerData, {
status: 200,
});
});
๋ฒ ๋ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ง๋ก ์๋ตํด์ฃผ๋ ํจ๋ค๋ฌ๋ฅผ ์์ฑํฉ๋๋ค.
http://localhost:9999/banners๋ก GET ์์ฒญ์ด ์ค๋ฉด, bannerData ๋ฐฐ์ด์ JSON ํ์์ผ๋ก 200 OK๋ก ์๋ตํด์ฃผ๋ ๊ฐ์ง API์
๋๋ค.import { setupWorker } from 'msw/browser';
import { addReview, reviewForMain, reviewsById } from './review';
import { bestBooks } from './books';
import { banners } from './banner';
const handlers = [reviewsById, addReview, reviewForMain, bestBooks, banners];
export const worker = setupWorker(...handlers);
๋ฆฌ๋ทฐ, ๋ฒ ์คํธ์
๋ฌ, ๋ฐฐ๋(review, books, banner) ๊ฐ์ง API ํธ๋ค๋ฌ๋ฅผ ๋ชจ์์ Service Worker๋ฅผ ๋ฑ๋กํ์ฌ ์์ฒญ์ ๊ฐ๋ก์ฑ๋๋ค.
import { Banner } from '@/models/banner.model';
import { requestHandler } from './http';
export const fetchBanners = async () => {
return await requestHandler<Banner[]>('get', '/banners');
};
GET /banners ์์ฒญ์ ๋ณด๋ด Banner[] ํ์
์ ๋ฐฐ๋ ๋ฆฌ์คํธ๋ฅผ ๋ฐ์์ค๋ ๋น๋๊ธฐ ํจ์์
๋๋ค.
import { Banner as IBanner } from '@/models/banner.model';
import styled from 'styled-components';
import BannerItem from './BannerItem';
import { useMemo, useState } from 'react';
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa';
import { Theme } from '@/style/theme';
interface Props {
banners: IBanner[];
}
export default function Banner({ banners }: Props) {
const [currentIndex, setCurrentIndex] = useState(0);
const transFormValue = useMemo(() => {
return currentIndex * -100;
}, [currentIndex]);
const handlePrev = () => {
if (currentIndex === 0) return;
setCurrentIndex(currentIndex - 1);
};
const handleNext = () => {
if (currentIndex === banners.length - 1) return;
setCurrentIndex(currentIndex + 1);
};
const handleIndicatorClick = (index: number) => {
setCurrentIndex(index);
};
return (
<StyledBanner>
<StyledBannerContainer $transFormValue={transFormValue}>
{banners.map((item, index) => (
<BannerItem banner={item} />
))}
</StyledBannerContainer>
<StyledBannerButton>
<button className='prev' onClick={handlePrev}>
<FaAngleLeft />
</button>
<button className='next' onClick={handleNext}>
<FaAngleRight />
</button>
</StyledBannerButton>
<StyledBannerIndicator>
{banners.map((banner, index) => (
<span
className={index === currentIndex ? 'active' : ''}
onClick={() => handleIndicatorClick(index)}
></span>
))}
</StyledBannerIndicator>
</StyledBanner>
);
}
const StyledBanner = styled.div`
overflow: hidden;
position: relative;
`;
interface StyledBannerContainerProps {
$transFormValue: number;
}
const StyledBannerContainer = styled.div<StyledBannerContainerProps>`
display: flex;
transform: translateX(${(props) => props.$transFormValue}%);
transition: transform 0.5s ease-in-out;
`;
const StyledBannerButton = styled.div`
button {
border: 0;
width: 40px;
height: 40px;
background: rgba(0, 0, 0, 0.5);
border-radius: 500px;
font-size: 2rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
top: 50%;
transform: translateY(-50%);
svg {
fill: #fff;
}
&.prev {
left: 10px;
}
&.next {
right: 10px;
}
@media ${({ theme }) => `screen and ${theme.mediaQuery.mobile}`} {
width: 28px;
height: 28px;
font-size: 1.5rem;
&.prev {
left: 0px;
}
&.next {
right: 0px;
}
}
}
`;
const StyledBannerIndicator = styled.div`
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
span {
display: inline-block;
width: 16px;
height: 16px;
border-radius: 100px;
background: #fff;
margin: 0 4px;
cursor: pointer;
&.active {
background: ${({ theme }) => (theme as Theme).color.primary};
}
}
@media ${({ theme }) => `screen and ${theme.mediaQuery.mobile}`} {
bottom: 0;
span {
width: 12px;
height: 12px;
&.active {
width: 24px;
}
}
}
`;
<StyledBanner> : banners๋ฅผ ๋๋ฉด์ ํ๋์ฉ BannerItem ์ปดํฌ๋ํธ๋ฅผ ์ข์ฐ๋ก ์ฌ๋ผ์ด๋ฉ ์ฐ์ถ์ ํฉ๋๋ค.
<StyledBannerButton> : ์ด์ /๋ค์ ๋ฒํผ ๋๋ฅด๋ฉด currentIndex ์กฐ์ ๋์ด ๋ณด์ฌ์ง๋ ์ปดํฌ๋ํธ๋ฅผ ๋ณ๊ฒฝํฉ๋๋ค.
<StyledBannerIndicator> : ํ๋จ ๋๊ทธ๋ ์ธ๋์ผ์ดํฐ๋ก ํ์ฌ ์ฌ๋ผ์ด๋์ index์ ๊ฐ์ผ๋ฉด .active ํด๋์ค๋ฅผ ์ ์ฉ์ํค๋ฉด์ ํด๋น ์ธ๋ฑ์ค๋ก ์ฌ๋ผ์ด๋ ์ด๋ํฉ๋๋ค.
import { Banner as IBanner } from '@/models/banner.model';
import styled from 'styled-components';
interface Props {
banner: IBanner;
}
export default function BannerItem({ banner }: Props) {
return (
<StyledBannerItem>
<div className='img'>
<img src={banner.image} alt={banner.title} />
</div>
<div className='content'>
<h2>{banner.title}</h2>
<p>{banner.description}</p>
</div>
</StyledBannerItem>
);
}
const StyledBannerItem = styled.div`
flex: 0 0 100%;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
position: relative;
.img {
img {
width: 100%;
max-width: 100%;
}
}
.content {
position: absolute;
top: 0;
left: 0;
width: 60%;
height: 100%;
background: linear-gradient(
to right,
rgba(255, 255, 255, 1),
rgba(255, 255, 255, 0)
);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
h2 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 1rem;
color: ${({ theme }) => theme.color.primary};
}
p {
font-size: 1.2rem;
color: ${({ theme }) => theme.color.text};
margin: 0;
}
}
@media ${({ theme }) => `screen and ${theme.mediaQuery.mobile}`} {
.content {
width: 100%;
background: linear-gradient(
to top,
rgba(255, 255, 255, 1),
rgba(255, 255, 255, 0)
);
h2 {
font-size: 1.5rem;
margin: 8px;
}
p {
font-size: 0.75rem;
}
}
}
`;
import { fetchBanners } from '@/api/banner.api';
import { fetchBestBooks, fetchBooks } from '@/api/books.api';
import { fetchReviewAll } from '@/api/review.api';
import { Banner } from '@/models/banner.model';
import { Book, BookReviewItem } from '@/models/book.model';
import { useEffect, useState } from 'react';
export const useMain = () => {
const [reviews, setReviews] = useState<BookReviewItem[]>([]);
const [newBooks, setNewBooks] = useState<Book[]>([]);
const [bestBooks, setBestBooks] = useState<Book[]>([]);
const [banners, setBanners] = useState<Banner[]>([]);
useEffect(() => {
fetchReviewAll().then((reviews) => {
setReviews(reviews);
});
fetchBooks({
category_id: undefined,
news: true,
currentPage: 1,
limit: 4,
}).then(({ books }) => {
setNewBooks(books);
});
fetchBestBooks().then((books) => setBestBooks(books));
fetchBanners().then((banners) => setBanners(banners));
}, []);
return { reviews, newBooks, bestBooks, banners };
};
๋ฉ์ธ ํ์ด์ง์ ํ์ํ ๋ฐ์ดํฐ ๋ฆฌ๋ทฐ, ์ ๊ฐ, ๋ฒ ์คํธ์
๋ฌ, ๋ฐฐ๋๋ฅผ useEffect ํ ๋ฒ์ผ๋ก ๋ถ๋ฌ์ค๊ณ , ๊ฐ๊ฐ useState์ ์ ์ฅํด์ ์ปดํฌ๋ํธ์์ ์ฝ๊ฒ ์ฌ์ฉํ ์ ์๋๋ก ํด์ฃผ๋ ๋ฉ์ธ ํ์ด์ง ์ปค์คํ
ํ
์
๋๋ค.
import Title from '@/components/common/Title';
import MainNewBooks from '@/components/main/MainNewBooks';
import { useMain } from '@/hooks/useMain';
import styled from 'styled-components';
import MainReview from '@/components/main/MainReview';
import MainBest from '@/components/main/MainBest';
import Banner from '@/components/common/banner/Banner';
export default function Home() {
const { reviews, newBooks, bestBooks, banners } = useMain();
return (
<StyledHome>
<Banner banners={banners} />
<section className='section'>
<Title size='large'>๋ฒ ์คํธ ์
๋ฌ</Title>
<MainBest books={bestBooks} />
</section>
<section className='section'>
<Title size='large'>์ ๊ฐ ์๋ด</Title>
<MainNewBooks books={newBooks} />
</section>
<section className='section'>
<Title size='large'>๋ฆฌ๋ทฐ</Title>
<MainReview reviews={reviews} />
</section>
</StyledHome>
);
}
const StyledHome = styled.div`
display: flex;
flex-direction: column;
gap: 24px;
`;
๋ฐฐ๋, ๋ฒ ์คํธ์ ๋ฌ, ์ ๊ฐ, ๋ฆฌ๋ทฐ๋ก ํ ํ๋ฉด์ ๊ตฌ์ฑํฉ๋๋ค.

export type MediaQuery = 'mobile' | 'tablet' | 'desktop';
export interface Theme {
...
mediaQuery: {
[key in MediaQuery]: string;
};
}
export const light: Theme = {
...
mediaQuery: {
mobile: '(max-width: 768px)',
tablet: '(max-width: 1024px)',
desktop: '(min-width: 1025px)',
},
};
MediaQuery๋ผ๋ ์ ๋์ธ ํ์
์ผ๋ก ๋ชจ๋ฐ์ผ ๋ฐ์ํ ํ์
์ ์ง์ ํ์์ต๋๋ค.import { getTheme } from '@/style/theme';
import { useEffect, useState } from 'react';
export const useMediaQuery = () => {
const [isMobile, setIsMobile] = useState(
window.matchMedia(getTheme('light').mediaQuery.mobile).matches
);
useEffect(() => {
const isMobileQuery = window.matchMedia(
getTheme('light').mediaQuery.mobile
);
setIsMobile(isMobileQuery.matches);
}, []);
return { isMobile };
};
๋ฐ์ํ ์ฒดํฌ์ฉ ์ปค์คํ
ํ
์ผ๋ก, ํ์ฌ ํ๋ฉด์ด ๋ชจ๋ฐ์ผ ์ฌ์ด์ฆ์ธ์ง ํ์ธํ์ฌ boolean ๊ฐ์ผ๋ก ๋ฐํํด์ค๋๋ค.
const FooterStyle = styled.footer`
display: flex;
justify-content: space-between;
@media ${({ theme }) => `screen and ${theme.mediaQuery.mobile}`} {
flex-direction: column;
align-items: center;
}
`
๋ฏธ๋์ด ์ฟผ๋ฆฌ๋ฅผ ํ์ฉํ์ฌ ์คํฌ๋ฆฐ ์ฌ์ด์ฆ๊ฐ (max-width: 768px) ์ฆ, ์ต๋ ๊ฐ๋ก ๋์ด๊ฐ 768px์ผ ๋ ์คํ์ผ์ด ์ ์ฉ๋ฉ๋๋ค.
[ํด์๋ ์ฝ๋์ ๋ชจ์ต]
@media screen and (max-width: 768px) {
flex-direction: column;
align-items: center;
}

Mock ์๋ฒ๋ฅผ ํ ๋ฒ ๋ ๊ฒฝํํ๋ฉด์ ์กฐ๊ธ ๋ ์ต์ํด์ง ๊ฒ ๊ฐ๊ณ , mediaQuery ๋ํ ์ปค์คํ ํ ์ผ๋ก ์ ์ฉํ๊ฒ ์ธ ์ ์๋ ๋ฐฉ๋ฒ์ ์๊ฒ ๋์๋ค.