프로필 카드 컴포넌트 만들기
우리는 인라인 스타일링부터 일반 CSS 클래스, CSS 모듈, 그리고 Styled-components까지 다양한 스타일링 방법과 반응형 디자인의 기초를 다졌습니다.
이번 실습에서는 이러한 지식들을 총동원하여 "반응형 카드 목록(Responsive Card List)" 컴포넌트를 만들어 볼 것입니다. 이 실습을 통해 각 스타일링 기법이 어떤 상황에 적합한지 체감하고, 복잡한 UI를 여러 스타일링 방식을 혼합하여 어떻게 구현하는지 경험하실 수 있을 것입니다.
실습 목표
- 다양한 스타일링 기법 적용
- 인라인 스타일링: 동적인 조건부 스타일에 활용.
- CSS 클래스: 전역적으로 필요한 기본 스타일 및 재사용 가능한 유틸리티 클래스에 활용.
- CSS 모듈: 컴포넌트 스코프 스타일링으로 클래스 이름 충돌 방지.
- Styled-components: 컴포넌트 자체를 스타일링하고 props를 통한 동적 스타일링에 활용.
- 미디어 쿼리를 이용한 반응형 디자인: 화면 크기에 따라 카드 레이아웃(컬럼 수)과 텍스트 크기 등이 유연하게 변하도록 구현.
- Flexbox 또는 CSS Grid 활용: 카드 목록을 유연하게 배치하는 레이아웃 구현.
프로젝트 준비
create-react-app
으로 생성된 프로젝트가 있다고 가정합니다. src
폴더에 다음과 같은 구조로 파일들을 생성하고 코드를 작성하겠습니다.
src/
├── App.js
├── index.css (전역/기본 스타일)
├── components/
│ ├── CardList.js
│ ├── CardItem.js
│ └── CardItem.module.css
└── styles/
└── global.css (선택적, App.css 역할)
전역 스타일링 (index.css
)
전체 애플리케이션에 적용될 기본적인 CSS 리셋 및 폰트 설정을 포함합니다.
/* src/index.css */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 0;
background-color: #f0f2f5;
color: #333;
line-height: 1.6;
}
.App {
padding: 20px;
max-width: 1200px;
margin: 20px auto;
background-color: #ffffff;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
h1, h2 {
color: #2c3e50;
text-align: center;
margin-bottom: 30px;
}
/* 유틸리티 클래스 (일반 CSS 클래스 예시) */
.text-center {
text-align: center;
}
.margin-top-20 {
margin-top: 20px;
}
Styled-components를 사용한 컨테이너
CardList.js
에서 카드 목록 전체를 감싸는 컨테이너를 Styled-components로 만들고, 반응형 레이아웃을 구현합니다.
// src/components/CardList.js
import React, { useState } from 'react';
import styled from 'styled-components';
import CardItem from './CardItem'; // 다음 단계에서 만들 CardItem 컴포넌트
// (가상 데이터) 카드 목록에 표시될 데이터
const mockCards = [
{ id: 1, title: '리액트 기초', description: 'JSX, 컴포넌트, props의 이해', category: '프론트엔드', imgUrl: 'https://via.placeholder.com/150/f44336/FFFFFF?text=React1' },
{ id: 2, title: '훅의 마법', description: 'useState, useEffect, useContext 심화 학습', category: '리액트 훅', imgUrl: 'https://via.placeholder.com/150/E91E63/FFFFFF?text=React2' },
{ id: 3, title: '스타일링 마스터', description: 'CSS-in-JS, CSS 모듈, 반응형 디자인', category: 'UI/UX', imgUrl: 'https://via.placeholder.com/150/9C27B0/FFFFFF?text=React3' },
{ id: 4, title: '성능 최적화', description: '메모이제이션, 코드 스플리팅 기법', category: '심화', imgUrl: 'https://via.placeholder.com/150/673AB7/FFFFFF?text=React4' },
{ id: 5, title: '라우팅 가이드', description: 'React Router를 이용한 페이지 전환', category: '프론트엔드', imgUrl: 'https://via.placeholder.com/150/3F51B5/FFFFFF?text=React5' },
{ id: 6, title: '상태 관리 패턴', description: 'Redux, Recoil, Zustand 비교 분석', category: '아키텍처', imgUrl: 'https://via.placeholder.com/150/2196F3/FFFFFF?text=React6' },
];
// Styled-components를 사용한 반응형 그리드 컨테이너
const CardsContainer = styled.div`
display: grid;
gap: 25px; /* 카드 사이의 간격 */
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); /* 기본 3열 (데스크톱) */
padding: 20px;
background-color: #fdfdfd;
border-radius: 10px;
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.05);
/* 미디어 쿼리: 태블릿 (최대 992px) */
@media (max-width: 992px) {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); /* 2열 */
gap: 20px;
padding: 15px;
}
/* 미디어 쿼리: 모바일 (최대 576px) */
@media (max-width: 576px) {
grid-template-columns: 1fr; /* 1열 */
gap: 15px;
padding: 10px;
}
`;
// 카테고리 필터링을 위한 버튼 스타일 (인라인 스타일 + CSS 클래스 조합 예시)
const FilterButton = styled.button`
padding: 10px 20px;
margin: 5px;
border: 1px solid #ccc;
border-radius: 5px;
background-color: white;
cursor: pointer;
font-size: 1em;
transition: all 0.2s ease;
&:hover {
background-color: #eee;
}
/* active prop에 따른 동적 스타일 (인라인 스타일링과 유사) */
${props => props.active && `
background-color: #007bff;
color: white;
border-color: #007bff;
font-weight: bold;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
`}
`;
function CardList() {
const [filterCategory, setFilterCategory] = useState('All'); // 필터링 상태
const categories = ['All', ...new Set(mockCards.map(card => card.category))];
const filteredCards = filterCategory === 'All'
? mockCards
: mockCards.filter(card => card.category === filterCategory);
return (
<>
<h2 className="text-center">반응형 카드 목록</h2>
<div className="text-center margin-top-20"> {/* 일반 CSS 클래스 활용 */}
{categories.map(category => (
<FilterButton
key={category}
onClick={() => setFilterCategory(category)}
active={filterCategory === category} // active prop 전달
>
{category}
</FilterButton>
))}
</div>
<CardsContainer>
{filteredCards.map(card => (
<CardItem key={card.id} card={card} />
))}
</CardsContainer>
</>
);
}
export default CardList;
CSS 모듈을 사용한 카드 아이템
개별 카드 아이템의 스타일은 CSS 모듈을 사용하여 컴포넌트 스코프를 확보합니다. 또한, 내부적으로 인라인 스타일링도 사용합니다.
CardItem.module.css
/* src/components/CardItem.module.css */
.card {
background-color: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
display: flex; /* 내부 요소 정렬을 위한 Flexbox */
flex-direction: column;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.15);
}
.cardImage {
width: 100%;
height: 180px; /* 고정 높이 */
object-fit: cover; /* 이미지가 잘리지 않고 채워지도록 */
background-color: #eee; /* 이미지 없을 때 대비 */
}
.cardContent {
padding: 15px;
flex-grow: 1; /* 남은 공간 차지 */
display: flex;
flex-direction: column;
justify-content: space-between;
}
.cardTitle {
font-size: 1.4em;
color: #34495e;
margin-top: 0;
margin-bottom: 10px;
}
.cardDescription {
font-size: 0.95em;
color: #7f8c8d;
margin-bottom: 15px;
flex-grow: 1; /* 설명이 길어져도 레이아웃 유지 */
}
.cardCategory {
font-size: 0.85em;
color: #9b59b6; /* 보라색 */
background-color: #ecf0f1;
padding: 5px 10px;
border-radius: 15px;
display: inline-block;
margin-top: 10px;
align-self: flex-start; /* 좌측 정렬 */
}
/* 반응형을 위한 미디어 쿼리 (CSS 모듈 내에서도 사용 가능) */
@media (max-width: 576px) {
.cardTitle {
font-size: 1.2em;
}
.cardDescription {
font-size: 0.9em;
}
}
CardItem.js
// src/components/CardItem.js
import React from 'react';
import styles from './CardItem.module.css'; // CSS 모듈 임포트
function CardItem({ card }) {
// (예시) 카테고리에 따른 동적 인라인 스타일 (배경색 변경)
const categoryColorMap = {
'프론트엔드': '#e74c3c',
'리액트 훅': '#2980b9',
'UI/UX': '#f39c12',
'심화': '#8e44ad',
'아키텍처': '#16a085',
'All': '#7f8c8d'
};
const categoryStyle = {
backgroundColor: categoryColorMap[card.category] || '#7f8c8d',
color: 'white'
};
return (
<div className={styles.card}> {/* CSS 모듈 클래스 사용 */}
<img src={card.imgUrl} alt={card.title} className={styles.cardImage} />
<div className={styles.cardContent}>
<h3 className={styles.cardTitle}>{card.title}</h3>
<p className={styles.cardDescription}>{card.description}</p>
<span className={styles.cardCategory} style={categoryStyle}> {/* CSS 모듈 + 인라인 스타일 조합 */}
{card.category}
</span>
</div>
</div>
);
}
export default CardItem;
App.js
(최종)
모든 컴포넌트를 조합하여 렌더링합니다.
// src/App.js
import React from 'react';
import './index.css'; // 전역 스타일
import CardList from './components/CardList';
function App() {
return (
<div className="App">
<CardList />
</div>
);
}
export default App;
실습 진행 방법
- 위의 모든 코드 파일들을 해당 경로에 맞게 생성하고 내용을 복사하여 붙여넣으세요.
- Styled-components가 설치되어 있는지 확인합니다 (
npm install styled-components
또는yarn add styled-components
). npm start
(또는yarn start
) 명령어를 실행하여 개발 서버를 시작합니다.- 브라우저에서
http://localhost:3000
에 접속합니다. - 카드 목록 확인: 다양한 카드들이 그리드 형태로 배열되어 있는지 확인합니다.
- 카테고리 필터링: 상단의 필터 버튼을 클릭하여 특정 카테고리의 카드만 표시되는지 확인합니다. (이때 버튼의
active
상태에 따라 배경색이 변하는 Styled-components의 동적 스타일링을 관찰하세요.) - 반응형 테스트: 브라우저 창의 너비를 조절하거나 개발자 도구의 반응형 모드를 사용하여, 화면 크기에 따라 카드의 열(column) 수가 3열 -> 2열 -> 1열로 변하는지 확인합니다. (
CardList.js
의 Styled-components와CardItem.module.css
의 미디어 쿼리가 함께 작동) - 클래스 이름 확인: 개발자 도구로
CardItem
의 DOM 요소를 검사하여 CSS 모듈에 의해 클래스 이름이 고유하게 변환되었는지 (CardItem_card__abc123
형태) 확인합니다.CardList
에서 Styled-components가 생성한 클래스 이름도 확인합니다. - 인라인 스타일 확인:
CardItem
내부의 카테고리 스팬에categoryColorMap
에 따라 동적으로 적용된background-color
인라인 스타일이 적용되었는지 확인합니다.
이제 여러분은 리액트 애플리케이션에서 다양한 스타일링 기법(인라인, CSS 클래스, CSS 모듈, Styled-components)을 목적에 맞게 혼용하고, 미디어 쿼리와 유연한 레이아웃(CSS Grid)을 사용하여 반응형 UI를 구현하는 능력을 갖추게 되셨습니다.
이 실습을 통해 각 스타일링 방식의 장단점을 명확히 이해하고, 실제 프로젝트에서 어떤 상황에 어떤 기법을 사용할지 판단하는 데 필요한 실용적인 지식을 얻으셨기를 바랍니다.