08. Section과 Card — 영화 카드 목록 만들기
요약
Gemini CLI로 구현하기 — Section & Card 컴포넌트
npx gemini-
1src/components/Section.jsx와 Card.jsx를 작성해줘. Section은 title, items, category props를 받아서 제목, 더보기 링크(/category/[category]), [열 수]열 그리드 카드 목록을 렌더링해줘. Card는 item prop을 받아서 TMDB 포스터 이미지(https://image.tmdb.org/t/p/w500/), 호버 시 줄거리+평점 오버레이, 하단에 제목+평점+개봉일을 표시해줘. Tailwind CSS group-hover를 활용해줘.
- 사용 가이드:
[열 수]를 원하는 그리드 열 개수(기본: 4)로 바꾼다. 카드에 기능을 추가하려면 프롬프트 끝에 요구사항을 덧붙인다.
1. 이번 편에서 만들 것
| 컴포넌트 | 역할 |
|---|---|
Section | 제목 + “더보기” 링크 + 영화 카드 그리드 |
Card | 포스터 + 호버 시 줄거리/평점 오버레이 + 하단 정보 |
2. Section.jsx 작성
src/components/Section.jsx 파일을 열고 임시 코드를 모두 지운 뒤 아래를 작성한다. Section은 제목, “더보기” 링크, 카드 그리드를 하나의 단위로 묶는 레이아웃 컴포넌트이다. title과 items prop만 바꿔서 세 가지 카테고리에 재사용한다.
1import { Link } from "react-router";2import { Card } from "./Card.jsx";3import { Container } from "./UI.jsx";4
5export function Section({ title, items, category }) {6 return (7 <Container className="py-24">8 <div className="flex items-center justify-between pt-10 pb-5 px-3">9 <h2 className="text-4xl font-bold text-white">{title}</h2>10 {category && (11 <Link to={`/category/${category}`} className="text-yellow-400 hover:text-yellow-300 text-sm font-bold">12 더보기 →13 </Link>14 )}15 </div>16 <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-6">17 {items.map((el) => (18 <Card key={el.id} item={el} />19 ))}20 </div>21 </Container>22 );23}| 줄 | 설명 |
|---|---|
| 3 | 4편에서 만든 Container 컴포넌트를 가져온다. 공통 레이아웃 컴포넌트이다. |
| 5 | title(제목), items(영화 배열), category(카테고리 키)를 받는다. |
| 8 | flex — 자식 요소를 가로로 나란히 배치하는 Tailwind 클래스이다. justify-between — 자식들을 양쪽 끝으로 밀어낸다. 결과적으로 제목은 왼쪽, “더보기”는 오른쪽에 배치된다. |
| 10-14 | category가 있을 때만 “더보기” 링크를 표시한다. 클릭하면 /category/popular 같은 페이지로 이동한다. →는 → 화살표이다. |
| 16 | grid — CSS Grid 레이아웃이다. grid-cols-4는 4열 구성이다. gap-6은 그리드 아이템 사이의 간격이다. sm:, md: 반응형 접두사로 뷰포트 크기에 따라 열 수가 변경된다. |
| 17-19 | .map()(맵)은 배열의 각 요소에 콜백 함수를 실행하고, 그 반환값으로 구성된 새 배열을 만드는 메서드이다. items에 영화가 20개 있으면 Card 컴포넌트 20개로 이루어진 새 배열이 만들어지고, React가 이를 화면에 렌더링한다. |
Home.jsx에서는 category="popular" 같은 값을 전달하지만, 나중에 다른 곳에서 Section을 “더보기” 없이 사용할 수도 있다. {category && (...)}로 처리하면 category가 없으면 “더보기” 링크가 자동으로 숨겨진다.
3. Card.jsx — 1단계: import + 포스터
src/components/Card.jsx 파일을 열고 임시 코드를 모두 지운 뒤 작성한다. Card는 영화 포스터, 호버 오버레이(줄거리·평점), 하단 정보를 표시하는 컴포넌트이다. 클릭하면 영화 상세 페이지로 이동한다.
1import { Link } from "react-router";2import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";3import { faHeart, faStar } from "@fortawesome/free-solid-svg-icons";4
5export function Card({ item }) {6 const poster = `https://image.tmdb.org/t/p/w500/${item.poster_path}`;| 줄 | 설명 |
|---|---|
| 3 | 하트(faHeart)와 별(faStar) 아이콘을 가져온다. |
| 5 | props 이름이 item이다. (이전 교안에서는 movie였다.) |
| 6 | TMDB 포스터 이미지 URL을 조합한다. |
4. Card.jsx — 2단계: JSX 렌더링
- 포스터 + 호버 오버레이
- 카드 하단 정보
1 return (2 <div className="card py-10 group">3 <Link to={`/movie/${item.id}`}>4 <div className="relative overflow-hidden rounded-md aspect-[2/3]">5 <img6 className="object-cover w-full h-full transition-transform duration-300 group-hover:scale-110"7 src={poster}8 alt={item.title}9 />10 <div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex flex-col justify-end p-4">11 <p className="text-white text-sm line-clamp-3">{item.overview}</p>12 <div className="flex items-center gap-1 mt-2 text-yellow-400">13 <FontAwesomeIcon icon={faStar} className="text-xs" />14 <span className="text-sm font-bold">{(item.vote_average || 0).toFixed(1)}</span>15 </div>16 </div>17 </div>| 줄 | 설명 |
|---|---|
| 2 | group 클래스를 선언한다. 자식 요소에서 group-hover:를 사용할 수 있다. |
| 6 | group-hover:scale-110 — 카드에 마우스를 올리면 이미지가 1.1배 확대된다. |
| 10 | 호버 오버레이 — absolute inset-0은 top:0, right:0, bottom:0, left:0을 한 번에 설정하는 Tailwind 단축 클래스이다. 부모 요소를 완전히 덮는다. 평소에는 opacity-0(투명), 마우스를 올리면 group-hover:opacity-100(표시)으로 전환된다. |
| 11 | line-clamp-3은 줄거리를 3줄까지만 표시하고 나머지는 말줄임표로 처리한다. |
| 14 | (item.vote_average || 0).toFixed(1) — ||(OR 연산자)는 왼쪽이 거짓 값이면 오른쪽을 반환한다. 평점이 없으면 0으로 대체하고, .toFixed(1)로 소수점 1자리로 고정한다. |
1 <div className="flex flex-col px-1 mt-2">2 <h4 className="text-white text-xl font-bold truncate">3 {item.title}4 </h4>5 <span className="flex items-center gap-2 font-bold text-yellow-500">6 <FontAwesomeIcon icon={faHeart} />7 <span>{(item.vote_average || 0).toFixed(1)}</span>8 <span className="font-medium text-gray-400">9 {item.release_date}10 </span>11 </span>12 </div>13 </Link>14 </div>15 );16}| 줄 | 설명 |
|---|---|
| 2 | truncate로 긴 제목을 말줄임표(...)로 처리한다. |
| 6 | 하트 아이콘 + 평점 + 개봉일을 노란색/회색으로 표시한다. |
1(8.432).toFixed(1) // "8.4"2(10).toFixed(1) // "10.0"3(0).toFixed(1) // "0.0"숫자를 소수점 N자리까지만 표시하는 메서드이다. TMDB 평점은 8.432 같은 값이 오므로, toFixed(1)로 "8.4"처럼 깔끔하게 표시한다.
텍스트를 최대 3줄까지만 표시하고 나머지는 ...으로 잘라내는 Tailwind 클래스이다. 줄거리가 아무리 길어도 카드 레이아웃이 깨지지 않는다.
5. 동작 확인
| 확인 항목 | 기대 결과 |
|---|---|
| 메인 페이지 스크롤 | 영화 포스터가 4열 그리드로 나열됨 |
| 카드에 마우스 올림 | 포스터가 확대되고, 어두운 오버레이에 줄거리+평점 표시 |
| ”더보기 →” 클릭 | /category/popular 등 카테고리 페이지로 이동 |
| 카드 클릭 | /movie/숫자로 이동 |
| 모바일 크기 | 1열 → 2열 → 4열로 반응형 변화 |
6. 전체 코드
- Section.jsx
- Card.jsx
1import { Link } from "react-router";2import { Card } from "./Card.jsx";3import { Container } from "./UI.jsx";4
5export function Section({ title, items, category }) {6 return (7 <Container className="py-24">8 <div className="flex items-center justify-between pt-10 pb-5 px-3">9 <h2 className="text-4xl font-bold text-white">{title}</h2>10 {category && (11 <Link to={`/category/${category}`} className="text-yellow-400 hover:text-yellow-300 text-sm font-bold">12 더보기 →13 </Link>14 )}15 </div>16 <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-6">17 {items.map((el) => (18 <Card key={el.id} item={el} />19 ))}20 </div>21 </Container>22 );23}1import { Link } from "react-router";2import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";3import { faHeart, faStar } from "@fortawesome/free-solid-svg-icons";4
5export function Card({ item }) {6 const poster = `https://image.tmdb.org/t/p/w500/${item.poster_path}`;7
8 return (9 <div className="card py-10 group">10 <Link to={`/movie/${item.id}`}>11 <div className="relative overflow-hidden rounded-md">12 <img13 className="object-cover w-full h-full transition-transform duration-300 group-hover:scale-110"14 src={poster}15 alt={item.title}16 />17 <div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex flex-col justify-end p-4">18 <p className="text-white text-sm line-clamp-3">{item.overview}</p>19 <div className="flex items-center gap-1 mt-2 text-yellow-400">20 <FontAwesomeIcon icon={faStar} className="text-xs" />21 <span className="text-sm font-bold">{(item.vote_average || 0).toFixed(1)}</span>22 </div>23 </div>24 </div>25 <div className="flex flex-col px-1 mt-2">26 <h4 className="text-white text-xl font-bold truncate">27 {item.title}28 </h4>29 <span className="flex items-center gap-2 font-bold text-yellow-500">30 <FontAwesomeIcon icon={faHeart} />31 <span>{(item.vote_average || 0).toFixed(1)}</span>32 <span className="font-medium text-gray-400">33 {item.release_date}34 </span>35 </span>36 </div>37 </Link>38 </div>39 );40}