
지난 시간에 이어 북스토어 프로젝트의 메인 화면을 구성하고 모바일 환경에 맞게 반응형을 적용했습니다.
메인 화면 상단의 배너는 리액트 기본 기능을 활용해 직접 슬라이드 형태로 구현했습니다. CSS의 transform: translate() 속성과 현재 보여줄 배너 데이터의 인덱스 상태를 조합하면 이미지를 옆으로 이동시킬 수 있습니다.
const [currentIndex, setCurrentIndex] = useState(0);
const handleNext = () => {
setCurrentIndex((prev) => (prev + 1) % banners.length);
};
// 오른쪽 버튼 클릭 시 왼쪽으로 이동하므로 음수(-)를 적용합니다
const BannerWrap = styled.div<{ $currentIndex: number }>`
display: flex;
transition: transform 0.4s ease;
transform: translateX(${({ $currentIndex }) => $currentIndex * -100}%);
`;
배너를 불러올 백엔드 API가 아직 없기 때문에 MSW를 활용해 모킹 서버에서 가짜 데이터를 받아오도록 처리했습니다. 이전 시간에 MSW를 세팅해둔 덕분에 API 없이도 자연스럽게 개발을 이어갈 수 있었습니다.
추천 도서 영역은 기존에 만들어둔 BookItem 컴포넌트를 재사용하여 구현했습니다. styled-components 를 사용하면 기존 컴포넌트의 스타일 객체를 export 하여 다른 파일에서 선택자로 접근할 수 있습니다.
이를 활용해 추천 도서 목록에서는 불필요한 요약, 가격, 좋아요 수를 숨기도록 스타일을 덮어씌웠습니다.
// BookItem.tsx — 스타일 객체를 export합니다
export const BookItemStyle = styled.div`
/* BookItem 기본 스타일 */
`;
// BookBestItem.tsx — BookItemStyle을 선택자로 활용해 스타일을 덮어씁니다
const BookBestItemStyle = styled.div`
${BookItemStyle} {
.summary,
.price,
.likes {
display: none;
}
h2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
}
`;
function BookBestItem({ book, itemIndex }: Props) {
return (
<BookBestItemStyle>
<BookItem book={book} view="grid" />
<div className="rank">{itemIndex + 1}</div>
</BookBestItemStyle>
);
}
컴포넌트를 새로 만들지 않고 기존 컴포넌트를 재사용하면서 필요한 부분만 스타일로 제어할 수 있어 코드 중복을 줄이고 유지보수도 쉬워집니다.
배너에서는 슬라이더를 직접 구현했다면, 리뷰 목록은 react-slick 과 slick-carousel 라이브러리를 활용해 캐러셀(Carousel) 형태로 구현했습니다.
npm install react-slick slick-carousel
npm install -D @types/react-slick
캐러셀은 하나의 한정된 영역 안에 여러 개의 콘텐츠를 교차해서 표시할 수 있는 UI 컴포넌트입니다. 검증된 라이브러리를 사용하면 스와이프 기능이나 무한 반복 등 복잡한 슬라이더 로직을 쉽게 처리할 수 있습니다.
import Slider from 'react-slick';
import 'slick-carousel/slick/slick.css';
import 'slick-carousel/slick/slick-theme.css';
const settings = {
dots: true,
infinite: true,
speed: 500,
slidesToShow: 3,
slidesToScroll: 1,
responsive: [
{
breakpoint: 768,
settings: { slidesToShow: 1 },
},
],
};
function ReviewCarousel({ reviews }: Props) {
return (
<Slider {...settings}>
{reviews.map((review) => (
<ReviewItem key={review.id} review={review} />
))}
</Slider>
);
}
직접 구현과 라이브러리 활용 두 가지 방식을 모두 경험해보니, 간단한 슬라이더는 직접 구현하고 복잡한 인터랙션이 필요한 경우에는 라이브러리를 활용하는 것이 효율적이라는 것을 느꼈습니다.
다양한 기기 크기에 맞춰 최적화된 화면을 제공하기 위해 반응형 웹 디자인을 적용했습니다. 반응형 구현을 위해 고려해야 할 세 가지 주요 요소가 있습니다.
| 요소 | 설명 |
|---|---|
| 뷰포트(Viewport) | meta 태그로 모바일 기기에서 화면 배율을 제어합니다 |
| 상대 길이 단위 | px 대신 %, vw, vh 등 화면 크기에 따라 유연하게 변하는 단위를 사용합니다 |
| 미디어 쿼리(Media Query) | 특정 화면 너비의 분기점(Breakpoint)을 감지해 레이아웃을 다르게 적용합니다 |
// theme.ts — 브레이크포인트를 테마에서 관리합니다
export const mediaQuery = (maxWidth: number) =>
`@media (max-width: ${maxWidth}px)`;
export const media = {
mobile: mediaQuery(768),
tablet: mediaQuery(1024),
};
// 컴포넌트에서 사용
const LayoutStyle = styled.div`
max-width: 1020px;
padding: 0 20px;
${media.mobile} {
padding: 0 12px;
}
`;
폼을 구성할 때 input 태그의 inputMode 속성을 명시하면, 모바일 브라우저에서 입력 필드 성격에 맞는 가상 키보드를 띄워줘 사용자 경험을 크게 개선할 수 있습니다.
{/* 숫자 패드 */}
<input type="text" inputMode="numeric" placeholder="수량 입력" />
{/* 이메일 키보드 */}
<input type="text" inputMode="email" placeholder="이메일 입력" />
작은 속성 하나지만 모바일 사용자 입장에서는 체감 편의성이 크게 달라지는 부분입니다.