icon
5장 : 스타일링과 CSS

프로필 카드 컴포넌트 만들기

우리는 인라인 스타일링부터 일반 CSS 클래스, CSS 모듈, 그리고 Styled-components까지 다양한 스타일링 방법과 반응형 디자인의 기초를 다졌습니다.

이번 실습에서는 이러한 지식들을 총동원하여 "반응형 카드 목록(Responsive Card List)" 컴포넌트를 만들어 볼 것입니다. 이 실습을 통해 각 스타일링 기법이 어떤 상황에 적합한지 체감하고, 복잡한 UI를 여러 스타일링 방식을 혼합하여 어떻게 구현하는지 경험하실 수 있을 것입니다.


실습 목표

  1. 다양한 스타일링 기법 적용
    • 인라인 스타일링: 동적인 조건부 스타일에 활용.
    • CSS 클래스: 전역적으로 필요한 기본 스타일 및 재사용 가능한 유틸리티 클래스에 활용.
    • CSS 모듈: 컴포넌트 스코프 스타일링으로 클래스 이름 충돌 방지.
    • Styled-components: 컴포넌트 자체를 스타일링하고 props를 통한 동적 스타일링에 활용.
  2. 미디어 쿼리를 이용한 반응형 디자인: 화면 크기에 따라 카드 레이아웃(컬럼 수)과 텍스트 크기 등이 유연하게 변하도록 구현.
  3. 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
/* 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
// 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
/* 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
// 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
// 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;

실습 진행 방법

  1. 위의 모든 코드 파일들을 해당 경로에 맞게 생성하고 내용을 복사하여 붙여넣으세요.
  2. Styled-components가 설치되어 있는지 확인합니다 (npm install styled-components 또는 yarn add styled-components).
  3. npm start (또는 yarn start) 명령어를 실행하여 개발 서버를 시작합니다.
  4. 브라우저에서 http://localhost:3000에 접속합니다.
  5. 카드 목록 확인: 다양한 카드들이 그리드 형태로 배열되어 있는지 확인합니다.
  6. 카테고리 필터링: 상단의 필터 버튼을 클릭하여 특정 카테고리의 카드만 표시되는지 확인합니다. (이때 버튼의 active 상태에 따라 배경색이 변하는 Styled-components의 동적 스타일링을 관찰하세요.)
  7. 반응형 테스트: 브라우저 창의 너비를 조절하거나 개발자 도구의 반응형 모드를 사용하여, 화면 크기에 따라 카드의 열(column) 수가 3열 -> 2열 -> 1열로 변하는지 확인합니다. (CardList.js의 Styled-components와 CardItem.module.css의 미디어 쿼리가 함께 작동)
  8. 클래스 이름 확인: 개발자 도구로 CardItem의 DOM 요소를 검사하여 CSS 모듈에 의해 클래스 이름이 고유하게 변환되었는지 (CardItem_card__abc123 형태) 확인합니다. CardList에서 Styled-components가 생성한 클래스 이름도 확인합니다.
  9. 인라인 스타일 확인: CardItem 내부의 카테고리 스팬에 categoryColorMap에 따라 동적으로 적용된 background-color 인라인 스타일이 적용되었는지 확인합니다.

이제 여러분은 리액트 애플리케이션에서 다양한 스타일링 기법(인라인, CSS 클래스, CSS 모듈, Styled-components)을 목적에 맞게 혼용하고, 미디어 쿼리와 유연한 레이아웃(CSS Grid)을 사용하여 반응형 UI를 구현하는 능력을 갖추게 되셨습니다.

이 실습을 통해 각 스타일링 방식의 장단점을 명확히 이해하고, 실제 프로젝트에서 어떤 상황에 어떤 기법을 사용할지 판단하는 데 필요한 실용적인 지식을 얻으셨기를 바랍니다.