본문으로 건너뛰기

안동민 개발노트

본문 시작

반응형 디자인 기초

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

우리는 styled-components로 스타일 규칙을 React 컴포넌트 가까이에 두는 방법을 배웠습니다.

반응형 디자인은 별도의 React 기능이 아니라, 같은 콘텐츠와 동작을 다양한 뷰포트·입력 방식·사용자 설정에서도 읽고 조작할 수 있게 만드는 웹 설계 접근입니다. React는 의미 있는 HTML을 렌더링하고, 브라우저는 CSS의 유연한 크기와 레이아웃 알고리즘, 조건부 쿼리를 계산합니다.

의미 있는 콘텐츠와 DOM 순서를 기반으로 유연한 크기와 Flexbox·Grid를 적용하고, 필요한 경계에서만 media·container query가 환경 신호에 반응하는 반응형 책임 구조

Responsive contract · stable meaning, adaptive presentation

React가 렌더링한 의미와 동작은 유지하고, CSS 표현만 받은 공간과 사용자 환경에 맞춰 조정한다.

반응형 설계 책임 레이어 위에서부터 뷰포트와 사용자 환경 신호, 조건부 CSS 쿼리, 유연한 레이아웃, 의미 있는 콘텐츠와 DOM 순서가 쌓인다. 가장 아래 콘텐츠와 DOM 계약은 모든 폭에서 유지되는 기반이다. 환경 기반 L4 환경 신호 viewport · print · preference · input L3 조건부 CSS @media · @container L2 유연한 레이아웃 normal flow · Flexbox · Grid · intrinsic size L1 콘텐츠와 DOM 계약 meaning · source order · actions · data
환경 신호
뷰포트, 출력 매체, 사용자 선호와 입력 특성은 CSS가 질의할 수 있는 외부 조건입니다.
조건부 CSS
@media는 뷰포트·환경을, @container는 조상 컨테이너가 제공한 공간을 기준으로 규칙을 선택합니다.
유연한 레이아웃
normal flow, Flexbox, Grid와 intrinsic sizing으로 가능한 변화는 query 없이 먼저 해결합니다.
콘텐츠와 DOM 계약
제목·라벨·주요 동작·데이터와 논리적인 source order는 모든 폭에서 유지되는 기반입니다.
반응형 설계는 React에서 기기별 DOM을 따로 만드는 일이 아니라, 안정적인 의미 위에 적응형 CSS 표현을 쌓는 일입니다.

먼저 고정할 반응형 계약

화면이 좁아져도 제목·라벨·상태·주요 동작의 의미가 사라지면 안 됩니다. 먼저 CSS가 없어도 읽기 순서와 키보드 이동 순서가 자연스러운 HTML을 만들고, 그 위에 표현 계층을 더합니다.

유연한 크기: 고정된 화면 폭을 가정하지 않고 max-inline-size, %, fr, minmax(), clamp()처럼 주변 공간과의 관계를 표현합니다.

유연한 미디어: 이미지와 영상이 컨테이너를 넘지 않게 합니다. 기본적인 이미지에는 max-inline-size: 100%; block-size: auto;를 적용할 수 있습니다.

조건부 전환: 기본 흐름만으로 해결되지 않는 지점에 미디어 쿼리나 컨테이너 쿼리를 추가합니다.

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


메타 뷰포트 태그

좁은 화면의 모바일 브라우저가 데스크톱용 가상 뷰포트로 페이지를 축소해 보여 주면, 작성한 작은 폭용 CSS가 예상대로 선택되지 않을 수 있습니다. 반응형 페이지의 HTML <head>에는 일반적으로 다음 설정을 둡니다.

Vite 프로젝트에서는 public 폴더가 아니라 프로젝트 루트의 index.html이 기본 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이라는 뜻은 아닙니다.

사용자의 확대를 막는 user-scalable=no나 과도한 maximum-scale 제한은 추가하지 않습니다. 확대는 작은 글자를 읽는 중요한 접근성 수단입니다.


미디어 쿼리와 breakpoint

미디어 쿼리는 뷰포트 폭뿐 아니라 출력 매체, 방향, 해상도, hover·pointer 같은 입력 특성, 모션 감소 같은 사용자 선호를 조건으로 CSS를 선택합니다.

/* 좁은 폭의 한 열이 기본값 */
.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을 넣고 폭을 연속해서 바꿔 보며 줄바꿈·overflow·조작 문제가 처음 생기는 지점을 찾습니다.

좁은 폭의 기본 레이아웃을 실제 콘텐츠로 연속 검증하고, 읽기·조작·줄바꿈이 유지되면 query 없이 종료하며 깨지는 지점에만 breakpoint를 추가한 뒤 양쪽 경계를 다시 확인하는 분기 흐름

Breakpoint · content stress, not device names

미리 정한 mobile·tablet 숫자에 맞추지 말고, 실제 콘텐츠 계약이 처음 깨지는 경계를 찾는다.

콘텐츠 기반 breakpoint 선택 흐름 좁은 폭의 단순한 기본값에서 시작해 실제 콘텐츠로 폭을 연속 변경한다. 읽기와 조작, 줄바꿈이 유지되면 query를 추가하지 않고 그 흐름을 끝낸다. 문제가 생긴 분기에서만 바로 그 경계에 breakpoint를 추가하고 경계 양쪽과 중간 폭을 다시 검증한다. 유지 깨짐 좁은 폭 기본값 한 열 · 논리적 source order 실제 콘텐츠로 폭 변경 긴 제목 · URL · 숫자 · 빈 상태 읽기·조작·줄바꿈 계약이 유지되는가? query 없이 유지 · 종료 유연한 CSS로 충분 깨지는 경계에 query 추가 layout 관계만 필요한 만큼 전환 경계 전후 재검증 아래 · 위 · 중간 폭
  1. 좁은 폭의 단순한 기본값에서 시작

    한 열과 논리적인 source order를 먼저 완성합니다.

  2. 실제 콘텐츠로 폭을 연속 변경

    가장 긴 제목, URL, 숫자, 빈 상태를 넣어 읽기·조작·줄바꿈을 확인합니다.

유지 → query 없이 종료

Flexbox, Grid와 intrinsic sizing만으로 충분하면 이 분기는 그대로 끝납니다.

깨짐 → query 추가·재검증

깨진 경계에서 필요한 관계만 전환하고, 바로 아래·위와 중간 폭을 다시 확인합니다.

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

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

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

src/components/ResponsiveLayout.jsxsrc/components/ResponsiveLayout.css를 만듭니다.

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-fitminmax()가 카드 열을 가능한 범위에서 자동으로 바꾸므로 카드마다 별도 breakpoint가 필요하지 않습니다. 미디어 쿼리는 목록과 보조 영역의 관계가 실제로 바뀌는 지점에만 사용했습니다.


Flexbox와 Grid 선택

Flexbox는 한 번에 한 축, 즉 행 또는 열에서 공간을 분배하고 정렬하는 1차원 레이아웃 모델입니다. 버튼 행, 내비게이션, 한 줄의 메타 정보처럼 한 방향의 흐름이 중심일 때 적합합니다.

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, 별도 배치 관계가 없다면 normal flow를 유지하는 CSS 레이아웃 선택 흐름

Layout choice · relationship before tool

화면 크기부터 묻지 말고, 콘텐츠 사이에서 어떤 축과 정렬 관계를 제어해야 하는지 먼저 묻는다.

Flexbox와 Grid 선택 흐름 배치할 콘텐츠 관계를 확인한다. 행이나 열 한 축에서 분배하고 정렬하면 Flexbox를, 행과 열을 함께 맞추면 Grid를 선택한다. 두 관계가 모두 필요 없다면 문서의 normal flow를 유지한다. 아니요 아니요 배치할 콘텐츠 관계 도구보다 구조를 먼저 확인 한 축의 분배·정렬인가? 행 또는 열을 한 번에 제어 Flexbox 1차원 흐름·정렬·분배 행과 열을 함께 맞추는가? 두 축의 track 관계 Grid 2차원 행·열 배치 normal flow 유지 불필요한 layout 선언 생략
Flexbox
행 또는 열 한 축에서 여유 공간을 분배하고 정렬하거나, 항목을 다음 줄로 감쌉니다.
Grid
행과 열을 함께 맞추며 track, 영역, 반복 열 사이의 2차원 관계를 정의합니다.
normal flow
별도 정렬 관계가 없다면 문서 흐름을 유지하는 것이 가장 단순하고 유연합니다.

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

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

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

Flexbox의 order, row-reverse나 Grid 배치로 시각 순서만 바꿔도 기본 DOM 읽기 순서와 키보드 탐색 순서가 함께 바뀌지는 않습니다. 논리적인 순서는 HTML 소스에 먼저 반영하고, CSS 재배치는 의미를 바꾸지 않는 표현 조정에만 사용합니다.


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

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

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

size query는 자신이 아니라 가장 가까운 적합한 조상 query container를 기준으로 그 안의 요소에 스타일을 적용합니다. 따라서 위 예제에서는 .card-region이 폭을 제공하고 그 자손인 .responsive-card가 전환됩니다.


styled-components에서 반응형 CSS 사용

웹용 styled-components는 실제 stylesheet를 만들며 일반 CSS와 nesting을 지원합니다. 따라서 template literal 안에 @media@container를 직접 작성할 수 있고, 단순한 예제에 별도 JavaScript viewport state나 helper가 필요하지 않습니다.

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 helper를 사용할 수 있으며, 그 경우 import styled, { css } from 'styled-components';처럼 명시적으로 가져와야 합니다. 단순히 breakpoint 문자열을 감추기 위한 추상화부터 만들 필요는 없습니다.


반응형 컴포넌트 검수

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

반응형 컴포넌트에서 폭과 입력 환경이 달라도 유지할 의미·정보·동작과 바꿀 수 있는 레이아웃·밀도·표현, 그리고 keyboard·zoom·실제 콘텐츠로 검증할 항목을 정리한 표

Responsive component · invariants and adaptations

breakpoint 값보다 먼저 무엇은 유지하고 무엇만 바꿀 수 있는지를 컴포넌트 계약으로 적는다.

폭·입력 환경이 바뀌어도 지켜야 할 계약과 검증 방법
계약 유지할 것 바꿀 수 있는 것 검증
의미와 순서 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 또는 별도의 접근 가능한 표현
검증 카드 모양으로 바꾸며 데이터 의미가 사라지지 않는지 확인
반응형 검수는 대표 기기 캡처만 확인하는 일이 아닙니다. 의미·정보·동작의 불변 조건을 실제 콘텐츠와 여러 입력 방식으로 증명합니다.

다음 조건을 breakpoint 바로 아래와 위뿐 아니라 중간 폭에서도 확인합니다.

  • 긴 한국어 제목, 끊기 어려운 URL, 큰 숫자와 빈 상태를 넣어 overflow와 줄바꿈을 확인합니다.
  • 브라우저 확대와 글자 크기 변경 후에도 콘텐츠와 조작 요소가 잘리거나 겹치지 않는지 봅니다.
  • 마우스 없이 키보드로 이동하며 시각 순서와 focus 순서가 자연스러운지 확인합니다.
  • hover가 없거나 정밀하지 않은 pointer에서도 같은 기능을 사용할 수 있게 합니다.
  • 작은 화면에서 핵심 정보를 단순히 숨기지 않습니다. 접거나 요약한다면 전체 정보에 도달할 수 있는 명확한 경로를 제공합니다.
  • 데이터 표를 카드처럼 보이게 바꿀 때는 header와 cell의 관계를 잃지 않아야 합니다. 우선 의미 있는 table을 유지한 채 가로 스크롤이나 별도의 접근 가능한 표현이 필요한지 검토합니다.

반응형 디자인 기초는 여기까지입니다.

핵심은 기기 이름별 화면을 따로 만드는 것이 아닙니다. 의미 있는 HTML을 기반으로 유연한 CSS를 먼저 사용하고, 실제 콘텐츠가 요구하는 경계에만 viewport·preference media query 또는 container query를 추가합니다. 이로써 5장 스타일링과 CSS를 마칩니다.

공식 문서