본문 바로가기

반응형 디자인 기초

안정적인 HTML 의미 위에 유연한 크기, Flexbox·Grid, 미디어·컨테이너 쿼리를 조합하고 콘텐츠 기준 breakpoint를 검증합니다.

앞 글에서는 styled-components로 스타일 규칙을 React 컴포넌트 가까이에 두는 방법을 배웠습니다. 이번에는 반응형 디자인을 다룹니다. 반응형 디자인은 React의 별도 기능이 아니라, 같은 콘텐츠와 동작을 여러 뷰포트와 입력 방식, 사용자 설정에서도 읽고 조작할 수 있게 만드는 웹 설계의 접근법입니다. React는 의미 있는 HTML을 렌더링하고, 브라우저는 CSS의 유연한 크기와 레이아웃 알고리즘, 조건부 쿼리를 계산합니다.

같은 의미와 동작 위에 반응형 CSS를 쌓습니다

RESPONSIVE CSS · STABLE MEANING

같은 의미와 동작 위에 반응형 CSS를 쌓습니다

콘텐츠와 DOM 순서를 기반으로 유연한 크기·레이아웃을 적용합니다. 기본 배치로 해결되지 않는 관계만 환경·컨테이너 조건으로 전환합니다.

본문 ResponsiveLayout · 순서는 그대로

DOM: 제목 → 카드 목록 → 보조 정보
좁은 폭: 목록 다음에 보조 정보
48rem 이상: 보조 정보가 옆 열
DOM 순서와 주요 정보는 유지

48rem은 이 예제의 관계 전환 값입니다. viewport·container·사용자 선호 조건을 구분하며, 확대와 논리적 읽기 순서는 계속 확인합니다.

반응형 설계 책임 레이어환경 신호, 필요한 조건부 규칙, 유연한 배치가 콘텐츠와 DOM이라는 불변 기반 위에 놓입니다. 왼쪽 방향 표시는 기반 쪽으로 내려가는 관계이며 실행 단계의 시간 순서가 아닙니다. L4 · 환경 신호viewport · printpreference · inputL3 · 조건부 CSS@media · @container필요한 관계만 전환L2 · 유연한 배치normal flowFlexbox · Gridintrinsic sizeL1 · 콘텐츠·DOMmeaning · source orderactions · data환경에서 불변 기반으로
반응형 설계 책임 레이어환경 신호, 필요한 조건부 규칙, 유연한 배치가 콘텐츠와 DOM이라는 불변 기반 위에 놓입니다. 왼쪽 방향 표시는 기반 쪽으로 내려가는 관계이며 실행 단계의 시간 순서가 아닙니다. L4 · 환경 신호viewport · printpreference · inputL3 · 조건부 CSS@media · @container필요한 관계만 전환L2 · 유연한 배치normal flowFlexbox · Gridintrinsic sizeL1 · 콘텐츠·DOMmeaning · source orderactions · data환경에서 불변 기반으로

환경 신호, 필요한 조건부 규칙, 유연한 배치가 콘텐츠와 DOM이라는 불변 기반 위에 놓입니다. 왼쪽 방향 표시는 기반 쪽으로 내려가는 관계이며 실행 단계의 시간 순서가 아닙니다.

반응형 설계는 React에서 기기별 DOM을 따로 만드는 일이 아니라, 안정적인 의미 위에 적응형 CSS 표현을 쌓는 일입니다.

위 그림은 반응형 설계를 네 층으로 나눠 보여 줍니다. 가장 아래의 L1은 콘텐츠와 DOM으로, 의미와 소스 순서, 동작과 데이터처럼 화면 폭이 바뀌어도 변하지 않아야 하는 기반입니다. 그 위의 L2는 일반 문서 흐름과 Flexbox, Grid, 콘텐츠 고유의 크기로 이루어진 유연한 배치이고, L3는 @media와 @container로 필요한 관계만 바꾸는 조건부 CSS이며, 가장 위의 L4는 뷰포트와 인쇄, 사용자 선호와 입력 방식 같은 환경의 신호입니다. 그림의 화살표는 환경에서 변하지 않는 기반 쪽으로 내려가는 관계를 나타낼 뿐, 실행되는 순서를 뜻하지는 않습니다.

그림 위쪽의 예는 이 글의 ResponsiveLayout입니다. DOM의 순서는 제목, 카드 목록, 보조 정보이며, 좁은 폭에서는 보조 정보가 목록 다음에 오고 48rem 이상에서는 보조 정보가 옆 열로 옮겨 갑니다. 이때도 DOM의 순서와 주요 정보는 그대로입니다. 그림의 설명처럼 48rem은 이 예제에서 관계를 바꾸는 값일 뿐이며, 반응형 설계는 React에서 기기마다 다른 DOM을 만드는 일이 아니라 안정적인 의미 위에 적응하는 CSS 표현을 쌓는 일입니다.


먼저 고정할 반응형의 기본

화면이 좁아져도 제목과 라벨, 상태, 주요 동작의 의미가 사라져서는 안 됩니다. 먼저 CSS가 없어도 읽는 순서와 키보드로 이동하는 순서가 자연스러운 HTML을 만들고, 그 위에 표현을 더합니다. 그다음 세 가지를 갖춥니다. 첫째는 유연한 크기로, 고정된 화면 폭을 가정하지 않고 max-inline-size, %, fr, minmax(), clamp()처럼 주변 공간과의 관계를 나타냅니다. 둘째는 유연한 미디어로, 이미지와 영상이 컨테이너를 넘지 않게 하며 기본적인 이미지에는 max-inline-size: 100%; block-size: auto;를 적용할 수 있습니다. 셋째는 조건부 전환으로, 기본 흐름만으로 해결되지 않는 지점에만 미디어 쿼리나 컨테이너 쿼리를 더합니다.

모든 값을 상대 단위로 바꾸는 것이 목표는 아닙니다. 글자와 여백, 컨테이너, 선 두께처럼 그 값이 무엇과 관계를 맺는지에 따라 단위를 고릅니다. 특히 글자 크기를 vw만으로 정하면 작은 화면이나 확대한 환경에서 지나치게 작아질 수 있으므로 rem과 clamp()를 함께 고려합니다.


메타 뷰포트 태그

좁은 화면의 모바일 브라우저가 데스크톱용 가상 뷰포트로 페이지를 축소해 보여 주면, 작은 폭용으로 작성한 CSS가 예상대로 선택되지 않을 수 있습니다. 그래서 반응형 페이지의 HTML head에는 보통 다음 설정을 둡니다. Vite 프로젝트에서는 public 폴더가 아니라 프로젝트 루트의 index.html이 기본 HTML 진입점이며(Vite의 index.html과 프로젝트 루트), 템플릿에 이 태그가 이미 들어 있습니다.

index.html
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1" />
</head>

width=device-width는 레이아웃 뷰포트의 폭을 기기 화면 폭의 CSS 픽셀에 맞추고, initial-scale=1은 문서를 의도한 크기의 100% 배율로 처음 보여 주라는 힌트입니다. 하드웨어 픽셀과 CSS 픽셀이 언제나 1:1이라는 뜻은 아닙니다(MDN의 viewport meta). 사용자의 확대를 막는 user-scalable=no나 지나친 maximum-scale 제한은 넣지 않습니다. 확대는 작은 글자를 읽는 중요한 접근성 수단입니다.


미디어 쿼리와 breakpoint

미디어 쿼리는 뷰포트의 폭뿐 아니라 출력 매체, 방향, 해상도, hover와 pointer 같은 입력의 특성, 모션 줄이기 같은 사용자 선호를 조건으로 CSS를 고릅니다(MDN의 미디어 쿼리 사용하기). 아래 코드는 그 가운데 두 가지를 보여 줍니다. 첫 번째 규칙은 조건 없이 한 열 Grid를 기본값으로 두고, @media (min-width: 48rem)은 뷰포트가 그 폭 이상일 때만 두 열로 바꿉니다. 두 번째 쿼리는 화면 크기가 아니라, 운영 체제에서 모션 줄이기를 켠 사용자에게만 전환 효과를 끕니다.

/* 좁은 폭의 한 열이 기본값 */
.page-layout {
  display: grid;
  gap: 1rem;
}

/* 실제 콘텐츠가 두 열을 감당하는 지점부터 확장 */
@media (min-width: 48rem) {
  .page-layout {
    grid-template-columns: minmax(0, 1fr) 18rem;
  }
}

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

48rem은 특정 태블릿 모델의 이름이 아니라, 이 예제의 콘텐츠에서 두 열이 편안해지는 지점입니다. 가장 긴 제목이나 번역된 문구, 실제 숫자와 URL을 넣고 폭을 조금씩 바꿔 가며, 줄바꿈이나 넘침, 조작의 문제가 처음 생기는 지점을 찾습니다.

콘텐츠가 요구하는 경계에서만 breakpoint를 추가합니다

BREAKPOINT · CONTENT CONTRACT

콘텐츠가 요구하는 경계에서만 breakpoint를 추가합니다

좁은 폭의 기본 배치를 실제 제목·URL·숫자·빈 상태로 확인합니다. 읽기·조작·줄바꿈이 유지되는 분기와 깨져 전환이 필요한 분기를 모두 검증합니다.

본문 48rem · 목록과 보조 영역의 관계

기본: page-layout 한 열
min-width: 48rem → 본문 + 18rem 보조 열
auto-fit 카드 열은 공간에 따라 자동 조정

카드마다 query를 넣는 예제가 아닙니다. 전환 바로 아래·위, 중간 폭과 확대에서 긴 실제 제목의 읽기·조작을 다시 확인합니다.

콘텐츠 기반 breakpoint 선택 흐름실제 콘텐츠를 넣어 읽기와 조작이 유지되면 query 없이 종결합니다. 깨지는 분기에서만 경계를 추가하고 그 양쪽과 중간 폭을 다시 검증합니다. 48rem은 예제의 목록과 보조 영역 관계를 바꾸는 조건입니다. 좁은 폭 기본값논리적 DOM 순서실제 콘텐츠 검증긴 제목·URL·숫자연속해서 폭 바꾸기읽기·조작·줄바꿈유지되는가?query 없이 유지유연한 배치로 충분경계에 query 추가필요한 관계만 전환경계 전후 재검증바로 아래·위·중간유지깨짐
콘텐츠 기반 breakpoint 선택 흐름실제 콘텐츠를 넣어 읽기와 조작이 유지되면 query 없이 종결합니다. 깨지는 분기에서만 경계를 추가하고 그 양쪽과 중간 폭을 다시 검증합니다. 48rem은 예제의 목록과 보조 영역 관계를 바꾸는 조건입니다. 좁은 폭 기본값논리적 DOM 순서실제 콘텐츠 검증긴 제목·URL·숫자연속해서 폭 바꾸기읽기·조작·줄바꿈유지되는가?query 없이 유지유연한 배치로 충분경계에 query 추가필요한 관계만 전환경계 전후 재검증바로 아래·위·중간유지깨짐

실제 콘텐츠를 넣어 읽기와 조작이 유지되면 query 없이 종결합니다. 깨지는 분기에서만 경계를 추가하고 그 양쪽과 중간 폭을 다시 검증합니다. 48rem은 예제의 목록과 보조 영역 관계를 바꾸는 조건입니다.

기기 이름은 검증 대상이지 계약이 아님
48rem 같은 값은 예제 콘텐츠의 경계입니다. 새로운 폭과 확대 환경도 계속 통과해야 합니다.
breakpoint 수보다 빈 구간이 중요
등록한 몇 개 폭만 캡처하지 말고 두 breakpoint 사이에서 overflow와 어색한 밀도가 없는지 확인합니다.

좋은 breakpoint는 특정 기기 목록을 외운 숫자가 아니라 콘텐츠, 레이아웃, 조작 계약의 검증 결과입니다.

위 그림은 콘텐츠에서 breakpoint를 찾는 확인의 흐름입니다. 좁은 폭의 기본값과 논리적인 DOM 순서에서 시작해, 긴 제목과 URL, 숫자 같은 실제 콘텐츠를 넣고 폭을 연속해서 바꿔 봅니다. 읽기와 조작, 줄바꿈이 유지된다면 쿼리 없이 유연한 배치로 충분하므로 거기서 끝납니다. 깨지는 지점이 있을 때만 그 지점에 쿼리를 더해 필요한 관계만 바꾸고, 그 지점의 바로 아래와 위, 그 사이의 폭에서 다시 확인합니다.

그림 위쪽의 예처럼 이 글의 48rem은 page-layout을 한 열에서 본문과 18rem의 보조 열로 바꾸는 조건이며, 카드의 열 수는 auto-fit이 공간에 따라 따로 맞춥니다. 카드마다 쿼리를 넣는 예제가 아닙니다. 그림 아래 칸이 덧붙이듯, 기기의 이름은 확인할 대상일 뿐 기준이 아니며, 등록해 둔 몇 개의 폭만 캡처해 볼 것이 아니라 두 breakpoint 사이의 구간에서도 넘침이나 어색한 밀도가 없는지 확인해야 합니다.

모바일 우선(mobile-first)은 좁은 폭의 단순한 규칙을 기본값으로 두고, min-width 조건에서 넓은 폭의 레이아웃을 더하는 작성 순서입니다. 덮어쓰는 관계를 단순하게 만드는 데 도움이 될 수 있지만, 그 자체가 더 빠른 페이지를 보장하지는 않습니다(MDN의 반응형 웹 디자인).

예제: 유연한 Grid와 콘텐츠 기준 전환

src/components/ResponsiveLayout.jsx와 src/components/ResponsiveLayout.css를 만듭니다. 컴포넌트는 앞의 .page-layout 규칙을 그대로 쓰고, 그 안에 카드 목록(ul)과 보조 정보(aside)를 차례로 둡니다. DOM 순서가 목록 다음에 보조 정보이므로, 좁은 폭의 한 열에서도 화면 낭독기와 키보드가 화면과 같은 순서로 내용을 읽습니다.

src/components/ResponsiveLayout.jsx
import './ResponsiveLayout.css';

export default function ResponsiveLayout() {
  return (
    <section className="responsive-container" aria-label="반응형 카드 목록">
      <h2>반응형 카드 목록</h2>
      <div className="page-layout">
        <ul className="boxes-wrapper" role="list">
          <li className="box">가장 긴 실제 제목</li>
          <li className="box">상태와 요약</li>
          <li className="box">주요 동작</li>
        </ul>
        <aside className="supporting-info">
          넓은 폭에서는 옆 열에 놓이고, 좁은 폭에서는 목록 뒤에 이어집니다.
        </aside>
      </div>
    </section>
  );
}
src/components/ResponsiveLayout.css
.responsive-container {
  inline-size: min(calc(100% - 2rem), 75rem);
  margin-inline: auto;
  padding-block: clamp(1rem, 3vw, 2rem);
}

.page-layout {
  display: grid;
  gap: 1rem;
}

.boxes-wrapper {
  display: grid;
  grid-template-columns: repeat(
    auto-fit,
    minmax(min(100%, 15rem), 1fr)
  );
  gap: clamp(0.75rem, 2vw, 1.25rem);
  list-style: none;
  margin: 0;
  padding: 0;
}

.box,
.supporting-info {
  border: 1px solid #cbd5e1;
  border-radius: 0.5rem;
  padding: 1rem;
}

.box {
  min-block-size: 7.5rem;
}

@media (min-width: 48rem) {
  .page-layout {
    grid-template-columns: minmax(0, 1fr) 18rem;
    align-items: start;
  }
}

auto-fit과 minmax()가 공간에 맞춰 카드의 열 수를 알아서 바꾸므로, 카드마다 breakpoint를 따로 둘 필요가 없습니다. 미디어 쿼리는 목록과 보조 영역의 관계가 실제로 바뀌는 지점에만 썼습니다.

48rem 양쪽에서 실제 공간 계산하기

앞의 ResponsiveLayout을 App.jsx에서 렌더링하고, 기본 글꼴 크기는 1rem = 16px, body의 여백은 0, 예제 바깥에 다른 폭 제한은 없다고 가정합니다. 카드의 내용은 위의 짧은 문자열 그대로입니다.

뷰포트가 767px이면 컨테이너의 폭은 767 − 32 = 735px입니다. 48rem인 768px보다 좁으므로 보조 정보는 목록 다음에 옵니다. 카드 사이의 간격은 2vw = 15.34px이고 최소 열 폭은 240px이므로, 두 열에 필요한 최소 폭은 240 × 2 + 15.34 = 495.34px, 세 열은 240 × 3 + 15.34 × 2 = 750.68px입니다. 그래서 카드 세 개는 두 열까지만 들어갑니다.

뷰포트가 768px이 되면 컨테이너는 736px이지만, 새로 생긴 288px의 보조 열과 바깥 Grid의 간격 16px이 먼저 공간을 씁니다. 목록에 남는 폭은 736 − 288 − 16 = 432px이므로, 카드 두 열에 필요한 495.36px을 확보하지 못해 목록은 한 열이 됩니다. 뷰포트가 넓어졌는데도 보조 영역이 옆에 생기면서 카드의 열 수가 줄어든 것입니다. 미디어 쿼리가 나누는 두 영역과, 안쪽 auto-fit이 정하는 카드의 열 수를 같은 값으로 생각하지 마세요.

CSS를 끈 읽기 순서는 두 경우 모두 제목, 목록, 보조 정보입니다. 개발자 도구에서 바깥의 .page-layout과 안쪽의 .boxes-wrapper Grid를 각각 골라 이 계산과 대조해 보세요. 루트 글자 크기나 주변의 여백을 바꾸면 수치도 다시 계산해야 합니다.

더 깊이: 미디어 쿼리의 rem이 따르는 글자 크기

미디어 쿼리 안의 rem과 em은 페이지의 CSS가 정한 글자 크기가 아니라 초기값을 기준으로 계산됩니다. 미디어 쿼리 명세는 쿼리의 상대 길이 단위가 선언의 결과가 아니라 초기값을 따른다고 정하며, HTML에서는 브라우저의 기본값이나 사용자가 설정한 글자 크기가 그 초기값입니다. 그래서 html { font-size: 62.5%; }처럼 루트의 글자 크기를 바꿔도 @media (min-width: 48rem)의 전환 지점은 그대로입니다. 반대로 사용자가 브라우저의 기본 글자 크기를 20px로 키우면, 같은 조건은 48 × 20 = 960px에서 바뀝니다.

컨테이너 쿼리는 기준이 다릅니다. CSS 포함(containment) 명세는 컨테이너 쿼리 조건의 상대 단위를 쿼리 컨테이너의 계산값으로 평가한다고 정하므로, @container (min-width: 30em)의 em은 그 컨테이너의 글자 크기를 따릅니다. 같은 em이라도 어느 쿼리에 쓰느냐에 따라 기준이 달라지므로, 글자 크기를 바꾼 영역에서 전환 지점이 예상과 다르다면 이 차이부터 확인합니다.


Flexbox와 Grid 선택

Flexbox는 한 번에 한 축, 곧 행이나 열 가운데 하나에서 공간을 나누고 정렬하는 1차원 레이아웃 모델로, 버튼 행이나 내비게이션, 한 줄의 메타 정보처럼 한 방향의 흐름이 중심일 때 알맞습니다(MDN의 Flexbox 기본 개념). Grid는 행과 열을 함께 다루는 2차원 레이아웃 모델로, 카드의 열을 맞추거나 본문과 사이드바처럼 두 축의 관계가 중요한 배치에 알맞습니다.

.action-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
}

.card-grid {
  display: grid;
  grid-template-columns: repeat(
    auto-fit,
    minmax(min(100%, 16rem), 1fr)
  );
  gap: 1rem;
}
축과 정렬 관계에 맞춰 Flexbox·Grid를 고릅니다

CSS LAYOUT · AXIS & RELATIONSHIP

축과 정렬 관계에 맞춰 Flexbox·Grid를 고릅니다

한 축의 공간 분배와 정렬, 행·열을 함께 맞추는 관계, 별도 배치가 필요 없는 문서 흐름을 구별합니다. 두 도구는 서로 다른 컨테이너에서 함께 쓸 수 있습니다.

본문 레이아웃 · 컨테이너별 책임

.action-row → flex + flex-wrap: wrap
.card-grid  → grid + auto-fit / minmax
읽기·키보드 순서 → 논리적인 DOM

Flexbox는 한 번에 한 축을 제어합니다. 여러 줄로 감싸도 Grid처럼 줄 사이 track 정렬을 공유하는 모델은 아닙니다.

Flexbox와 Grid 선택 흐름한 축의 분배는 Flexbox, 두 축의 track 관계는 Grid, 별도 관계가 없으면 normal flow로 끝납니다. 두 도구는 다른 컨테이너에서 함께 사용할 수 있으며 시각 배치가 논리적 DOM 순서를 바꾸지는 않습니다. 설계할 역할바꾸려는 계약 확인한 축에서분배·정렬하는가?두 축의 track을함께 맞추는가?Flexbox한 번에 행 또는 열wrap도 한 축 기준Grid행·열 track 관계normal flow별도 배치가 불필요예아니오예아니오
Flexbox와 Grid 선택 흐름한 축의 분배는 Flexbox, 두 축의 track 관계는 Grid, 별도 관계가 없으면 normal flow로 끝납니다. 두 도구는 다른 컨테이너에서 함께 사용할 수 있으며 시각 배치가 논리적 DOM 순서를 바꾸지는 않습니다. 설계할 역할바꾸려는 계약 확인한 축에서분배·정렬하는가?두 축의 track을함께 맞추는가?Flexbox한 번에 행 또는 열wrap도 한 축 기준Grid행·열 track 관계normal flow별도 배치가 불필요예아니오예아니오

한 축의 분배는 Flexbox, 두 축의 track 관계는 Grid, 별도 관계가 없으면 normal flow로 끝납니다. 두 도구는 다른 컨테이너에서 함께 사용할 수 있으며 시각 배치가 논리적 DOM 순서를 바꾸지는 않습니다.

CSS 배치가 source order를 대신하지 않습니다.

order, reverse 방향, Grid placement는 보이는 위치를 바꿀 수 있지만 기본 읽기·키보드 탐색 순서를 논리적으로 고치지 않습니다.

Flexbox와 Grid는 함께 쓸 수 있습니다. 다만 각 컨테이너에서는 먼저 지배적인 축과 관계를 정하고, 조건부 query는 그 관계가 실제로 바뀌는 경우에만 추가합니다.

위 그림은 콘텐츠의 관계에 맞춰 Flexbox와 Grid를 고르는 흐름입니다. 먼저 한 축에서 공간을 나누고 정렬하는지 묻고, 그렇다면 Flexbox를 고릅니다. Flexbox는 flex-wrap으로 여러 줄로 감싸더라도 한 축을 기준으로 하며, Grid처럼 줄 사이의 트랙 정렬을 공유하지 않습니다. 그렇지 않고 두 축의 트랙을 함께 맞춰야 한다면 Grid를, 둘 다 아니라면 별도의 배치 없이 일반 문서 흐름으로 끝냅니다. 그림 위쪽의 예처럼 .action-row는 flex와 flex-wrap: wrap을, .card-grid는 grid와 auto-fit, minmax를 쓰며, 읽기와 키보드 순서는 논리적인 DOM이 정합니다. 두 도구는 서로 다른 컨테이너에서 함께 쓸 수 있습니다.

그림 아래 칸이 강조하듯 CSS의 배치는 소스 순서를 대신하지 않습니다. Flexbox의 order나 row-reverse, Grid의 배치로 보이는 순서만 바꿔도 기본 DOM의 읽기 순서와 키보드 탐색 순서는 함께 바뀌지 않습니다(MDN의 Grid 레이아웃과 접근성). 논리적인 순서는 HTML 소스에 먼저 반영하고, CSS의 재배치는 의미를 바꾸지 않는 표현의 조정에만 씁니다.


미디어 쿼리와 컨테이너 쿼리

페이지 전체의 열 수처럼 뷰포트나 사용자 환경에 반응해야 한다면 @media가 알맞습니다. 하지만 같은 카드가 넓은 본문과 좁은 사이드바에 동시에 놓일 수 있다면, 뷰포트 폭만으로는 카드가 실제로 받은 공간을 알 수 없습니다. 이때는 크기 컨테이너(size container)를 선언하고 @container로 조상 컨테이너의 인라인 크기를 물을 수 있습니다(MDN의 CSS 컨테이너 쿼리).

.card-region {
  container-type: inline-size;
}

.responsive-card {
  display: grid;
  gap: 1rem;
}

@container (min-width: 32rem) {
  .responsive-card {
    grid-template-columns: 8rem minmax(0, 1fr);
  }
}

크기 쿼리는 자기 자신이 아니라 가장 가까운 알맞은 조상 쿼리 컨테이너를 기준으로, 그 안의 요소에 스타일을 적용합니다. 그래서 위 예제에서는 .card-region이 폭을 제공하고, 그 자손인 .responsive-card가 배치를 바꿉니다.


styled-components에서 반응형 CSS 사용

웹용 styled-components는 실제 스타일시트를 만들며 일반 CSS와 중첩(nesting)을 지원합니다. 그래서 템플릿 리터럴 안에 @media와 @container를 바로 쓸 수 있고, 단순한 예제라면 뷰포트를 읽는 자바스크립트 state나 도우미 함수가 따로 필요하지 않습니다.

src/components/StyledResponsiveExample.jsx
import styled from 'styled-components';

const PageLayout = styled.div`
  display: grid;
  gap: 1rem;

  @media (min-width: 48rem) {
    grid-template-columns: minmax(0, 1fr) 18rem;
  }
`;

const CardRegion = styled.section`
  container-type: inline-size;
`;

const ResponsiveCard = styled.article`
  display: grid;
  gap: 1rem;
  border: 1px solid #cbd5e1;
  border-radius: 0.5rem;
  padding: 1rem;

  @container (min-width: 32rem) {
    grid-template-columns: 8rem minmax(0, 1fr);
  }
`;

export default function StyledResponsiveExample() {
  return (
    <PageLayout>
      <CardRegion>
        <ResponsiveCard>
          <div aria-hidden="true">미리보기</div>
          <div>
            <h2>컨테이너 폭에 반응하는 카드</h2>
            <p>같은 뷰포트에서도 실제로 받은 공간에 따라 배치가 달라집니다.</p>
          </div>
        </ResponsiveCard>
      </CardRegion>
      <aside>보조 정보</aside>
    </PageLayout>
  );
}

여러 곳에서 함수 보간이 들어간 CSS 조각을 조합해야 한다면 styled-components의 css 도우미를 쓸 수 있으며, 그때는 import styled, { css } from 'styled-components';처럼 명시적으로 가져와야 합니다(styled-components API). breakpoint 문자열을 감추려고 추상화부터 만들 필요는 없습니다.


반응형 컴포넌트 검수

반응형 컴포넌트가 지켜야 할 것은 “몇 px에서 몇 열인가”로 끝나지 않습니다. 폭이 달라도 같은 정보와 동작을 찾을 수 있어야 하고, 표현이 바뀌어도 DOM의 의미나 탐색 순서가 깨지지 않아야 합니다.

폭이 바뀌어도 의미·정보·조작 계약을 유지합니다

RESPONSIVE COMPONENT · INVARIANTS

폭이 바뀌어도 의미·정보·조작 계약을 유지합니다

열·간격·보조 설명 위치는 바꿀 수 있습니다. 핵심 값·상태·동작의 도달 경로와 DOM·focus 순서, 구조화 데이터의 관계는 모든 폭에서 확인합니다.

본문 반응형 카드 · 같은 계약, 다른 배치

카드 목록 + 보조 정보 → 좁은 폭 한 열
같은 제목·상태·동작 → 넓은 폭 두 영역
긴 문구·중간 폭·확대·키보드로 확인

카드 모양으로 바꾸는 표현과 표의 header/cell 의미 보존은 다른 문제입니다. 전체 데이터와 주요 동작에 도달할 경로를 유지합니다.

폭·입력 환경이 바뀌어도 지켜야 할 계약과 검증 방법
계약 유지할 것 바꿀 수 있는 것 검증
의미와 순서 heading·label·control 의미, 논리적 DOM과 focus 순서 열, 정렬, 간격 같은 시각 배치 CSS를 끄고 읽기, keyboard-only 탐색
핵심 정보와 동작 주요 값, 상태, 완료해야 할 task와 도달 경로 밀도, 보조 설명 위치, 명확한 접기·펼치기 작은 폭에서 정보·기능 누락 확인
레이아웃 콘텐츠 관계와 읽을 수 있는 최소 공간 Flex·Grid 방향, track 수, gap, wrapping 긴 실제 copy, zoom, 중간 폭 overflow
query 경계 전환이 해결해야 할 구체적인 layout 문제 @media 환경 조건, @container 조상 크기 조건 경계 바로 아래·위와 query 없는 구간
입력과 조작 keyboard, pointer, touch에서 같은 기능 target 크기·간격, hover 보조 표현 hover 없는 환경, focus 표시, 오작동 간격
구조화 데이터 table header와 cell 관계, 전체 데이터 접근 가로 scroll 또는 별도의 접근 가능한 표현 화면만 카드처럼 바꾸며 의미가 사라지지 않는지 확인
의미와 순서
유지 heading·label·control 의미와 논리적 DOM·focus 순서
변경 열, 정렬, 간격 같은 시각 배치
검증 CSS 없이 읽고 keyboard만으로 이동
핵심 정보와 동작
유지 주요 값·상태·task와 도달 경로
변경 밀도, 보조 설명 위치, 명확한 접기·펼치기
검증 작은 폭의 정보·기능 누락 확인
레이아웃
유지 콘텐츠 관계와 읽을 수 있는 최소 공간
변경 Flex·Grid 방향, track 수, gap, wrapping
검증 긴 실제 copy, zoom, 중간 폭 overflow
query 경계
유지 전환이 해결할 구체적인 layout 문제
변경 @media 환경 또는 @container 조상 크기 조건
검증 경계 바로 아래·위와 query 없는 구간
입력과 조작
유지 keyboard, pointer, touch에서 같은 기능
변경 target 크기·간격과 hover 보조 표현
검증 hover 없는 환경, focus 표시, 오작동 간격
구조화 데이터
유지 table header·cell 관계와 전체 데이터 접근
변경 가로 scroll 또는 별도의 접근 가능한 표현
검증 카드 모양으로 바꾸며 데이터 의미가 사라지지 않는지 확인

반응형 검수는 대표 기기 캡처만 확인하는 일이 아닙니다. 의미·정보·동작의 불변 조건을 실제 콘텐츠와 여러 입력 방식으로 증명합니다.

위 표는 폭과 입력 환경이 바뀌어도 유지할 것, 바꿀 수 있는 것, 확인하는 방법을 여섯 항목으로 정리합니다. 제목과 라벨, 컨트롤의 의미와 논리적인 DOM·포커스 순서는 유지하되 열과 정렬, 간격 같은 시각 배치는 바꿀 수 있으며, CSS를 끄고 읽어 보거나 키보드만으로 이동해 확인합니다. 주요 값과 상태, 끝내야 할 작업과 그곳에 도달하는 경로는 유지하고 밀도나 보조 설명의 위치, 분명한 접기와 펼치기는 바꿀 수 있으며, 작은 폭에서 정보나 기능이 빠지지 않았는지 봅니다.

레이아웃에서는 콘텐츠의 관계와 읽을 수 있는 최소 공간을 지키면서 Flex와 Grid의 방향, 트랙 수, 간격, 줄바꿈을 바꿀 수 있고, 긴 실제 문구와 확대, 중간 폭의 넘침으로 확인합니다. 쿼리를 둘 지점은 그 전환이 해결할 구체적인 레이아웃 문제를 기준으로 하며, @media는 환경 조건에, @container는 조상의 크기 조건에 쓰고 그 지점의 바로 아래와 위, 쿼리가 없는 구간을 확인합니다. 입력과 조작은 키보드와 포인터, 터치에서 같은 기능을 쓸 수 있어야 하며, 대상의 크기와 간격, hover의 보조 표현은 바꿀 수 있습니다. 마지막으로 구조화된 데이터는 표의 머리글과 셀의 관계, 전체 데이터에 접근하는 길을 지키면서 가로 스크롤이나 별도의 접근 가능한 표현을 고를 수 있습니다. 그림 위쪽의 예가 말하듯 카드 모양으로 바꾸는 표현과 표의 의미를 지키는 것은 다른 문제입니다.

이 기준에 따라, breakpoint의 바로 아래와 위뿐 아니라 중간 폭에서도 다음을 확인합니다. 긴 한국어 제목과 끊기 어려운 URL, 큰 숫자, 빈 상태를 넣어 넘침과 줄바꿈을 봅니다. 브라우저를 확대하거나 글자 크기를 바꾼 뒤에도 콘텐츠와 조작 요소가 잘리거나 겹치지 않는지 확인합니다. 마우스 없이 키보드로 이동하며 보이는 순서와 포커스 순서가 자연스러운지, hover가 없거나 정밀하지 않은 포인터에서도 같은 기능을 쓸 수 있는지 봅니다. 작은 화면에서 핵심 정보를 그냥 숨기지 말고, 접거나 요약한다면 전체 정보에 도달할 분명한 길을 남깁니다. 데이터 표를 카드처럼 보이게 바꿀 때는 머리글과 셀의 관계를 잃지 않아야 하므로, 먼저 의미 있는 table을 유지한 채 가로 스크롤이나 별도의 접근 가능한 표현이 필요한지 검토합니다.

React가 DOM에 넘기는 className과 style, ARIA 속성의 규칙은 React DOM 공통 컴포넌트에서 확인할 수 있습니다.


정리

  • 반응형 디자인은 기기마다 화면을 따로 만드는 일이 아니라, 의미 있는 HTML 위에 유연한 크기와 레이아웃, 필요한 조건부 쿼리를 쌓는 일입니다.
  • index.html의 <meta name="viewport" content="width=device-width, initial-scale=1">로 모바일 브라우저가 기기 폭에 맞춰 렌더링하게 하고, 확대는 막지 않습니다.
  • breakpoint는 기기 이름이 아니라 실제 콘텐츠가 깨지는 지점에서 찾고, 그 양쪽과 중간 폭에서 다시 확인합니다.
  • 한 축의 분배는 Flexbox, 두 축의 트랙 관계는 Grid로 다루며, CSS의 재배치는 DOM의 읽기·포커스 순서를 바꾸지 않습니다.
  • 뷰포트나 사용자 환경에는 @media, 컴포넌트가 실제로 받은 공간에는 @container를 씁니다.
  • 폭이 달라도 같은 정보와 동작에 도달할 수 있는지, 확대와 키보드, 터치에서 확인합니다.

연습 문제

1. 뷰포트 1100px의 카드 열 수

ResponsiveLayout을 뷰포트 1100px에서 렌더링합니다. 앞의 계산과 같은 가정에서, 보조 정보는 어디에 놓이고 카드는 몇 열이 될까요?

해설 보기

컨테이너의 폭은 min(1100 − 32, 1200) = 1068px이고, 48rem 이상이므로 보조 정보는 옆 열에 놓입니다. 목록에 남는 폭은 1068 − 288 − 16 = 764px입니다.

카드 간격은 2vw = 22px이지만 clamp()의 최댓값 1.25rem에 걸려 20px이 됩니다. 세 열에 필요한 폭은 240 × 3 + 20 × 2 = 760px이므로 764px 안에 들어가고, 네 열은 240 × 4 + 20 × 3 = 1020px이라 들어가지 않습니다. 그래서 카드는 세 열입니다.

2. 어느 크기에 반응할까

같은 1000px 뷰포트에서 .card-region 두 개가 각각 400px과 600px의 인라인 크기를 제공합니다. 루트 글자 크기는 16px입니다. 어느 카드가 @container (min-width: 32rem)의 두 열을 적용할까요? 카드가 받은 공간 대신 뷰포트 폭을 자바스크립트 state로 읽으면 이 차이를 구별할 수 있을까요?

해설 보기

32rem은 512px이므로 600px인 조상 컨테이너 안의 카드만 두 열을 적용하고, 400px 쪽은 기본 한 열입니다.

뷰포트 폭은 두 경우 모두 1000px이라 그 값 하나로는 각 카드가 실제로 받은 공간을 구별할 수 없습니다. 크기 쿼리를 제공하는 조상과 규칙이 적용되는 자손도 구별해 두세요.

다음 글 반응형 카드 목록 컴포넌트 만들기에서는 지금까지 배운 스타일링 방법을 모아 반응형 카드 목록 컴포넌트를 직접 만듭니다.