실습: Flexbox로 카드형 레이아웃 만들기 완벽 가이드
카드형 레이아웃은 현대 웹 디자인에서 가장 널리 사용되는 UI 패턴 중 하나입니다. 제품 목록, 블로그 포스트, 팀원 소개 등 다양한 콘텐츠를 깔끔하고 일관된 방식으로 표시할 수 있어 사용자 경험을 크게 향상시킵니다. 이번 실습에서는 CSS Flexbox를 활용하여 반응형 카드 레이아웃을 처음부터 만드는 방법을 단계별로 알아보겠습니다.
완성 미리보기
우리가 이번 실습을 통해 만들 카드형 레이아웃의 최종 결과물은 다음과 같습니다:
- 균일한 크기의 카드 컴포넌트
- 그리드처럼 정렬된 카드들
- 반응형(모바일, 태블릿, 데스크톱 지원)
- 이미지, 제목, 설명, 버튼을 포함한 카드 디자인
- 부드러운 호버 효과
- 깔끔한 그림자 효과
모바일에서는 한 줄에 하나의 카드가, 태블릿에서는 두 개의 카드가, 데스크톱에서는 세 개 이상의 카드가 한 줄에 표시됩니다.
카드형 레이아웃의 장점
카드형 레이아웃이 인기 있는 이유는 여러 가지가 있습니다:
- 일관된 정보 구조: 모든 콘텐츠가 동일한 형식으로 표시되어 사용자가 정보를 쉽게 파악할 수 있습니다.
- 시각적 계층 구조: 각 카드는 독립적인 단위로 인식되어 정보의 구분이 명확합니다.
- 반응형 디자인에 적합: 화면 크기에 따라 카드의 배치를 쉽게 조정할 수 있습니다.
- 터치 친화적: 모바일 환경에서 카드는 터치하기 좋은 크기로 설계할 수 있습니다.
- 확장성: 콘텐츠가 추가되어도 레이아웃의 일관성을 유지할 수 있습니다.
HTML 구조 작성
먼저, 카드 레이아웃을 위한 기본 HTML 구조를 작성해보겠습니다. 이번 예제에서는 여행 목적지를 소개하는 카드 레이아웃을 만들어 보겠습니다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>여행 목적지 카드 레이아웃</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<h1 class="page-title">인기 여행 목적지</h1>
<div class="card-container">
<!-- 카드 1 -->
<div class="card">
<div class="card-image">
<img src="https://blog.kakaocdn.net/dn/o1KIw/btqu9mflPY6/rGk1mM3iugV1c6jj9Z3E80/img.jpg" alt="제주도 풍경">
</div>
<div class="card-content">
<h2 class="card-title">제주도</h2>
<p class="card-description">
아름다운 자연 경관과 독특한 문화가 어우러진 한국의 대표 휴양지입니다.
화산섬 특유의 독특한 지형과 맑은 바다가 매력적입니다.
</p>
<div class="card-footer">
<span class="card-price">350,000원~</span>
<button class="card-button">자세히 보기</button>
</div>
</div>
</div>
<!-- 카드 2 -->
<div class="card">
<div class="card-image">
<img src="https://i.namu.wiki/i/XO6E06_qrvaiZnbaLsHC-Ov1z4WI8vRqSn_R9IqHobEgHD2gYLLN3Ldi3zvnFLRYcLTQmWpRuAdsfH_2IrNNPQ.webp" alt="경주 불국사">
</div>
<div class="card-content">
<h2 class="card-title">경주</h2>
<p class="card-description">
천년 고도의 역사와 문화를 간직한 도시로, 불국사, 석굴암 등
유네스코 세계문화유산을 만나볼 수 있습니다.
</p>
<div class="card-footer">
<span class="card-price">150,000원~</span>
<button class="card-button">자세히 보기</button>
</div>
</div>
</div>
<!-- 카드 3 -->
<div class="card">
<div class="card-image">
<img src="https://i.namu.wiki/i/hkDOgJHC40yiIFKQDRz7YjHpzWrL9vCTT7mve4TF6Lj-GpGsBpvT8WlXbwOT_To1Ndl1zKrVLQ-SiwaGNFOgQA.webp" alt="부산 해운대">
</div>
<div class="card-content">
<h2 class="card-title">부산</h2>
<p class="card-description">
아름다운 해변과 활기찬 시장, 현대적인 도시 경관이 어우러진
한국 제2의 도시입니다. 해운대와 광안리는 필수 방문지입니다.
</p>
<div class="card-footer">
<span class="card-price">200,000원~</span>
<button class="card-button">자세히 보기</button>
</div>
</div>
</div>
<!-- 카드 4 -->
<div class="card">
<div class="card-image">
<img src="https://i2.wp.com/www.agoda.com/wp-content/uploads/2019/05/Gyeongbokgung-palace-Seoul-architecture-view.jpg" alt="서울 경복궁">
</div>
<div class="card-content">
<h2 class="card-title">서울</h2>
<p class="card-description">
전통과 현대가 공존하는 대한민국의 수도. 경복궁, 명동, 강남 등
다양한 매력의 장소들이 여러분을 기다립니다.
</p>
<div class="card-footer">
<span class="card-price">180,000원~</span>
<button class="card-button">자세히 보기</button>
</div>
</div>
</div>
<!-- 카드 5 -->
<div class="card">
<div class="card-image">
<img src="https://www.travelnbike.com/news/photo/201710/46420_50343_3757.jpg" alt="속초 설악산">
</div>
<div class="card-content">
<h2 class="card-title">속초/설악산</h2>
<p class="card-description">
웅장한 설악산과 동해의 푸른 바다가 만나는 곳. 신선한 해산물과
아름다운 자연 경관을 동시에 즐길 수 있습니다.
</p>
<div class="card-footer">
<span class="card-price">220,000원~</span>
<button class="card-button">자세히 보기</button>
</div>
</div>
</div>
<!-- 카드 6 -->
<div class="card">
<div class="card-image">
<img src="https://cdn.visitkorea.or.kr/img/call?cmd=VIEW&id=1a802bf2-2628-4d77-bfe5-19bef8d0ece2" alt="전주 한옥마을">
</div>
<div class="card-content">
<h2 class="card-title">전주</h2>
<p class="card-description">
전통 한옥과 맛있는 음식으로 유명한 도시. 한옥마을에서 한국의
전통 문화를 체험하고 비빔밥의 본고장을 느껴보세요.
</p>
<div class="card-footer">
<span class="card-price">130,000원~</span>
<button class="card-button">자세히 보기</button>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
이 HTML 구조에서 주목할 점:
.container
: 전체 콘텐츠를 감싸는 컨테이너.card-container
: 모든 카드를 감싸는 Flexbox 컨테이너가 될 요소.card
: 개별 카드 컴포넌트.card-image
: 카드 상단의 이미지 영역.card-content
: 카드의 텍스트 콘텐츠 영역.card-footer
: 가격과 버튼이 위치하는 카드 하단 영역
기본 CSS 설정
이제 스타일링을 시작하겠습니다. 먼저 기본적인 CSS 설정과 전체 레이아웃을 잡아보겠습니다.
/* 기본 리셋 및 폰트 설정 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Noto Sans KR', sans-serif;
line-height: 1.6;
color: #333;
background-color: #f8f9fa;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.page-title {
text-align: center;
margin-bottom: 40px;
color: #1a1a1a;
font-size: 2.5rem;
}
/* 이미지 기본 스타일 */
img {
max-width: 100%;
height: auto;
display: block;
}
이 기본 설정은 다음과 같은 역할을 합니다:
- 모든 요소의 기본 마진과 패딩을 제거
box-sizing: border-box
로 요소의 크기 계산 방식 설정- 기본 폰트와 라인 높이, 텍스트 색상 설정
- 페이지 배경색 설정
- 최대 너비가 1200px인 중앙 정렬된 컨테이너 설정
- 페이지 제목 스타일링
- 이미지가 컨테이너를 넘어가지 않도록 설정
카드 컴포넌트 스타일링
이제 개별 카드 컴포넌트의 스타일을 정의해보겠습니다.
/* 카드 기본 스타일 */
.card {
background-color: #fff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15);
}
/* 카드 이미지 영역 */
.card-image {
height: 200px;
overflow: hidden;
}
.card-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
}
.card:hover .card-image img {
transform: scale(1.05);
}
/* 카드 콘텐츠 영역 */
.card-content {
padding: 20px;
}
.card-title {
font-size: 1.5rem;
margin-bottom: 10px;
color: #1a1a1a;
}
.card-description {
color: #666;
font-size: 0.95rem;
margin-bottom: 20px;
/* 두 줄 이상이면 말줄임표(...) 표시 */
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
height: 4.8em; /* line-height(1.6) * 3줄 */
}
/* 카드 푸터 영역 */
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 15px;
border-top: 1px solid #eee;
}
.card-price {
font-weight: bold;
color: #ff6b6b;
font-size: 1.1rem;
}
.card-button {
background-color: #4dabf7;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: background-color 0.2s ease;
}
.card-button:hover {
background-color: #339af0;
}
카드 스타일의 주요 포인트:
- 기본 스타일: 흰색 배경, 둥근 모서리, 부드러운 그림자
- 호버 효과: 마우스 오버시 카드가 약간 위로 떠오르고 그림자가 강화
- 이미지 영역: 고정 높이와
object-fit: cover
로 이미지 비율 유지 - 이미지 호버 효과: 마우스 오버시 이미지가 약간 확대
- 설명 텍스트: 3줄 이상은 말줄임표(...) 처리
- 푸터 영역: Flexbox로 가격과 버튼 배치
Flexbox로 카드 배치하기
이제 Flexbox를 사용하여 카드들을 그리드 형태로 배치하겠습니다.
/* 카드 컨테이너 레이아웃 */
.card-container {
display: flex;
flex-wrap: wrap;
gap: 30px;
justify-content: center;
}
/* 기본(모바일) 카드 너비 */
.card {
width: 100%; /* 모바일에서는 한 줄에 하나의 카드 */
max-width: 400px;
}
이 코드는 다음과 같은 레이아웃을 구성합니다:
.card-container
에display: flex
를 적용하여 Flexbox 컨테이너로 만듭니다.flex-wrap: wrap
으로 카드가 한 줄에 다 들어가지 않으면 다음 줄로 넘어가도록 합니다.gap: 30px
으로 카드 사이의 간격을 설정합니다.justify-content: center
로 카드들을 중앙에 정렬합니다.- 모바일 뷰에서는 카드 너비를 100%로 설정하여 한 줄에 하나의 카드만 표시합니다.
반응형 디자인 적용
이제 미디어 쿼리를 사용하여 화면 크기에 따라 카드 레이아웃이 변하도록 설정하겠습니다.
/* 태블릿 뷰 */
@media (min-width: 768px) {
.card {
width: calc(50% - 15px); /* 한 줄에 2개 카드 (간격 고려) */
}
}
/* 데스크톱 뷰 */
@media (min-width: 1024px) {
.card {
width: calc(33.333% - 20px); /* 한 줄에 3개 카드 */
}
}
/* 대형 데스크톱 뷰 */
@media (min-width: 1440px) {
.card {
width: calc(25% - 22.5px); /* 한 줄에 4개 카드 */
}
}
반응형 디자인의 브레이크포인트:
- 768px 이상(태블릿): 한 줄에 2개의 카드가 표시됩니다.
- 1024px 이상(데스크톱): 한 줄에 3개의 카드가 표시됩니다.
- 1440px 이상(대형 데스크톱): 한 줄에 4개의 카드가 표시됩니다.
각 카드의 너비는 컨테이너 너비 백분율에서 간격을 고려하여 계산됩니다.
고급 스타일링 효과
카드 레이아웃에 몇 가지 고급 효과를 추가해 보겠습니다.
/* 카드 배지(신규 상품 등 표시) */
.card {
position: relative;
}
.card-badge {
position: absolute;
top: 10px;
right: 10px;
background-color: #ff6b6b;
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 0.8rem;
font-weight: bold;
z-index: 1;
}
/* 카드 로딩 애니메이션 */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.card {
animation: fadeIn 0.5s ease forwards;
opacity: 0;
}
.card:nth-child(1) { animation-delay: 0.1s; }
.card:nth-child(2) { animation-delay: 0.2s; }
.card:nth-child(3) { animation-delay: 0.3s; }
.card:nth-child(4) { animation-delay: 0.4s; }
.card:nth-child(5) { animation-delay: 0.5s; }
.card:nth-child(6) { animation-delay: 0.6s; }
이 코드는 다음 효과를 추가합니다:
- 카드 배지: "NEW", "인기" 등의 배지를 카드 우측 상단에 표시할 수 있습니다.
- 로딩 애니메이션: 페이지가 로드될 때 카드가 아래에서 위로 서서히 나타나는 효과가 적용됩니다.
HTML에서 배지를 추가하려면 아래와 같이 특정 카드에 코드를 추가하세요:
<div class="card">
<span class="card-badge">NEW</span>
<!-- 나머지 카드 내용 -->
</div>
접근성 향상 팁
카드 레이아웃의 접근성을 향상시키기 위한 몇 가지 팁을 적용해 보겠습니다.
/* 포커스 스타일 개선 */
.card-button:focus {
outline: 3px solid #4dabf7;
outline-offset: 2px;
}
/* 고대비 모드 지원 */
@media (prefers-contrast: high) {
.card {
border: 2px solid #000;
}
.card-title, .card-description {
color: #000;
}
.card-button {
background-color: #000;
color: #fff;
border: 2px solid #000;
}
}
/* 화면 리더기 전용 텍스트 */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
HTML에서는 다음과 같이 활용할 수 있습니다:
<button class="card-button">
자세히 보기
<span class="sr-only">제주도 여행 상품에 대한 상세 정보</span>
</button>
완성된 코드
지금까지 작성한 모든 CSS 코드를 하나로 합치면 다음과 같습니다:
/* 기본 리셋 및 폰트 설정 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Noto Sans KR', sans-serif;
line-height: 1.6;
color: #333;
background-color: #f8f9fa;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.page-title {
text-align: center;
margin-bottom: 40px;
color: #1a1a1a;
font-size: 2.5rem;
}
/* 이미지 기본 스타일 */
img {
max-width: 100%;
height: auto;
display: block;
}
/* 카드 컨테이너 레이아웃 */
.card-container {
display: flex;
flex-wrap: wrap;
gap: 30px;
justify-content: center;
}
/* 카드 기본 스타일 */
.card {
background-color: #fff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
width: 100%; /* 모바일에서는 한 줄에 하나의 카드 */
max-width: 400px;
position: relative;
animation: fadeIn 0.5s ease forwards;
opacity: 0;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15);
}
/* 카드 로딩 애니메이션 */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.card:nth-child(1) { animation-delay: 0.1s; }
.card:nth-child(2) { animation-delay: 0.2s; }
.card:nth-child(3) { animation-delay: 0.3s; }
.card:nth-child(4) { animation-delay: 0.4s; }
.card:nth-child(5) { animation-delay: 0.5s; }
.card:nth-child(6) { animation-delay: 0.6s; }
/* 카드 배지 */
.card-badge {
position: absolute;
top: 10px;
right: 10px;
background-color: #ff6b6b;
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 0.8rem;
font-weight: bold;
z-index: 1;
}
/* 카드 이미지 영역 */
.card-image {
height: 200px;
overflow: hidden;
}
.card-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
}
.card:hover .card-image img {
transform: scale(1.05);
}
/* 카드 콘텐츠 영역 */
.card-content {
padding: 20px;
}
.card-title {
font-size: 1.5rem;
margin-bottom: 10px;
color: #1a1a1a;
}
.card-description {
color: #666;
font-size: 0.95rem;
margin-bottom: 20px;
/* 두 줄 이상이면 말줄임표(...) 표시 */
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
height: 4.8em; /* line-height(1.6) * 3줄 */
}
/* 카드 푸터 영역 */
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 15px;
border-top: 1px solid #eee;
}
.card-price {
font-weight: bold;
color: #ff6b6b;
font-size: 1.1rem;
}
.card-button {
background-color: #4dabf7;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: background-color 0.2s ease;
}
.card-button:hover {
background-color: #339af0;
}
.card-button:focus {
outline: 3px solid #4dabf7;
outline-offset: 2px;
}
/* 화면 리더기 전용 텍스트 */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
/* 태블릿 뷰 */
@media (min-width: 768px) {
.card {
width: calc(50% - 15px); /* 한 줄에 2개 카드 (간격 고려) */
}
}
/* 데스크톱 뷰 */
@media (min-width: 1024px) {
.card {
width: calc(33.333% - 20px); /* 한 줄에 3개 카드 */
}
}
/* 대형 데스크톱 뷰 */
@media (min-width: 1440px) {
.card {
width: calc(25% - 22.5px); /* 한 줄에 4개 카드 */
}
}
/* 고대비 모드 지원 */
@media (prefers-contrast: high) {
.card {
border: 2px solid #000;
}
.card-title, .card-description {
color: #000;
}
.card-button {
background-color: #000;
color: #fff;
border: 2px solid #000;
}
}
응용 및 확장 아이디어
카드 레이아웃을 더 발전시킬 수 있는 몇 가지 아이디어를 소개합니다:
1. 필터링 기능 추가
카테고리 버튼을 추가하여 특정 카드만 표시할 수 있는 필터링 기능을 구현할 수 있습니다:
<div class="filter-buttons">
<button class="filter-btn active" data-filter="all">전체</button>
<button class="filter-btn" data-filter="nature">자연</button>
<button class="filter-btn" data-filter="culture">문화</button>
<button class="filter-btn" data-filter="food">음식</button>
</div>
JavaScript로 필터링 기능을 구현할 수 있습니다.
2. 카드 내용의 동적 로딩
카드 데이터를 JSON 형식으로 준비하고 JavaScript로 동적으로 카드를 생성할 수 있습니다. 이를 통해 API에서 데이터를 가져오거나 페이지네이션을 구현할 수 있습니다.
3. 그리드 레이아웃 대안
Flexbox 대신 CSS Grid를 사용하여 카드 레이아웃을 구현하는 방법도 있습니다:
.card-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 30px;
}
이 방법은 카드의 크기를 일정하게 유지하면서 화면 크기에 따라 열 수가 자동으로 조정됩니다.
4. 스켈레톤 로딩 상태
데이터 로딩 중에 표시할 스켈레톤 UI를 추가할 수 있습니다:
.card-skeleton {
background: #fff;
border-radius: 8px;
overflow: hidden;
height: 350px;
padding: 20px;
}
.skeleton-image {
width: 100%;
height: 200px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
margin-bottom: 15px;
}
.skeleton-title {
width: 70%;
height: 24px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
margin-bottom: 10px;
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
마무리
이 실습을 통해 Flexbox를 활용하여 반응형 카드 레이아웃을 처음부터 만드는 방법을 배웠습니다. 카드 디자인은 웹사이트의 사용자 경험을 크게 향상시킬 수 있으며, 이 기본 패턴을 다양한 프로젝트에 응용할 수 있습니다.
기억해야 할 주요 포인트:
- 구조와 스타일 분리: HTML로 의미 있는 구조를 만들고 CSS로 스타일을 적용
- Flexbox의 유연성: 다양한 화면 크기에 쉽게 적응하는 레이아웃 구성
- 반응형 디자인: 미디어 쿼리를 통한 다양한 화면 크기 지원
- 시각적 효과: 호버 효과, 애니메이션 등으로 사용자 경험 향상
- 접근성: 모든 사용자가 콘텐츠에 접근할 수 있도록 배려
이 카드 레이아웃은 쇼핑몰, 블로그, 포트폴리오, 레스토랑 메뉴 등 다양한 웹사이트에 적용할 수 있습니다. 필요에 맞게 색상, 폰트, 여백 등을 조정하여 브랜드에 맞는 디자인을 만들어보세요!
이 튜토리얼이 도움이 되었나요? 질문이나 피드백이 있으시면 댓글로 남겨주세요. 다음 실습에서는 CSS Grid를 활용한 더 복잡한 레이아웃 구성에 대해 알아보겠습니다.