티스토리 뷰
💥 리뷰 섹션
1. MainReview.tsx
import { 생략 }
import Slider from "react-slick"
import "slick-carousel/slick/slick.css"
import "slick-carousel/slick/slick-theme.css"
interface Props {
reviews: IBookReviewItem[]
}
function MainReview({ reviews }: Props) {
const sliderSettings = {
dots: true,
infinite: true,
speed: 500,
slidesToShow: 3,
slidesToScroll: 3,
gap: 16,
}
return (
<MainReviewStyle>
<Slider {...sliderSettings}>
{reviews.map((review) => (
<BookReviewItem key={review.id} review={review} />
))}
</Slider>
</MainReviewStyle>
)
}
const MainReviewStyle = styled.div`
{ 생략 }
`
export default MainReview
2. useMain.ts
import { fetchReviewAll } from "@/api/review.api"
import { BookReviewItem } from "@/models/book.model"
import { useEffect, useState } from "react"
export const useMain = () => {
const [reviews, setReviews] = useState<BookReviewItem[]>([])
useEffect(() => {
fetchReviewAll().then((reviews) => {
setReviews(reviews)
})
}, [])
return { reviews }
}
3. review.ts / browser.ts
- review.ts 코드 추가
export const reviewForMain = http.get("http://localhost:9999/reviews", () => {
return HttpResponse.json(mockReviewData, {
status: 200
})
})
- browser.ts 에 reviewForMain 추가
const handlers = [reviewsById, addReview, reviewForMain]
4. review.api.ts 코드 추가
export const fetchReviewAll = async () => {
return await requestHandler<BookReviewItem>("get", "/reviews")
}
5. Home.tsx
import MainReview from "@/components/main/MainReview"
import { useMain } from "@/hooks/useMain"
import styled from "styled-components"
function Home() {
const { reviews } = useMain()
return (
<HomeStyle>
{/* 배너 */}
{/* 베스트셀러 */}
{/* 신간 */}
{/* 리뷰 */}
<MainReview reviews={reviews} />
</HomeStyle>
)
}
const HomeStyle = styled.div``
export default Home
리뷰 섹션은 Slick 이라는 라이브러리를 사용해 Slider 를 import 해서 사용했다.
Slider 는 우리가 흔히 사용하는 Slide 와 같은 효과이다.
마우스를 눌러 드래그 하면 화면이 넘어가는 효과같은 것이다.
sliderSettings 라는 변수를 만들어서 Slider 세팅을 해주었다.
dots 는 넘어가는 화면의 갯수를 점 으로 나타내 준다.
infinite 는 마지막 화면에 도달했을 때, 멈추게 할 것인 지 계속 이어지게 할 것인 지 고를 수 있다.
slidesToShow 는 한 번에 몇 개가 보여지게 설정할 건지에 대한 내용이다.
slidesToScroll 은 한 번에 몇 개가 슬라이드 되게 할 건지에 대한 내용이다.
둘 다 3 으로 넣어놨기 때문에, 3개씩 보여지고 3개가 한 번에 넘어가게 된다.
그리고 이 sliderSettings 를 Slider 컴포넌트에 스프레드 방법으로 넣어준다.
Slick 라이브러리를 사용하면 자동으로 클래스가 주어지게 되는데, 슬라이드 양 옆에 있는 버튼 클래스가 생긴다.
.slick-prev:before, .slick-next:before 이다.
css 부분은 { 생략 } 으로 적어놨지만 이 클래스에 color: #777 을 따로 넣어주었다.
그럼 이렇게 3개씩 보여지고 3개가 한꺼번에 넘어가게 되는 슬라이드가 만들어진다.
양 옆에 화살표 모양의 버튼도 잘 나와있는 걸 볼 수 있다.
💥 신간 섹션
1. MainNewBooks.tsx
import { 생략 }
interface Props {
books: Book[]
}
function MainNewBooks({ books }: Props) {
return (
<MainNewBooksStyle>
{books.map((book) => (
<BookItem key={book.id} book={book} view="grid" />
))}
</MainNewBooksStyle>
)
}
const MainNewBooksStyle = styled.div`
{ 생략 }
`
export default MainNewBooks
2. useMain.ts
import { 생략 }
export const useMain = () => {
const [reviews, setReviews] = useState<BookReviewItem[]>([])
const [newBooks, setNewBooks] = useState<Book[]>([])
useEffect(() => {
fetchReviewAll().then((reviews) => {
setReviews(reviews)
})
fetchBooks({
category_id: undefined,
news: true,
currentPage: 1,
limit: 4,
}).then(({ books }) => {
setNewBooks(books)
})
}, [])
return { reviews, newBooks }
}
3. Home.tsx
신간 섹션은 MainNewBooks 이라는 파일을 새로 만들어서 작성했다.
books 를 순회하면서 book 아이템을 띄워주게 하였고,
books api 의 fetchBooks 를 사용해서 useMain 에 fetchBooks 를 작성해주었다.
그리드 형태로 4개씩 1줄로 나타나게 grid-template-columns 를 주었다.
그리고 Home.tsx 에서 4가지의 섹션을 나타내기 위해 배너를 제외한 나머지는 section 으로 묶어주었다.
그럼 이렇게 4개씩 1줄의 형태로 신간 안내가 생긴 걸 볼 수 있다.
지금은 데이터에 신간이 1개밖에 없어서 1개만 뜨지만, 저 모양 그대로 3개가 더 생길 수 있는 공간이 있는 걸 볼 수 있다.
💥 베스트 섹션
1. BookBestItem.tsx
import { Book } from "@/models/book.model"
import styled from "styled-components"
import BookItem, { BookItemStyle } from "./BookItem"
interface Props {
book: Book
itemIndex: number
}
function BookBestItem({ book, itemIndex }: Props) {
return (
<BookBestItemStyle>
<BookItem book={book} view="grid" />
<div className="rank">{itemIndex + 1}</div>
</BookBestItemStyle>
)
}
const BookBestItemStyle = styled.div`
${BookItemStyle} {
.summary,
.price,
.likes {
display: none
}
...
`
export default BookBestItem
2. books.ts
import { Book } from "@/models/book.model"
import { http, HttpResponse } from "msw"
import { fakerKO as faker } from "@faker-js/faker"
const bestBooksData: Book[] = Array.from({ length: 10 }).map((item, 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
})
})
3. browser.ts 에 bestBooks 추가
4. books.api.ts 코드 추가
export const fetchBestBooks = async () => {
const response = await httpClient.get<Book[]>("/books/best")
return response.data
}
5. useMain.ts 에 코드 추가
import { 생략 }
export const useMain = () => {
const [reviews, setReviews] = useState<BookReviewItem[]>([])
const [newBooks, setNewBooks] = useState<Book[]>([])
const [bestBooks, setBestBooks] = useState<Book[]>([])
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)
})
}, [])
return { reviews, newBooks, bestBooks }
}
6. MainBest.tsx
import { 생략 }
interface Props {
books: Book[]
}
function MainBest({ books }: Props) {
return (
<MainBestStyle>
{books.map((item, index) => (
<BookBestItem key={item.id} book={item} itemIndex={index} />
))}
</MainBestStyle>
)
}
const MainBestStyle = styled.div`
{ 생략 }
`
export default MainBest
7. Home.tsx 에 '베스트 셀러' 섹션에 코드 추가
<MainBest books={bestBooks}/>
BookBestItem 은 이전에 만들었던 BookItem 컴포넌트를 사용했다.
css 역시 BookItemStyle 을 가져와서 사용했기 때문에 ${BookItemStyle} 이라고 적어서 사용했다.
이전에 사용해봤던 faker 를 사용해서 데이터들을 알아서 만들어서 나오게 하였다.
faker 를 이용해서 title 을 만들었더니 너무 길게 나와서..
h2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
이 코드를 추가하여, 2줄만 나오게하고 말줄임표(...)로 마무리하게끔 만들었다.
💥 배너 섹션
1. Banner.tsx
import { 생략 }
interface Props {
banners: IBanner[]
}
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 (
<BannerStyle>
<BannerContainerStyle $transFormValue={transFormValue} >
{banners.map((item, index) => (
<BannerItem banner={item} />
))}
</BannerContainerStyle>
<BannerButtonStyle>
<button className="prev" onClick={handlePrev}>
<FaAngleLeft />
</button>
<button className="next" onClick={handleNext}>
<FaAngleRight />
</button>
</BannerButtonStyle>
<BannerIndicatoreStyle>
{banners.map((_, index) => (
<span onClick={() => handleIndicatorClick(index)}
className={index === currentIndex ? "active" : ""}>
</span>
))}
</BannerIndicatoreStyle>
</BannerStyle>
)
}
const BannerStyle = styled.div`
{ 생략 }
`
interface BannerContainerStyleProps {
$transFormValue: number
}
const BannerContainerStyle = styled.div<BannerContainerStyleProps>`
display: flex;
transform: translateX(${(props) => props.$transFormValue}%);
transition: transform 0.5s ease-in-out;
`
const BannerButtonStyle = styled.div`
{ 생략 }
`
const BannerIndicatoreStyle = styled.div`
{ 생략 }
`
export default Banner
2. BannerItem.tsx
import { 생략 }
interface Props {
banner: IBanner
}
function BannerItem({ banner }: Props) {
return (
<BannerItemStyle>
<div className="img">
<img src={banner.image} alt={banner.title} />
</div>
<div className="content">
<h2>{banner.title}</h2>
<p>{banner.description}</p>
</div>
</BannerItemStyle>
)
}
const BannerItemStyle = styled.div`
{ 생략 }
`
export default BannerItem
3. banner.ts
import { http, HttpResponse } from "msw"
import { Banner } from "@/models/banner.model"
const bannersData: Banner[] = [
{
id: 1,
title: "배너 1 제목",
description: "Banner 1 description",
image: "https://picsum.photos/id/111/1200/400",
url: "http://some.url",
target: "_blank",
},
{
id: 2,
title: "배너 2 제목",
description: "Banner 2 description",
image: "https://picsum.photos/id/222/1200/400",
url: "http://some.url",
target: "_self",
},
{
id: 3,
title: "배너 3 제목",
description: "Banner 3 description",
image: "https://picsum.photos/id/33/1200/400",
url: "http://some.url",
target: "_blank",
},
]
export const banners = http.get("http://localhost:9999/banners", () => {
return HttpResponse.json(bannersData, {
status: 200
})
})
4. banner.api.ts
import { Banner } from "@/models/banner.model";
import { requestHandler } from "./http";
export const fetchBanners = async () => {
return await requestHandler<Banner[]>("get", "/banners")
}
5. useMain.ts 에 코드 추가 (fetchBanners)
import { 생략 }
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 }
}
6. browser.ts 에 banner 추가
7. banners.model.ts
export interface Banner {
id: number
title: string
description: string
image: string
url: string
target: string
}
8. Home.tsx 에 코드 추가
import { 생략 }
function Home() {
const { reviews, newBooks, bestBooks, banners } = useMain()
return (
<HomeStyle>
<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>
</HomeStyle>
)
}
const HomeStyle = styled.div`
{ 생략 }
`
export default Home
배너는 banner.model 과 banner 로 쓸 dummy data 가 들어있는 banner.ts 그리고 banner api 를 새로 만들었다.
banner api 로 만든 fetchBanners 를 useMain 에 만들고 browser.ts 에서 banners 를 가져와 사용한다.
배너는 보통 슬라이드 형식으로 많이 쓰는데, 리뷰 섹션과는 다르게 transform 을 사용하여 만들었다.
3개의 사진을 display: flex 로 가로로 만든 후, 크기를 맞춰주고 버튼을 누를 때 마다, 0, -100%, -200% 이 되게 만들었다.
그럼 이렇게 1번 사진은 translateX 가 0 이고, 2번 사진은 -100%, 3번 사진은 -200% 이 된다.
배너 아래에는 인디케이터 라는 버튼을 만들어 놓았고, 이 인디케이터 역시 누르면 해당 번호와 같은 사진으로 슬라이드 한다.
그리고 index와 currentIndex 가 같으면 active 를 부여하는 삼항식 코드 역시 작성했다.
💥 모바일 대응 (= 반응형 웹)
- 반응형 웹 제작 시 주의할 것들
1) viewport
2) 상대값을 가진 레이아웃
3) 화면 너비 감지(mediaquery)
모바일 대응으로 반응형 스타일을 바꿔보았다.
디자인을 한 사람으로써 진짜 스트레스 받는 것 중에 하나가 반응형 웹 이다.....
개인적으로 디자인을 엄청 좋아하는 편도 아니지만.. 막상 하면 예뻐지는 페이지가 꽤나 맘에 드는...ㅎ
근데도 반응형은 할 때마다 스트레스를 받았다 ㅠㅠ..
그래도 이번 book-shop 은 엄청 큰 프로젝트가 아니라서 금방금방 한 것 같다.
우선 5개 4개씩 나열됐던 베스트, 신간 리스트들은 다 2개로 나열했다.
모바일은 보통 많아도 3개까지만 나열하는 게 좋다고 알고있다.
보통 375px 이하면 모바일로 본다. 요즘 375px 이하인 폰이 있긴 한가 싶은데...
반응형 웹 디자인은 보통 media screen 을 이용해서 @media screen and max-width: 375px { } 이렇게 작성한다.
우린 만들어둔 theme.ts 에다가 mediaQuery 항목을 만들어서, mobile 버전 tablet 버전 desktop 버전 크기를 맞춰놨다.
mobile 과 tablet 은 max-width 를 쓰고, desktop 은 min-width 를 쓴다.
이번 모바일 대응을 하면서 새롭게 알게 된 것 중에 하나가 inputMode 였다.
말 그대로 input 태그에 쓰는 옵션인데, Desktop은 상관이 없지만 Mobile 로 접근하게 되면
inputMode 에 따라서 화면에 나타나는 키보드가 다르다.
가끔 로그인이나 비밀번호를 칠 때, 오 얘는 바로 숫자가 뜨네? 얘는 이메일에 딱 좋게 뜨네?
라고 생각해본 적이 있는데 그게 이거였던 것 같다.
이전에 배워보지 않은 부분을 배우는 것 만큼 확실히 흥미로운 건 없는 것 같다 ㅋㅋㅋ...
이번 book-shop 이 드디어 끝났다..
개인적으로 나머지도 다 고쳐보고싶긴하다 🤣
'웹 개발 공부하기' 카테고리의 다른 글
[02.17] 오픈소스2 (1) | 2025.02.20 |
---|---|
[02.14] 오픈소스 (0) | 2025.02.19 |
[02.11] 다양한 UI 경험 (3) | 2025.02.17 |
[02.11] 도서 상세 - 리뷰 (2) | 2025.02.17 |
[02.11] 모킹 서버 (1) | 2025.02.17 |