티스토리 뷰

💥 리뷰 섹션

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
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
글 보관함