본문 바로가기

반응형 카드 목록 컴포넌트 만들기

전역 CSS·styled-components·CSS 모듈·인라인 스타일의 책임을 나누어 반응형 카드 목록을 만들고 cascade와 접근성을 검증합니다.

5장에서는 인라인 스타일과 일반 CSS 클래스, CSS 모듈, styled-components까지 여러 스타일링 방법과 반응형 디자인의 기초를 익혔습니다. 이 실습에서는 이 지식을 모두 동원해 반응형 카드 목록(responsive card list) 컴포넌트를 만듭니다. 각 스타일링 기법이 어떤 상황에 알맞은지 체감하고, 여러 방식을 섞어 하나의 UI를 구현하는 방법을 경험해 보세요.

카드 목록의 스타일은 범위와 값의 출처로 나눕니다

CARD LIST · STYLE RESPONSIBILITY

카드 목록의 스타일은 범위와 값의 출처로 나눕니다

전역 기본값, 목록·필터 규칙, 카드 내부 정적 규칙, 데이터 기반 배경색을 분리합니다. 모든 결과 CSS는 같은 문서의 cascade에 참여합니다.

본문 필터 버튼 · 스타일과 접근성 상태

filterCategory="프론트엔드"
$active=true → 선택 CSS 규칙
aria-pressed=true → 실제 버튼 상태
$active는 DOM에 전달하지 않음

전역 CSS는 기본값, styled-components는 목록과 필터, CSS 모듈은 카드 내부, inline style은 배지 배경색 한 값을 맡습니다.

카드 목록 예제의 스타일 경계와 cascade 계약
경계 · 이 예제의 책임 확인할 계약
전역 CSS

body, 제목, .App, 공유 유틸리티의 기본값을 한 번 적용합니다.

요소 선택자와 전역 클래스는 모든 일치 요소에 닿으므로 카드 내부 세부 규칙을 넣지 않습니다.
styled-components

CardsContainer의 Grid·media query와 FilterButton의 prop 기반 상태를 맡습니다.

스타일 전용 값은 $active 같은 transient prop으로 두고 실제 선택 상태는 aria-pressed로 노출합니다.
CSS 모듈

CardItem.module.css가 카드 내부의 지역 클래스와 정적 규칙을 제공합니다.

지역화되는 것은 클래스 이름입니다. 생성 문자열은 빌드 설정에 따라 달라지고 규칙은 여전히 cascade에 참여합니다.
인라인 style

카테고리 데이터에서 계산한 backgroundColor 한 값만 카드 배지에 적용합니다.

동일 중요도에서는 style attribute 선언이 선택자 규칙보다 우선하므로 값 범위를 좁게 유지합니다.
전역 CSS · 공유 기본값
body, 제목, .App, 유틸리티처럼 모든 화면이 공유할 규칙만 둡니다. 전역 선택자는 모든 일치 요소에 영향을 줍니다.
styled-components · 레이아웃과 상태
Grid와 media query, 필터 버튼의 prop 기반 CSS를 맡습니다. $active는 DOM에 전달하지 않고 aria-pressed가 실제 상태를 설명합니다.
CSS 모듈 · 지역 클래스
카드 내부 정적 규칙을 지역 이름으로 import합니다. 클래스 이름 충돌은 줄지만 CSS cascade 자체가 사라지는 것은 아닙니다.
인라인 style · 계산된 한 값
카테고리 배경색처럼 JavaScript 데이터에서 정해지는 값만 둡니다. 같은 중요도에서는 선택자 규칙보다 우선합니다.

가장 넓은 전역 경계에서 가장 좁은 값 경계로 내려갈수록 책임을 줄이고, 같은 속성을 여러 도구가 동시에 소유하지 않게 합니다.

위 그림은 카드 목록의 스타일을 적용 범위와 값이 나오는 곳에 따라 네 가지로 나눈 표입니다. 전역 CSS는 body와 제목, .App, 공유 유틸리티의 기본값을 한 번 적용하며, 요소 선택자와 전역 클래스는 일치하는 모든 요소에 닿으므로 카드 안의 세부 규칙은 넣지 않습니다. styled-components는 CardsContainer의 Grid와 미디어 쿼리, FilterButton의 prop에 따른 상태를 맡습니다. 스타일 전용 값은 $active 같은 transient prop으로 두고, 실제 선택 상태는 aria-pressed로 드러냅니다.

CSS 모듈은 CardItem.module.css에서 카드 안의 지역 클래스와 정적 규칙을 제공합니다. 지역화되는 것은 클래스 이름뿐이라서 생성 문자열은 빌드 설정에 따라 달라지고, 규칙은 여전히 캐스케이드에 참여합니다. 인라인 style은 카테고리 데이터에서 계산한 backgroundColor 한 값만 카드의 배지에 적용합니다. 같은 중요도라면 style 속성의 선언이 선택자 규칙보다 우선하므로 그 범위를 좁게 유지합니다.

그림 위쪽의 예처럼 filterCategory가 “프론트엔드”이면 그 필터 버튼에는 $active=true로 선택 상태의 CSS 규칙이 적용되고, aria-pressed=true가 실제 버튼의 상태를 알리며, $active는 DOM에 넘어가지 않습니다. 그림의 마지막 설명처럼 가장 넓은 전역 범위에서 가장 좁은 값 하나로 내려갈수록 맡는 일을 줄이고, 같은 속성을 여러 도구가 동시에 정하지 않게 하는 것이 핵심입니다.


실습 목표: 스타일링 전략 비교 적용

이 실습은 스타일링 전략을 고르는 일을 중심으로 진행합니다. 같은 UI를 만들더라도 동적인 스타일이 필요한지, 재사용이 중요한지, 클래스 충돌을 막아야 하는지에 따라 도구가 달라지는 지점을 비교해 봅니다.

이 과정에서 네 가지 스타일링 기법을 모두 씁니다. 인라인 스타일은 자바스크립트 데이터에서 계산한 한두 개의 값에만 제한적으로, CSS 클래스는 전역 기본 스타일과 일부러 공유할 유틸리티 클래스에, CSS 모듈은 컴포넌트의 지역 클래스와 정적인 내부 스타일에, styled-components는 목록의 레이아웃과 컨트롤을 꾸미고 props에 따라 CSS 규칙을 고르는 데 씁니다. 미디어 쿼리로 쓸 수 있는 폭에 따라 카드의 열 수가 유연하게 정해지고 좁은 화면에서는 한 열과 작은 글자 크기로 바뀌게 하며, 카드 목록은 CSS Grid로 유연하게 배치합니다.


준비 단계: 스타일 역할 분리

준비 단계에서 나누는 기준은 단순합니다. 전역의 초기화와 기본값은 index.css에, 목록과 필터 컨트롤의 prop에 따른 규칙은 styled-components에, 카드 안의 정적 규칙은 CSS 모듈에 둡니다. 카테고리처럼 데이터에서 계산되는 색 하나만 인라인 스타일로 남깁니다. CSS 모듈과 styled-components가 만든 클래스도 결국 같은 문서의 CSS 캐스케이드에 참여합니다. 클래스 이름의 지역화는 선택자의 충돌을 줄일 뿐, Shadow DOM 같은 완전한 스타일 격리는 아닙니다.

Vite로 만든 프로젝트가 있다고 가정하고, src 폴더에 다음 구조로 파일을 만듭니다. styled-components가 설치되어 있지 않다면 Styled-components 기초에서처럼 프로젝트 폴더에서 npm install styled-components로 먼저 설치합니다.

main.jsx
App.jsx
index.css # 전역/기본 스타일
CardList.jsx
CardItem.jsx
CardItem.module.css

레이아웃 기반 전역 스타일 설정 (index.css)

앱 전체에 적용할 기본적인 CSS 초기화와 글꼴 설정을 넣습니다. 첫 규칙의 box-sizing: border-box는 요소의 width에 안쪽 여백과 테두리를 포함시켜, 뒤에서 Grid의 열 폭을 계산할 때 여백 때문에 상자가 넘치는 일을 줄입니다. *::before, *::after까지 함께 적은 것은 가상 요소에도 같은 규칙을 적용하기 위해서입니다.

src/index.css
*,
*::before,
*::after {
  box-sizing: border-box;
}

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;
}

body, h1, h2 같은 요소 선택자와 .App, 유틸리티 클래스는 문서 전체에 영향을 줍니다. 그래서 전역 규칙에는 모든 화면이 공유해야 하는 기본값만 두고, 카드 안의 규칙은 뒤에서 CSS 모듈로 옮깁니다.


Styled-components를 사용한 컨테이너

CardList.jsx에서 카드 목록 전체를 감싸는 컨테이너를 styled-components로 만들고, 반응형 레이아웃을 구현합니다. 이 파일은 세 부분으로 읽으면 됩니다. 이미지 파일 없이 실행되도록 SVG를 데이터 URL로 만든 placeholderImage와 여섯 장의 mockCards, 목록과 필터 버튼의 모양을 정하는 두 스타일 컴포넌트, 그리고 선택한 카테고리를 state로 가지고 그에 맞는 카드만 그리는 CardList 함수입니다.

src/components/CardList.jsx
import { useState } from 'react';
import styled from 'styled-components';
import CardItem from './CardItem'; // 다음 단계에서 만들 CardItem 컴포넌트

// 예제가 별도 이미지 파일 없이도 실행되도록 만든 장식용 placeholder
const placeholderImage = `data:image/svg+xml,${encodeURIComponent(`
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 360">
    <rect width="640" height="360" fill="#e4e7ec" />
    <circle cx="320" cy="140" r="56" fill="#98a2b3" />
    <path d="M176 304c28-72 86-108 144-108s116 36 144 108" fill="#667085" />
  </svg>
`)}`;

// (가상 데이터) 카드 목록에 표시될 데이터
const mockCards = [
  { id: 1, title: '리액트 기초', description: 'JSX, 컴포넌트, props의 이해', category: '프론트엔드', imgUrl: placeholderImage },
  { id: 2, title: '훅 심화', description: 'useState, useEffect, useContext 심화 학습', category: '리액트 훅', imgUrl: placeholderImage },
  { id: 3, title: '스타일링 마스터', description: 'CSS-in-JS, CSS 모듈, 반응형 디자인', category: 'UI/UX', imgUrl: placeholderImage },
  { id: 4, title: '성능 최적화', description: '메모이제이션, 코드 스플리팅 기법', category: '심화', imgUrl: placeholderImage },
  { id: 5, title: '라우팅 가이드', description: 'React Router를 이용한 페이지 전환', category: '프론트엔드', imgUrl: placeholderImage },
  { id: 6, title: '상태 관리 패턴', description: 'Redux Toolkit, Zustand, Jotai 비교 분석', category: '아키텍처', imgUrl: placeholderImage },
];

// styled-components를 사용한 반응형 목록 컨테이너
const CardsContainer = styled.ul`
  margin: 0;
  list-style: none;
  display: grid;
  gap: 25px; /* 카드 사이의 간격 */
  /* 사용 가능한 너비에 280px 이상인 열을 가능한 만큼 배치 */
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  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-fit, minmax(250px, 1fr));
    gap: 20px;
    padding: 15px;
  }

  /* 미디어 쿼리: 모바일 (최대 576px) */
  @media (max-width: 576px) {
    grid-template-columns: 1fr; /* 1열 */
    gap: 15px;
    padding: 10px;
  }
`;

// 카테고리 필터링을 위한 prop 기반 버튼 스타일
const FilterButton = styled.button`
  padding: 10px 20px;
  margin: 5px;
  border: 1px solid #ccc;
  border-radius: 5px;
  background-color: white;
  color: #1d2939;
  cursor: pointer;
  font-size: 1em;
  transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;

  &:focus-visible {
    outline: 3px solid #111;
    outline-offset: 2px;
  }

  /* $active는 DOM으로 전달되지 않는 transient prop */
  ${({ $active }) => $active && `
    background-color: #005fcc;
    color: white;
    border-color: #005fcc;
    font-weight: bold;
  `}

  &:hover {
    background-color: ${({ $active }) => $active ? '#004a99' : '#eee'};
  }

  @media (prefers-reduced-motion: reduce) {
    transition: none;
  }
`;

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);
  const selectedLabel = filterCategory === 'All' ? '전체' : filterCategory;

  return (
    <>
      <h1 className="text-center">반응형 카드 목록</h1>
      <div
        className="text-center margin-top-20"
        role="group"
        aria-label="카테고리 필터"
      >
        {categories.map(category => (
          <FilterButton
            key={category}
            type="button"
            onClick={() => setFilterCategory(category)}
            $active={filterCategory === category}
            aria-pressed={filterCategory === category}
          >
            {category}
          </FilterButton>
        ))}
      </div>
      <p className="text-center" role="status" aria-live="polite">
        {selectedLabel} 카테고리: {filteredCards.length}개
      </p>
      <CardsContainer aria-label="강의 카드 목록">
        {filteredCards.map(card => (
          <CardItem key={card.id} card={card} />
        ))}
      </CardsContainer>
    </>
  );
}

export default CardList;

repeat(auto-fit, minmax(...))는 컨테이너에 들어갈 수 있는 열 수를 계산하므로, 주석만 보고 언제나 3열이나 2열이 된다고 생각해서는 안 됩니다. 992px 이하에서는 최소 열 폭과 간격을 줄이고, 576px 이하에서만 한 열로 고정합니다. $active는 styled-components가 스타일을 계산할 때만 쓰고 DOM 버튼에는 넘기지 않는 transient prop이며, 실제 선택 상태는 따로 aria-pressed로 나타냅니다.


CSS 모듈을 사용한 카드 아이템

개별 카드의 정적 스타일은 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;
}

@media (hover: hover) and (pointer: fine) {
  .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: #475467;
  margin-bottom: 15px;
  flex-grow: 1; /* 설명이 길어져도 레이아웃 유지 */
}

.cardCategory {
  font-size: 0.85em;
  color: white;
  background-color: #475467; /* 알 수 없는 category의 fallback */
  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;
  }
}

@media (prefers-reduced-motion: reduce) {
  .card {
    transition: none;
  }

  .card:hover {
    transform: none;
  }
}

CardItem.jsx

src/components/CardItem.jsx
import styles from './CardItem.module.css'; // CSS 모듈 임포트

function CardItem({ card }) {
  // 흰 글자와 충분한 대비를 갖는 카테고리 배경색
  const categoryColorMap = {
    '프론트엔드': '#b42318',
    '리액트 훅': '#175cd3',
    'UI/UX': '#8a4b08',
    '심화': '#6941c6',
    '아키텍처': '#107569'
  };
  const categoryStyle = {
    backgroundColor: categoryColorMap[card.category] || '#475467'
  };

  return (
    <li className={styles.card}> {/* CSS 모듈 클래스 사용 */}
      <img
        src={card.imgUrl}
        alt=""
        width={640}
        height={360}
        className={styles.cardImage}
        loading="lazy"
      />
      <div className={styles.cardContent}>
        <h3 className={styles.cardTitle}>{card.title}</h3>
        <p className={styles.cardDescription}>{card.description}</p>
        <span className={styles.cardCategory} style={categoryStyle}>
          {card.category}
        </span>
      </div>
    </li>
  );
}

export default CardItem;

CardItem.module.css를 가져오면 지역 이름과 실제 클래스 이름의 대응표 객체가 만들어집니다. 생성되는 문자열의 형식은 Vite의 설정과 개발·프로덕션 모드에 따라 달라질 수 있으므로, 특정 접두사나 해시의 모양에 기대지 않습니다. 부모의 map()에 둔 key={card.id}는 React가 형제 항목을 추적하는 힌트일 뿐 CardItem의 prop으로 전달되지 않으므로, 카드가 id를 써야 한다면 card.id나 별도의 prop으로 분명히 넘겨야 합니다.

이 예제의 이미지는 모든 카드가 같은 대체 그림을 쓰고 바로 아래에 제목이 되풀이되므로, alt=""로 장식용 이미지임을 나타냈습니다. 이미지가 제목에 없는 정보를 전하도록 바뀐다면 그 목적에 맞는 대체 텍스트도 함께 넘겨야 합니다.

CardItem은 데이터·지역 클래스·동적 값을 한 li로 결합합니다

CARDITEM · INPUT & DOM

CardItem은 데이터·지역 클래스·동적 값을 한 li로 결합합니다

card prop은 제목·설명·카테고리를, CSS 모듈은 정적 규칙을 제공합니다. 카테고리에서 계산한 배경색 한 값만 style로 전달하며 부모의 key는 별도 식별 계약입니다.

본문 첫 카드 · 값 하나만 계산

card.title: 리액트 기초
card.category: 프론트엔드
backgroundColor: #b42318
결과: ul 안 li / h3 / p / span

같은 장식 placeholder는 alt=""로 중복 낭독을 피합니다. 이미지가 새 정보를 전달한다면 그 목적에 맞는 대체 텍스트를 함께 제공해야 합니다.

CardItem 입력과 렌더링 구조card 데이터, 지역 클래스, 카테고리 배경색이라는 세 입력은 CardItem으로 각각 들어갑니다. 결합 결과는 ul 안의 li이며 장식 img와 h3,p,span을 갖습니다. 부모의 key는 자식 prop으로 자동 전달되지 않습니다. card proptitle·descriptioncategory·imgUrl·idCSS 모듈지역 클래스 매핑정적 내부 규칙계산한 값 하나category → 배경색CardItemprop·class·style 결합key는 부모 목록목록 항목li / img alt=""h3 / p / spanul 안 의미 있는 결과
CardItem 입력과 렌더링 구조card 데이터, 지역 클래스, 카테고리 배경색이라는 세 입력은 CardItem으로 각각 들어갑니다. 결합 결과는 ul 안의 li이며 장식 img와 h3,p,span을 갖습니다. 부모의 key는 자식 prop으로 자동 전달되지 않습니다. card proptitle·descriptioncategory·imgUrl·idCSS 모듈지역 클래스 매핑정적 내부 규칙계산한 값 하나category → 배경색CardItemprop·class·style 결합key는 부모 목록목록 항목li / img alt=""h3 / p / spanul 안 의미 있는 결과

card 데이터, 지역 클래스, 카테고리 배경색이라는 세 입력은 CardItem으로 각각 들어갑니다. 결합 결과는 ul 안의 li이며 장식 img와 h3,p,span을 갖습니다. 부모의 key는 자식 prop으로 자동 전달되지 않습니다.

CSS 모듈은 클래스 이름 충돌을 줄이지만 cascade를 격리하지 않습니다. 같은 속성의 소유자를 하나로 정해 최종 계산 스타일을 예측 가능하게 유지합니다.

위 그림은 CardItem이 세 가지 입력을 하나의 li로 합치는 모습을 보여 줍니다. card prop은 제목과 설명, 카테고리, 이미지 주소, id를, CSS 모듈은 지역 클래스의 대응표와 정적인 내부 규칙을 제공하며, 카테고리에서 계산한 배경색 한 값은 style로 들어갑니다. 결과는 ul 안의 li로, 장식용 img와 h3, p, span을 가집니다. 부모의 key는 부모 목록에서 쓰는 식별 정보일 뿐 자식의 prop으로 저절로 전달되지 않습니다.

그림 위쪽의 예처럼 첫 카드는 card.title이 “리액트 기초”, card.category가 “프론트엔드”이므로 배지의 backgroundColor가 #b42318이 됩니다. 그림의 설명처럼 CSS 모듈은 클래스 이름의 충돌을 줄일 뿐 캐스케이드를 격리하지 않으므로, 같은 속성을 정하는 쪽을 하나로 두어 최종 계산 스타일을 예측할 수 있게 합니다.


App.jsx와 main.jsx 연결

App.jsx에서 컴포넌트를 조합하고, Vite의 진입점인 main.jsx에서 전역 CSS와 App을 연결합니다. App은 .App 클래스를 가진 바깥 상자 안에 CardList를 놓기만 합니다. 전역 CSS는 진입점인 main.jsx에서 한 번만 불러오고, 컴포넌트 파일에서는 다시 불러오지 않습니다.

src/App.jsx
import CardList from './components/CardList';

function App() {
  return (
    <div className="App">
      <CardList />
    </div>
  );
}

export default App;
src/main.jsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App.jsx';

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
);

완성된 카드 목록에서는 데이터와 필터 상태, props, 스타일이 서로 얽혀 있습니다. 문제가 생기면 원본 데이터에서 시작해 필터 결과, 자식의 prop, 최종 DOM과 계산된 스타일의 순서로 따라갑니다.


첫 실행에서 값과 요소 대조하기

앞의 Vite 프로젝트의 React 진입점과 루트의 index.html은 그대로 두고 여기의 소스 파일들을 연결합니다. 개발 서버에 접속할 때는 터미널에 출력된 주소를 씁니다.

처음에는 All이 선택되어 카드 6개와 “전체 카테고리: 6개” 문구가 보입니다. “프론트엔드”를 고르면 원래 데이터의 id 1과 5만 남아 카드 두 장과 “프론트엔드 카테고리: 2개”가 나타납니다. “리액트 훅”은 id 2 하나이며, 다시 All을 고르면 원래의 카드 여섯 장이 돌아옵니다. 배열에서 원래 데이터를 지우는 대신, 현재 필터에 맞는 결과를 렌더할 때마다 계산하기 때문입니다.

선택된 버튼의 aria-pressed가 하나만 true인지, 배경색과 결과의 개수도 그 선택을 가리키는지 확인합니다. 이미지가 모두 같은 장식용 대체 그림이라는 조건도 그대로 둡니다. 아직 구현하지 않은 상세 페이지로의 이동이나 필터의 저장을 버튼에 기대해서는 안 됩니다.

Grid의 열 수를 숫자로 확인하기

예제의 border-box 전역 규칙과 실제의 짧은 카드 문구를 그대로 씁니다. 뷰포트가 1200px이면 .App의 폭 1200px에서 좌우 안쪽 여백 40px을 빼고, 목록의 안쪽 여백 40px도 빼면 Grid의 내용 폭은 1120px입니다. 이 구간의 최소 열 폭은 280px, 간격은 25px이므로, 세 열에 필요한 280 × 3 + 25 × 2 = 890px은 들어가지만 네 열의 280 × 4 + 25 × 3 = 1195px은 들어가지 않습니다. 여섯 카드의 처음 목록은 세 열입니다.

375px에서는 576px 이하의 규칙이 적용됩니다. .App의 안쪽 여백 40px과 목록의 안쪽 여백 20px을 빼면 카드가 쓰는 폭은 315px이고, 열은 명시적으로 하나입니다. 다만 auto-fit을 쓰는 576px보다 넓은 폭에서는 필터로 항목 수가 줄어들 때 빈 트랙이 접혀 남은 카드가 넓어질 수 있으므로, 같은 뷰포트에서도 카드의 실제 폭이 달라질 수 있습니다.

긴 단어를 넣는 확장

원래 예제의 짧은 문구를 확인한 뒤, 끊기 어려운 URL을 설명에 넣어 봅니다. 다음 규칙을 CardItem.module.css의 끝에 더하면, 카드가 Grid 트랙 안에서 줄어들 수 있고 긴 문자열도 줄을 바꿀 수 있습니다. 기존 글자 크기를 줄이거나 정보를 숨기는 방식으로 해결하지 않습니다.

src/components/CardItem.module.css — 추가 규칙
.card {
  min-inline-size: 0;
}

.cardTitle,
.cardDescription {
  overflow-wrap: anywhere;
}

검증 순서: 반응형·스코프 점검

반응형 카드 목록 검증 추적표 — 상태 추적

실제 All 6개에서 프론트엔드 id 1/5 두 개로 바뀌며 filterCategory/배열/개수 문구/aria-pressed가 같은 새 상태를 읽는다. 스타일 진단 표는 전체 관계도와 표와 인쇄에 보존한다.

카드 오류는 데이터에서 DOM·계산 스타일로 추적합니다

CARD LIST · VERIFICATION TRACE

카드 오류는 데이터에서 DOM·계산 스타일로 추적합니다

필터 결과·개수와 목록을 먼저 대조하고, 최종 DOM 속성과 Computed를 확인합니다. 반응형 배치와 키보드·대체 텍스트·사용자 모션 선호도 함께 점검합니다.

같은 필터 결과에서 개수와 DOM이 나온다

같은 필터 결과에서 개수와 DOM이 나온다선택한 필터는 aria-pressed=true입니다. key는 부모의 형제 식별이며 자식 prop이 아닙니다. $active는 DOM에 전달하지 않습니다. 생성 hash를 고정하지 않고 실제 mapping·DOM·계산 스타일을 확인합니다.mockCards + 선택filterCategoryfilteredCards프론트엔드 id 1,5개수2부모 keyCardItemcard propul / li + 상태선택 상태 truemapping → DOMComputed 확인
같은 필터 결과에서 개수와 DOM이 나온다선택한 필터는 aria-pressed=true입니다. key는 부모의 형제 식별이며 자식 prop이 아닙니다. $active는 DOM에 전달하지 않습니다. 생성 hash를 고정하지 않고 실제 mapping·DOM·계산 스타일을 확인합니다.mockCards +선택filterCategoryfilteredCards프론트엔드 id1,5개수2부모 keyCardItemcardpropul / li + 상태선택 상태 truemapping → DOMComputed 확인

선택한 필터는 aria-pressed=true입니다. key는 부모의 형제 식별이며 자식 prop이 아닙니다. $active는 DOM에 전달하지 않습니다. 생성 hash를 고정하지 않고 실제 mapping·DOM·계산 스타일을 확인합니다.

원래 설명과 상세 기준

본문 프론트엔드 필터 · 검증 시작점

All: 카드 6개
프론트엔드: id 1, 5 → 카드 2개
status: "프론트엔드 카테고리: 2개"
선택 버튼: aria-pressed=true

안정 id는 부모 key이며 자식이 key prop을 받는 것은 아닙니다. 생성 hash를 고정하기보다 mapping·DOM·계산 스타일의 연결을 확인합니다.

반응형 카드 목록 구현의 검증 순서와 통과 기준
순서 확인 위치 통과 기준
01 · 데이터와 필터 mockCards, filterCategory, filteredCards 각 카드에 안정적인 id와 필요한 필드가 있고, 선택한 카테고리와 결과 목록·개수가 일치합니다.
02 · prop과 DOM 부모의 key, CardItem card, 렌더된 ul/li와 live 결과 문구 key는 형제 사이에서 안정적이고 고유하며 자식 prop으로 기대하지 않습니다. 목록과 결과 개수는 의미 있는 요소로 노출됩니다.
03 · 스타일 계산 전역 CSS, styled-components, CSS 모듈, 배지의 style, DevTools의 Computed $active는 DOM에 전달되지 않고 aria-pressed와 상태가 일치합니다. 인라인 선언은 계산된 배경색 한 값뿐입니다.
04 · 반응형과 접근성 Grid/media query, 키보드 초점, 이미지 대체 텍스트, 명암, reduced motion 가로 넘침 없이 폭에 맞춰 열이 재배치되고 576px 이하에서는 한 열입니다. 초점이 보이고 중복 이미지 설명이 없으며 상태·개수·모션이 사용자 설정에 맞습니다.
  1. 데이터와 필터

    mockCards의 안정적인 id와 필드를 확인하고, 선택 카테고리·결과 목록·개수가 같은 상태를 가리키는지 비교합니다.

  2. prop과 DOM

    key는 부모의 형제 목록에서만 식별에 쓰입니다. 결과가 ul/li 목록과 live 상태 문구로 노출되는지 확인합니다.

  3. 스타일 계산

    전역·styled-components·CSS 모듈·인라인 선언을 Computed에서 추적합니다. $active는 DOM에서 빠지고 aria-pressed가 실제 선택 상태와 일치해야 합니다.

  4. 반응형과 접근성

    가로 넘침, 576px 이하 한 열, 키보드 초점, 중복 낭독 없는 이미지, 텍스트 명암, reduced motion 대응을 함께 시험합니다.

CSS 모듈의 생성 클래스 문자열은 빌드 설정에 따라 달라질 수 있습니다. 특정 해시보다 import된 mapping과 최종 DOM·Computed 결과를 검증합니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
원도표의 전체 관계·조건·설명
구분내용
원도표 표 행순서 | 확인 위치 | 통과 기준
원도표 표 행01 · 데이터와 필터 | mockCards, filterCategory, filteredCards | 각 카드에 안정적인 id와 필요한 필드가 있고, 선택한 카테고리와 결과 목록·개수가 일치합니다.
원도표 표 행02 · prop과 DOM | 부모의 key, CardItem card, 렌더된 ul/li와 live 결과 문구 | key는 형제 사이에서 안정적이고 고유하며 자식 prop으로 기대하지 않습니다. 목록과 결과 개수는 의미 있는 요소로 노출됩니다.
원도표 표 행03 · 스타일 계산 | 전역 CSS, styled-components, CSS 모듈, 배지의 style, DevTools의 Computed | $active는 DOM에 전달되지 않고 aria-pressed와 상태가 일치합니다. 인라인 선언은 계산된 배경색 한 값뿐입니다.
원도표 표 행04 · 반응형과 접근성 | Grid/media query, 키보드 초점, 이미지 대체 텍스트, 명암, reduced motion | 가로 넘침 없이 폭에 맞춰 열이 재배치되고 576 px 이하에서는 한 열입니다. 초점이 보이고 중복 이미지 설명이 없으며 상태·개수·모션이 사용자 설정에 맞습니다.
원도표 설명mockCards의 안정적인 id와 필드를 확인하고, 선택 카테고리·결과 목록·개수가 같은 상태를 가리키는지 비교합니다.
원도표 조건·주석데이터와 필터 mockCards의 안정적인 id와 필드를 확인하고, 선택 카테고리·결과 목록·개수가 같은 상태를 가리키는지 비교합니다.
원도표 설명key는 부모의 형제 목록에서만 식별에 쓰입니다. 결과가 ul/li 목록과 live 상태 문구로 노출되는지 확인합니다.
원도표 조건·주석prop과 DOM key는 부모의 형제 목록에서만 식별에 쓰입니다. 결과가 ul/li 목록과 live 상태 문구로 노출되는지 확인합니다.
원도표 설명전역·styled-components·CSS 모듈·인라인 선언을 Computed에서 추적합니다. $active는 DOM에서 빠지고 aria-pressed가 실제 선택 상태와 일치해야 합니다.
원도표 조건·주석스타일 계산 전역·styled-components·CSS 모듈·인라인 선언을 Computed에서 추적합니다. $active는 DOM에서 빠지고 aria-pressed가 실제 선택 상태와 일치해야 합니다.
원도표 설명가로 넘침, 576 px 이하 한 열, 키보드 초점, 중복 낭독 없는 이미지, 텍스트 명암, reduced motion 대응을 함께 시험합니다.
원도표 조건·주석반응형과 접근성 가로 넘침, 576 px 이하 한 열, 키보드 초점, 중복 낭독 없는 이미지, 텍스트 명암, reduced motion 대응을 함께 시험합니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
All 목록filter: All / count: 6전체 6개 카드를 표시한다UI는 같은 필터 원천을 읽는다
프론트엔드 선택대상: id 1,5 / button: aria-pressed=truefilterCategory가 프론트엔드로 바뀐다transient $active는 DOM 속성이 아니다
일치한 출력count: 2 / 목록: id 1,5카드 두 개와 프론트엔드 카테고리 2개 문구를 표시한다key는 부모 추적용이며 자식 prop이 아니다

반응형 카드 목록 검증 추적표 — 상태 추적

실제 All 6개에서 프론트엔드 id 1/5 두 개로 바뀌며 filterCategory/배열/개수 문구/aria-pressed가 같은 새 상태를 읽는다. 스타일 진단 표는 전체 관계도와 표와 인쇄에 보존한다.

카드 오류는 데이터에서 DOM·계산 스타일로 추적합니다

CARD LIST · VERIFICATION TRACE

카드 오류는 데이터에서 DOM·계산 스타일로 추적합니다

필터 결과·개수와 목록을 먼저 대조하고, 최종 DOM 속성과 Computed를 확인합니다. 반응형 배치와 키보드·대체 텍스트·사용자 모션 선호도 함께 점검합니다.

같은 필터 결과에서 개수와 DOM이 나온다

같은 필터 결과에서 개수와 DOM이 나온다선택한 필터는 aria-pressed=true입니다. key는 부모의 형제 식별이며 자식 prop이 아닙니다. $active는 DOM에 전달하지 않습니다. 생성 hash를 고정하지 않고 실제 mapping·DOM·계산 스타일을 확인합니다.mockCards + 선택filterCategoryfilteredCards프론트엔드 id 1,5개수2부모 keyCardItemcard propul / li + 상태선택 상태 truemapping → DOMComputed 확인
같은 필터 결과에서 개수와 DOM이 나온다선택한 필터는 aria-pressed=true입니다. key는 부모의 형제 식별이며 자식 prop이 아닙니다. $active는 DOM에 전달하지 않습니다. 생성 hash를 고정하지 않고 실제 mapping·DOM·계산 스타일을 확인합니다.mockCards +선택filterCategoryfilteredCards프론트엔드 id1,5개수2부모 keyCardItemcardpropul / li + 상태선택 상태 truemapping → DOMComputed 확인

선택한 필터는 aria-pressed=true입니다. key는 부모의 형제 식별이며 자식 prop이 아닙니다. $active는 DOM에 전달하지 않습니다. 생성 hash를 고정하지 않고 실제 mapping·DOM·계산 스타일을 확인합니다.

원래 설명과 상세 기준

본문 프론트엔드 필터 · 검증 시작점

All: 카드 6개
프론트엔드: id 1, 5 → 카드 2개
status: "프론트엔드 카테고리: 2개"
선택 버튼: aria-pressed=true

안정 id는 부모 key이며 자식이 key prop을 받는 것은 아닙니다. 생성 hash를 고정하기보다 mapping·DOM·계산 스타일의 연결을 확인합니다.

반응형 카드 목록 구현의 검증 순서와 통과 기준
순서 확인 위치 통과 기준
01 · 데이터와 필터 mockCards, filterCategory, filteredCards 각 카드에 안정적인 id와 필요한 필드가 있고, 선택한 카테고리와 결과 목록·개수가 일치합니다.
02 · prop과 DOM 부모의 key, CardItem card, 렌더된 ul/li와 live 결과 문구 key는 형제 사이에서 안정적이고 고유하며 자식 prop으로 기대하지 않습니다. 목록과 결과 개수는 의미 있는 요소로 노출됩니다.
03 · 스타일 계산 전역 CSS, styled-components, CSS 모듈, 배지의 style, DevTools의 Computed $active는 DOM에 전달되지 않고 aria-pressed와 상태가 일치합니다. 인라인 선언은 계산된 배경색 한 값뿐입니다.
04 · 반응형과 접근성 Grid/media query, 키보드 초점, 이미지 대체 텍스트, 명암, reduced motion 가로 넘침 없이 폭에 맞춰 열이 재배치되고 576px 이하에서는 한 열입니다. 초점이 보이고 중복 이미지 설명이 없으며 상태·개수·모션이 사용자 설정에 맞습니다.
  1. 데이터와 필터

    mockCards의 안정적인 id와 필드를 확인하고, 선택 카테고리·결과 목록·개수가 같은 상태를 가리키는지 비교합니다.

  2. prop과 DOM

    key는 부모의 형제 목록에서만 식별에 쓰입니다. 결과가 ul/li 목록과 live 상태 문구로 노출되는지 확인합니다.

  3. 스타일 계산

    전역·styled-components·CSS 모듈·인라인 선언을 Computed에서 추적합니다. $active는 DOM에서 빠지고 aria-pressed가 실제 선택 상태와 일치해야 합니다.

  4. 반응형과 접근성

    가로 넘침, 576px 이하 한 열, 키보드 초점, 중복 낭독 없는 이미지, 텍스트 명암, reduced motion 대응을 함께 시험합니다.

CSS 모듈의 생성 클래스 문자열은 빌드 설정에 따라 달라질 수 있습니다. 특정 해시보다 import된 mapping과 최종 DOM·Computed 결과를 검증합니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.

관계도의 보충 설명

원도표의 전체 관계·조건·설명
구분내용
원도표 표 행순서 | 확인 위치 | 통과 기준
원도표 표 행01 · 데이터와 필터 | mockCards, filterCategory, filteredCards | 각 카드에 안정적인 id와 필요한 필드가 있고, 선택한 카테고리와 결과 목록·개수가 일치합니다.
원도표 표 행02 · prop과 DOM | 부모의 key, CardItem card, 렌더된 ul/li와 live 결과 문구 | key는 형제 사이에서 안정적이고 고유하며 자식 prop으로 기대하지 않습니다. 목록과 결과 개수는 의미 있는 요소로 노출됩니다.
원도표 표 행03 · 스타일 계산 | 전역 CSS, styled-components, CSS 모듈, 배지의 style, DevTools의 Computed | $active는 DOM에 전달되지 않고 aria-pressed와 상태가 일치합니다. 인라인 선언은 계산된 배경색 한 값뿐입니다.
원도표 표 행04 · 반응형과 접근성 | Grid/media query, 키보드 초점, 이미지 대체 텍스트, 명암, reduced motion | 가로 넘침 없이 폭에 맞춰 열이 재배치되고 576 px 이하에서는 한 열입니다. 초점이 보이고 중복 이미지 설명이 없으며 상태·개수·모션이 사용자 설정에 맞습니다.
원도표 설명mockCards의 안정적인 id와 필드를 확인하고, 선택 카테고리·결과 목록·개수가 같은 상태를 가리키는지 비교합니다.
원도표 조건·주석데이터와 필터 mockCards의 안정적인 id와 필드를 확인하고, 선택 카테고리·결과 목록·개수가 같은 상태를 가리키는지 비교합니다.
원도표 설명key는 부모의 형제 목록에서만 식별에 쓰입니다. 결과가 ul/li 목록과 live 상태 문구로 노출되는지 확인합니다.
원도표 조건·주석prop과 DOM key는 부모의 형제 목록에서만 식별에 쓰입니다. 결과가 ul/li 목록과 live 상태 문구로 노출되는지 확인합니다.
원도표 설명전역·styled-components·CSS 모듈·인라인 선언을 Computed에서 추적합니다. $active는 DOM에서 빠지고 aria-pressed가 실제 선택 상태와 일치해야 합니다.
원도표 조건·주석스타일 계산 전역·styled-components·CSS 모듈·인라인 선언을 Computed에서 추적합니다. $active는 DOM에서 빠지고 aria-pressed가 실제 선택 상태와 일치해야 합니다.
원도표 설명가로 넘침, 576 px 이하 한 열, 키보드 초점, 중복 낭독 없는 이미지, 텍스트 명암, reduced motion 대응을 함께 시험합니다.
원도표 조건·주석반응형과 접근성 가로 넘침, 576 px 이하 한 열, 키보드 초점, 중복 낭독 없는 이미지, 텍스트 명암, reduced motion 대응을 함께 시험합니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
All 목록filter: All / count: 6전체 6개 카드를 표시한다UI는 같은 필터 원천을 읽는다
프론트엔드 선택대상: id 1,5 / button: aria-pressed=truefilterCategory가 프론트엔드로 바뀐다transient $active는 DOM 속성이 아니다
일치한 출력count: 2 / 목록: id 1,5카드 두 개와 프론트엔드 카테고리 2개 문구를 표시한다key는 부모 추적용이며 자식 prop이 아니다

위 그림은 카드 목록에서 문제가 생겼을 때 원인을 좁혀 가는 순서를 보여 줍니다. 위쪽의 흐름처럼 mockCards와 선택한 filterCategory에서 filteredCards가 나오고, “프론트엔드”라면 id 1과 5의 두 장이 됩니다. 이 같은 필터 결과에서 개수 2와, 부모의 key로 구별된 CardItem의 ul/li가 함께 나오며, 선택된 버튼은 aria-pressed=true입니다. 마지막으로 대응표에서 DOM으로, 다시 Computed 패널로 실제 계산된 스타일을 확인합니다.

그림 아래의 표는 확인할 순서를 네 단계로 정리합니다. 첫째는 데이터와 필터로, mockCards의 각 카드에 바뀌지 않는 id와 필요한 필드가 있고 선택한 카테고리와 결과 목록, 개수가 맞는지 봅니다. 둘째는 prop과 DOM으로, key가 형제 사이에서 고유하고 바뀌지 않으며 자식의 prop으로 기대하지 않는지, 목록과 결과 문구가 의미 있는 요소로 드러나는지 봅니다. 셋째는 스타일 계산으로, 전역 CSS와 styled-components, CSS 모듈, 배지의 style을 개발자 도구의 Computed에서 따라가며, $active가 DOM에 넘어가지 않고 aria-pressed와 상태가 일치하는지, 인라인 선언이 계산된 배경색 하나뿐인지 확인합니다. 넷째는 반응형과 접근성으로, 가로 넘침 없이 폭에 맞춰 열이 다시 배치되고 576px 이하에서는 한 열이 되는지, 포커스가 보이고 이미지가 중복해서 읽히지 않으며 명암과 모션이 사용자 설정에 맞는지 봅니다.

실습에서는 이 순서대로, 먼저 화면 크기별 레이아웃이 깨지지 않는지 보고 다음으로 클래스 충돌과 동적 스타일의 반영을 점검합니다. 모든 코드 파일을 경로에 맞게 만든 뒤 npm run dev로 개발 서버를 띄우고 터미널의 주소를 엽니다. 카드들이 Grid로 배열되어 있는지 보고, 위쪽의 필터 버튼을 눌러 해당 카테고리의 카드만 보이는지, 그때 $active에 따른 styled-components 규칙과 DOM의 aria-pressed가 함께 바뀌는지 확인합니다.

다음으로 브라우저 창의 폭을 바꾸거나 개발자 도구의 반응형 모드를 씁니다. auto-fit은 쓸 수 있는 공간과 최소 열 폭에 따라 열 수를 정하므로 특정 폭에서 언제나 3열이나 2열이 된다고 단정하지 말고, 576px 이하에서 한 열로 바뀌며 카드의 제목과 설명 크기도 바뀌는지 확인합니다. 개발자 도구에서는 styles.card가 실제 지역 클래스 이름으로 대응되고 styled-components의 규칙도 만들어졌는지 보되, 정확한 클래스 문자열보다는 Styles와 Computed 패널에서 어떤 규칙이 적용되고 덮어써졌는지를 확인합니다. 카테고리 배지에는 background-color 하나만 인라인으로 적용되고, 글자색과 여백, 모양은 CSS 모듈의 규칙에서 오는지도 봅니다.

마지막으로 접근성입니다. 키보드로 모든 필터 버튼에 도달해 누를 수 있는지, 포커스 표시와 aria-pressed가 보이는 상태와 일치하는지, 결과가 ul과 li 목록으로 드러나는지, 장식용 이미지가 빈 대체 텍스트를 가지는지 확인합니다. 운영체제의 모션 줄이기 설정을 켠 환경에서는 카드가 위로 움직이는 효과도 꺼져야 합니다.


정리

  • 전역 CSS에는 모든 화면이 공유할 기본값만, styled-components에는 목록의 레이아웃과 prop에 따른 컨트롤의 상태를, CSS 모듈에는 카드 안의 정적 규칙을, 인라인 스타일에는 데이터에서 계산한 값 하나만 둡니다.
  • 모든 방식이 만든 CSS는 같은 문서의 캐스케이드에 참여하므로, 같은 속성을 정하는 쪽을 하나로 둡니다.
  • repeat(auto-fit, minmax(...))는 쓸 수 있는 폭에 따라 열 수를 계산하므로, 열 수는 실제 폭과 간격으로 계산해 확인합니다.
  • 스타일 전용 $active와 실제 상태를 알리는 aria-pressed를 구별하고, 부모의 key는 자식의 prop이 아닙니다.
  • 필터는 원래 데이터를 지우지 않고 렌더할 때마다 결과를 계산하며, 문제가 생기면 데이터에서 DOM과 계산된 스타일의 순서로 추적합니다.

연습 문제

1. 새 카테고리 추가하기

기존 여섯 개의 데이터는 그대로 두고, 고유한 id 7과 카테고리 '미분류'를 가진 카드 하나를 더합니다. 다른 필드는 기존 카드와 같은 형식의 올바른 값입니다. All의 개수, 필터 버튼의 개수와 새 필터를 골랐을 때의 결과, 새 카드 배지의 배경색은 무엇일까요? 색상 대응표는 아직 고치지 않았습니다.

해설 보기

All은 7개입니다. 카테고리를 모은 Set에 새 값이 더해져 기존 다섯 카테고리와 All에 “미분류”가 추가되므로 필터 버튼은 모두 7개이며, 새 필터의 결과는 id 7의 카드 한 장입니다. 색상 대응표에 그 카테고리가 없으므로 인라인 배경색은 대체값 #475467이 됩니다.

새 id가 형제 목록의 key를 고유하게 유지합니다. 새 카테고리 이름으로 CSS 클래스가 저절로 만들어진다고 가정하지 마세요. 목록의 필터, 자식의 데이터, 색상 대응표, DOM의 상태를 각각 따라가면 데이터를 더했을 때의 영향을 설명할 수 있습니다.

2. 900px에서의 열 수

뷰포트가 900px일 때 처음의 카드 여섯 장은 몇 열이 될까요? 같은 가정에서 계산해 보세요.

해설 보기

900px은 992px 이하이므로 최소 열 폭은 250px, 간격은 20px, 목록의 안쪽 여백은 15px이 됩니다. .App의 폭 900px에서 좌우 안쪽 여백 40px을 빼면 860px이고, 목록의 안쪽 여백 30px을 빼면 Grid의 내용 폭은 830px입니다.

세 열에 필요한 폭은 250 × 3 + 20 × 2 = 790px이라 들어가고, 네 열은 250 × 4 + 20 × 3 = 1060px이라 들어가지 않으므로 세 열입니다. 992px 이하에서 최소 열 폭을 줄였기 때문에 화면이 좁아졌는데도 세 열을 유지합니다.

이것으로 5장을 마칩니다. 5장에서는 className과 style의 역할, 클래스 이름을 지역화하는 CSS 모듈, 자바스크립트 안에서 스타일을 작성하는 styled-components, 의미 있는 HTML 위에 유연한 크기와 쿼리를 쌓는 반응형 디자인을 익히고, 이 방법들을 나눠 맡겨 반응형 카드 목록을 만들었습니다. 다음 장의 SPA 이해부터는 단일 페이지 애플리케이션의 개념과 React Router로 여러 화면을 오가는 라우팅을 다룹니다.