Skip to content
This page has been auto-translated and may contain errors.View in English

반응형 디자인

docs.scrimba.com

노트북으로 페이지를 만들고 보기에 딱 맞아서 만족했는데, 누군가 그 페이지를 휴대폰으로 열어봅니다. 정성껏 맞춰놓은 열들이 좁은 조각으로 짓눌리고, 텍스트는 화면 밖으로 넘쳐나고, 있어서는 안 될 가로 스크롤바까지 나타납니다. 그렇다고 사이트의 휴대폰 전용 버전을 따로 만든 사람은 없습니다. 페이지는 하나뿐이고, 그 하나가 380픽셀짜리 휴대폰에서도, 1400픽셀짜리 모니터에서도 모든 것을 두 벌씩 만들지 않고 제대로 보여야 합니다. 이런 적응력이 바로 반응형 디자인이며, 대부분은 이미 여러분이 알고 있는 도구들로 만들어집니다.

반응형이라는 것과 뷰포트

반응형(responsive) 페이지란 어떤 화면에 놓이든 그에 맞춰 스스로 모양을 바꾸는 하나의 페이지를 말합니다. 같은 HTML과 같은 CSS가 휴대폰, 태블릿, 데스크톱을 모두 처리하며, 무엇도 넘치지 않고 무엇도 답답해 보이지 않도록 레이아웃이 조정됩니다. 사이트를 세 개 만드는 것이 아니라, 유연하게 휘어지는 하나를 만드는 것입니다.

이런 동작이 가능해지기 전에, 브라우저는 HTML 안에 지시 하나를 필요로 합니다. 예전에는 휴대폰이 마치 데스크톱만큼 폭이 넓은 것처럼 행동하며 페이지 전체를 축소해서 맞췄는데, 그래서 옛날 사이트들이 작고 축소된 뭉텅이로 보였던 것입니다. <head> 안에 들어가는 이 한 줄이 휴대폰에게 진짜 화면 폭을 그대로 쓰라고 알려줍니다.

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

이 줄이 없으면 CSS는 실제로 존재하지 않는 넓은 화면을 기준으로 스타일을 입히게 되고, 정성껏 만든 레이아웃은 제대로 보일 기회조차 얻지 못합니다.

반응형 디자인이란 하나의 코드베이스가 브라우저 창 안에서 보이는 페이지 영역인 뷰포트(viewport) 크기에 맞춰 레이아웃을 조정하는 것을 뜻합니다. HTML과 스타일시트는 각각 하나뿐이고, 달라지는 것은 그 스타일시트가 주어진 공간에 반응하는 방식입니다. 모바일용과 데스크톱용 사이트를 따로 만드는 방식은 유지보수 부담을 두 배로 늘리는데, 그래서 업계가 오래전에 반응형 디자인으로 방향을 정한 것입니다.

시작점이 되는 필수 조건은 HTML head에 넣는 뷰포트 메타 태그입니다. 이 태그는 브라우저가 레이아웃을 계산하는 기준 폭을 기기의 실제 폭으로 설정해서, 모바일 브라우저가 가상의 데스크톱 폭으로 렌더링한 뒤 축소하는 것을 막아줍니다.

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

width=device-width는 레이아웃 뷰포트를 실제 화면에 맞추고, initial-scale=1은 시작 확대/축소 비율을 100%로 설정합니다. 이것을 빼먹으면 CSS를 아무리 정확하게 작성해도 레이아웃은 살아나지 않습니다. 브라우저가 애초에 잘못된 폭을 기준으로 측정하고 있기 때문입니다.

반응형 디자인이란 몇몇 정해진 화면을 타겟으로 삼는 것이 아니라, 연속적인 범위의 뷰포트(viewport) 크기 전체에 레이아웃이 적응하는 문서 하나를 만드는 작업입니다. 뷰포트는 브라우저가 페이지를 배치하고 그려내는 영역이며, 그 폭은 상대 단위, 퍼센트, 미디어 쿼리가 결국 모두 근거로 삼는 값입니다. 이 폭이 잘못되면 이후의 모든 계산이 잘못된 값을 기준으로 이루어집니다.

바로 이것을 막는 것이 뷰포트 메타 태그입니다. 모바일 브라우저는 역사적으로 레이아웃 뷰포트(CSS가 배치 기준으로 삼는 폭)와 비주얼 뷰포트(실제로 화면에 보이는 것)를 구분했으며, 레거시 데스크톱 전용 사이트들이 제대로 렌더링되도록 레이아웃 뷰포트를 기본값 약 980px로 설정한 뒤 화면에 맞게 축소해서 보여주었습니다. 메타 태그는 이 간극을 없애줍니다.

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

width=device-width는 레이아웃 뷰포트를 기기 자체의 CSS 픽셀 폭에 맞추고, initial-scale=1은 초기 확대/축소 비율을 고정해 두 뷰포트가 처음부터 일치하도록 만듭니다. 이 태그는 HTML head에 들어가야 하며, 있으면 좋은 부가 요소가 아니라 반드시 필요한 전제 조건입니다. 이것이 없으면 390px 휴대폰에서도 브라우저는 미디어 쿼리와 퍼센트를 대략 980px 기준으로 평가하게 되어, min-width: 40rem 쿼리가 발동하면 안 될 때 발동해버리고, 여러분이 테스트한 레이아웃은 끝내 나타나지 않습니다. 이 장의 모든 내용은 이 태그가 이미 적용되어 있다는 것을 전제로 합니다.

Juno반응형이라는 것과 뷰포트 반응형 페이지란 휴대폰부터 데스크톱까지 어떤 화면에든 맞춰 스스로 모양을 바꾸는 하나의 페이지로, 단 하나의 HTML과 CSS로 만들어집니다. CSS가 제대로 작동하기 전에 HTML head에는 뷰포트 메타 태그가 필요한데, 이 태그가 휴대폰에게 데스크톱인 척하지 말고 진짜 폭을 쓰라고 알려줍니다. 이 태그를 빼먹으면 CSS가 아무리 훌륭해도 레이아웃은 깨져 보입니다.
Juno반응형이라는 것과 뷰포트 반응형이란 모바일과 데스크톱을 따로 만드는 것이 아니라, 페이지의 보이는 영역인 뷰포트에 맞춰 조정되는 하나의 코드베이스를 뜻합니다. HTML head의 뷰포트 메타 태그는 시작점이 되는 필수 조건입니다. width=device-width는 브라우저가 실제 화면 폭을 기준으로 배치하게 만듭니다. 이를 빼먹으면 미디어 쿼리가 가상의 데스크톱 폭을 기준으로 발동하게 되어, CSS가 맞아도 결과는 틀리게 보입니다.
Juno반응형이라는 것과 뷰포트 모든 퍼센트와 미디어 쿼리는 뷰포트 폭을 기준으로 계산되므로, 뷰포트 메타 태그는 있으면 좋은 것이 아니라 반드시 필요한 전제 조건입니다. width=device-width, initial-scale=1은 레거시 기본값인 980px 대신 레이아웃 뷰포트를 기기의 실제 폭에 맞춥니다. 휴대폰에서 이를 빼먹으면 min-width: 40rem 쿼리가 발동하면 안 될 때 발동하고, 테스트한 레이아웃은 끝내 렌더링되지 않습니다.

먼저 유동적인 레이아웃부터

바로 휴대폰 전용 규칙을 만들고 싶은 마음이 들지만, 사실 규칙 하나 쓰기 전에도 반응형 동작의 많은 부분이 이미 공짜로 따라옵니다. 주어진 공간에 맞춰 늘어나고 줄어드는 값인 유동적인(fluid) 값으로 레이아웃을 만들면, 레이아웃은 이미 스스로 적응합니다.

이 효과를 좌우하는 습관은 두 가지입니다. 폭을 고정된 픽셀 수가 아니라 퍼센트로 지정해서 박스가 컨테이너의 일정 비율을 차지하게 하는 것, 그리고 max-width를 써서 작은 화면에서는 줄어들되 큰 화면에서는 읽기 편한 크기를 넘어 커지지 않게 하는 것입니다.

css
.container {
  width: 90%;              /* 주어진 공간의 90%를 차지함 */
  max-width: 60rem;        /* 하지만 큰 화면에서도 60rem을 넘지 않음 */
  margin: 0 auto;          /* 가운데 정렬 */
}

휴대폰에서는 이 컨테이너가 좁은 화면의 90%가 됩니다. 모니터에서는 60rem에서 성장을 멈추고 가운데에 자리 잡습니다. 휴대폰용 규칙을 따로 쓴 것이 아니라, 값들이 스스로 적응한 것입니다.

미디어 쿼리를 쓰기 전에, 스스로 유연하게 움직일 수 있도록 유동적인(fluid) 값으로 레이아웃을 만드세요. 상대 단위와 퍼센트는 요소가 고정된 크기가 아니라 컨테이너의 일정 비율을 차지하게 해주고, flexboxgrid는 공간이 바뀌면 이미 자식 요소들을 다시 배치해줍니다. 처음부터 화면과 싸우지 않기 때문에, 잘 만든 유동적인 레이아웃은 뻣뻣한 레이아웃보다 훨씬 적은 브레이크포인트로도 충분합니다.

유동적인 레이아웃이 더는 감당하지 못할 때가 되어서야 미디어 쿼리를 꺼내세요. 퍼센트와 max-width가 폭을 처리하고, 흔히 쓰는 카드 벽 배치는 쿼리 없이도 그리드 패턴 하나로 처리할 수 있습니다.

css
.card-grid {
  display: grid;
  gap: 1.5rem;
  /* 최소 16rem을 유지하며 들어갈 수 있는 만큼 열을 만들고, 남는 공간은 균등하게 나눔 */
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
}

repeat(auto-fit, minmax(16rem, 1fr))은 "각 열은 최소 16rem을 유지하면서 들어갈 수 있는 만큼 열을 만들고, 그다음 남는 공간을 채우도록 늘려라"라는 뜻입니다. 넓은 화면에서는 세 열, 태블릿에서는 두 열, 휴대폰에서는 한 열이 되며, 미디어 쿼리는 단 하나도 필요하지 않습니다. 이런 값에 어떤 상대 단위를 쓰는 것이 좋은지는 사이징 단위에서 다룹니다.

가장 견고한 반응형 레이아웃은 미디어 쿼리를 하나도 쓰기 전에 대부분 이미 **유동적(fluid)**입니다. 브레이크포인트는 이산적인 도약이지만, 작업의 대부분은 연속적인 성격을 갖기 때문입니다. 퍼센트, max-width, 그리고 flexboxgrid에 내재된 고유 크기 조정 기능은 레이아웃이 뷰포트를 매끄럽게 따라가게 해주고, 그다음 미디어 쿼리는 디자인이 정말로 모양을 바꿔야 하는 지점만 다루면 되지, 크기가 바뀌는 모든 픽셀을 처리할 필요가 없습니다.

값어치를 제대로 하는 패턴은 스스로 조정되는 그리드입니다. 브레이크포인트 여러 개를 선언 하나로 뭉쳐버리기 때문입니다.

css
.card-grid {
  display: grid;
  gap: 1.5rem;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
}

minmax(16rem, 1fr)은 각 트랙에 최소 16rem이라는 바닥과, 남는 공간을 균등하게 나눈 몫이라는 천장을 부여하고, auto-fit은 그 바닥값으로 들어갈 수 있는 만큼 트랙을 만든 뒤 행을 채우도록 확장합니다. 열 개수는 컨테이너 폭에서 자동으로 계산되므로, 브레이크포인트 세 개가 필요했을 레이아웃이 하나도 필요하지 않게 됩니다. 기억해둘 만한 정밀한 차이 하나: auto-fit은 빈 트랙을 없애버려서 남은 항목들이 늘어나 행을 채우지만, auto-fill은 빈 트랙을 그대로 유지합니다. 그러니 항목이 남는 공간으로 늘어나길 원하면 auto-fit을, 안정적인 그리드를 원하면 auto-fill을 선택하세요. 이 remfr 값이 어떻게 계산되는지는 사이징 단위를 참고하세요.

Juno먼저 유동적인 레이아웃부터 특별한 규칙을 쓰기 전에도 반응형 동작의 많은 부분은 공짜로 따라옵니다. 퍼센트를 써서 박스가 공간의 일정 비율을 차지하게 하고, max-width를 써서 휴대폰에서는 줄어들되 모니터에서는 너무 넓어지지 않게 하세요. 이 두 값만으로도 레이아웃은 스스로 적응합니다. 유동적인 레이아웃을 먼저 만들면, 뒤에 나올 휴대폰 전용 규칙이 훨씬 적게 필요합니다.
Juno먼저 유동적인 레이아웃부터 브레이크포인트를 만들기 전에 유동적인 레이아웃부터 만드세요. 퍼센트와 max-width가 폭을 처리하고, flexbox와 grid는 이미 스스로 다시 배치합니다. 꼭 기억해둘 그리드 한 줄은 repeat(auto-fit, minmax(16rem, 1fr))인데, 미디어 쿼리 없이도 반응형 카드 벽을 만들어줍니다. 유동적인 레이아웃이 정말로 더 감당하지 못할 때가 되어서야 쿼리를 꺼내세요.
Juno먼저 유동적인 레이아웃부터 브레이크포인트는 이산적인 도약이지만 크기 변화는 연속적이므로, 퍼센트와 max-width, 그리고 flex와 grid의 고유 크기 조정 기능이 매끄러운 부분을 처리하게 두고, 미디어 쿼리는 정말로 모양이 바뀌어야 할 때를 위해 남겨두세요. repeat(auto-fit, minmax(16rem, 1fr))은 브레이크포인트 여러 개를 한 줄로 압축해줍니다. auto-fit은 빈 트랙을 없애 항목을 늘리고, auto-fill은 빈 트랙을 그대로 유지한다는 점을 기억하세요.

미디어 쿼리

가끔은 유동적인 값만으로 충분하지 않고, 특정 크기에서 레이아웃이 실제로 바뀌어야 할 때가 있습니다. 예를 들어 메뉴가 휴대폰에서는 세로로 쌓이지만 데스크톱에서는 한 줄로 나란히 놓이는 경우입니다. 이럴 때 필요한 것이 **미디어 쿼리(media query)**입니다. 화면이 여러분이 정한 조건을 만족할 때만 적용되는 CSS 블록입니다.

작은 화면용 스타일을 일반 CSS로 먼저 작성하고, 그 위에 넓은 화면에서 바뀌는 부분을 얹는 쿼리를 추가하세요.

css
.nav {
  display: flex;
  flex-direction: column;   /* 작은 화면에서는 세로로 쌓임 */
}

@media (min-width: 40rem) {
  .nav {
    flex-direction: row;    /* 공간이 생기면 나란히 배치됨 */
  }
}

@media (min-width: 40rem) 안의 규칙은 화면이 최소 40rem 이상일 때만 켜집니다. 그 아래에서는 평범한 .nav 규칙이 적용되어 메뉴가 세로로 쌓입니다.

**미디어 쿼리(media query)**는 규칙 묶음을 조건, 주로 폭 조건으로 감싸서, 그 조건이 성립할 때만 규칙이 적용되게 합니다. 채택할 패턴은 **모바일 우선(mobile-first)**입니다. 가장 작은 화면을 위한 기본 스타일은 쿼리 없이 그냥 작성하고, min-width로 위쪽으로 개선사항을 쌓아 올리는 방식입니다.

css
.layout {
  display: grid;
  gap: 1rem;
  grid-template-columns: 1fr;      /* 기본: 작은 화면에서는 한 열 */
}

@media (min-width: 40rem) {
  .layout {
    grid-template-columns: 1fr 1fr; /* 공간이 생기면 두 열 */
  }
}

모바일 우선 방식이 중요한 이유는 단순히 깔끔해서가 아닙니다. 기본 스타일은 항상 적용되는 스타일이므로, 이를 가장 단순하고 가장 안정적인 레이아웃으로 만들면 가장 작고 제약이 많은 기기가 기본적으로 가장 안전한 경험을 얻게 되고, 각 쿼리는 오직 기능을 더할 뿐입니다. 반대 방향으로, 데스크톱을 기준으로 삼고 max-width 쿼리로 요소를 걷어내는 방식으로 만들면, 모바일 화면은 뒤늦게 손댄 결과물이 되어 자주 깨지곤 합니다.

**미디어 쿼리(media query)**는 미디어 조건이 참으로 평가될 때만 규칙 블록을 적용하며, 반응형 레이아웃에서는 그 조건이 거의 언제나 뷰포트 폭 검사입니다. 굳건하게 유지되는 원칙은 **모바일 우선(mobile-first)**입니다. 조건 없는 기본 스타일이 좁은 레이아웃을 기술하고, 모든 min-width 쿼리는 그 위에 순수하게 더해질 뿐입니다.

css
.layout {
  display: grid;
  gap: 1rem;
  grid-template-columns: 1fr;       /* 기본, 어디에나 적용됨 */
}

@media (min-width: 40rem) {         /* 태블릿 이상 */
  .layout { grid-template-columns: 1fr 1fr; }
}

@media (min-width: 64rem) {         /* 데스크톱 이상 */
  .layout { grid-template-columns: 1fr 2fr 1fr; }
}

max-width로 빼는 방식보다 min-width로 더하는 방식을 선호하는 이유는 캐스케이드에 있습니다. 이 쿼리들은 명시도가 같으므로 나중에 나오는 쿼리가 앞선 쿼리를 덮어쓰는데, 작은 값부터 큰 값 순서로 나열하면 각 넓은 브레이크포인트가 적용되는 지점에서 깨끗하게 이기게 됩니다. 짚어둘 만한 정밀한 사항 두 가지가 있습니다. 조건에는 px보다 rem을 선호해서 브레이크포인트가 사용자의 글꼴 크기를 따라가게 하고, 폭 쿼리는 레이아웃 뷰포트를 읽는데 이는 뷰포트 메타 태그가 그 값을 기기 폭으로 고정해두었기 때문에만 올바르게 작동한다는 점을 알아두세요. min-width 나열 중간에 끼어든 max-width 쿼리 하나가, 규칙이 알 수 없이 더는 적용되지 않는 흔한 원인입니다. 순서를 확장하는 것이 아니라 그 순서와 충돌하기 때문입니다.

Juno미디어 쿼리 미디어 쿼리는 화면이 특정 조건, 예를 들어 최소 40rem 이상이라는 조건을 만족할 때만 켜지는 CSS 블록입니다. 작은 화면용 스타일을 먼저 일반 CSS로 작성하고, @media (min-width: ...)로 큰 화면용 변화를 추가하세요. 그러면 휴대폰 화면이 나중에 덧붙인 것이 아니라 처음부터의 출발점이 됩니다.
Juno미디어 쿼리 미디어 쿼리는 조건, 보통 폭 조건이 성립할 때만 규칙을 적용합니다. 모바일 우선으로 가세요. 가장 작은 화면을 위한 기본 스타일에는 쿼리를 쓰지 않고, 그 위에 min-width로 쌓아 올려서 각 쿼리가 오직 더하기만 하게 만드세요. 반대로 max-width로 만들면 모바일 화면이 결국 뒤늦게 손댄 깨진 결과물이 되곤 합니다.
Juno미디어 쿼리 모바일 우선이란 좁은 레이아웃을 위한 조건 없는 기본 스타일과, 작은 값부터 큰 값 순으로 쌓은 더하기식 min-width 쿼리를 뜻하며, 이렇게 하면 캐스케이드가 각 브레이크포인트를 깨끗하게 해석합니다. 조건에는 rem을 써서 사용자의 글꼴 크기를 따라가게 하고, 폭 쿼리는 메타 태그가 고정해둔 레이아웃 뷰포트를 읽는다는 점을 기억하세요. min-width 나열 속에 홀로 끼어든 max-width가 규칙이 조용히 적용되지 않게 되는 대표적인 원인입니다.

브레이크포인트 정하기

**브레이크포인트(breakpoint)**란 미디어 쿼리가 발동해서 레이아웃이 바뀌는 폭을 말합니다. 하나를 정할 때 유혹적인 방법은 유명한 휴대폰이나 태블릿의 폭을 찾아보고 거기에 맞추는 것입니다. 이것이 함정입니다. 기기 크기는 수백 가지가 있고, 매년 바뀌기 때문에, 그것을 좇는 것은 절대 이길 수 없는 게임입니다.

더 좋은 질문은 "이 레이아웃이 정확히 어느 폭에서 나빠지기 시작하는가"입니다. 브라우저 폭을 천천히 넓혀보면서 지켜보세요. 한 줄 텍스트가 너무 길어져서 읽기 불편해지거나, 두 열이 너무 좁아질 때, 그 지점이 브레이크포인트가 있어야 할 곳이며, 그 숫자가 무엇이든 상관없습니다.

css
/* 여기서 텍스트가 너무 넓어졌기 때문에 정한 브레이크포인트이고, */
/* 휴대폰이 마침 이 크기라서 정한 것이 아님 */
@media (min-width: 45rem) {
  .article { max-width: 38rem; }
}

**브레이크포인트(breakpoint)**란 레이아웃을 바꾸는 뷰포트 폭을 말합니다. 기기 이름에 맞추지 말고 자신의 콘텐츠를 지켜보면서 정하세요. "아이패드니까 768px"처럼 딱 떨어지는 숫자는 그럴듯해 보이지만 함정입니다. 기기 판도는 끊임없이 바뀌고, 작년에 유행했던 크기에 맞춘 레이아웃은 올해 크기에서 깨집니다.

콘텐츠가 결정하게 하세요. 창을 리사이즈하면서 디자인이 실제로 힘들어지는 폭, 줄이 너무 길어지거나 카드가 너무 답답해지거나 내비게이션이 공간을 잃는 지점에서 브레이크포인트를 추가하세요. 사이트 전체를 통틀어 보통 두세 개면 충분합니다.

css
/* 콘텐츠 기반: 이 줄이 약 75자에 도달해서 읽기 힘들어짐 */
@media (min-width: 42rem) {
  .prose { max-width: 65ch; }   /* 공간이 생기면 가로 폭을 제한함 */
}

/* 콘텐츠 기반: 사이드바 레이아웃은 이 지점에서야 자기 공간을 벌 만함 */
@media (min-width: 60rem) {
  .page { grid-template-columns: 16rem 1fr; }
}

**브레이크포인트(breakpoint)**란 디자인이 모양을 바꾸는 지점이며, 이를 정하는 지속력 있는 방법은 기기 사양표가 아니라 콘텐츠가 스스로 결정하게 두는 것입니다. 특정 기기 이름의 폭에 맞추는 방식은 구조적인 이유로 취약합니다. 화면 크기의 모집단은 크고 매년 흘러가므로, 특정 기기 치수에 맞춘 레이아웃은 움직이는 목표를 좇는 셈이고 예정대로 낡아갑니다. 콘텐츠로 정당화되는 브레이크포인트, 즉 "여기서 한 줄의 가독성이 무너졌다"는 근거는 어떤 하드웨어가 나오든 계속 정당성을 유지합니다.

실제로 이는 레이아웃이 힘들어지는 지점까지 리사이즈해보고 거기에 브레이크포인트를 두는 것을 뜻하며, 대부분의 사이트는 아주 적은 수만으로 충분합니다.

css
/* 줄이 읽기 편한 길이, 약 65자를 넘는 지점에서 가로 폭을 제한함 */
@media (min-width: 42rem) {
  .prose { max-width: 65ch; }
}

/* 메인 열이 폭을 내어줄 수 있는 지점에서만 사이드바를 도입함 */
@media (min-width: 60rem) {
  .page { grid-template-columns: 16rem 1fr; }
}

한 단계 더 나아가는 방법은 clamp()에 의존해서 글자 크기와 여백을 유동적으로 만들고, 그렇게 함으로써 브레이크포인트 자체를 더 적게 필요로 하게 만드는 것입니다. clamp(min, preferred, max)는 뷰포트에 따라 값을 조정하면서도 양쪽 끝을 제한해서, 제목과 간격이 각 쿼리마다 껑충 뛰는 것이 아니라 매끄럽게 커지도록 해줍니다.

css
h1 {
  /* 1.75rem 아래로는 절대 내려가지 않고, 3rem 위로는 절대 올라가지 않으며, 그 사이는 유동적임 */
  font-size: clamp(1.75rem, 4vw + 1rem, 3rem);
}

이것만으로도 타이포그래피 크기 조정 대부분을 브레이크포인트 없이 처리할 수 있습니다. vw 항이 어떻게 계산되는지는 사이징 단위를, 타입 스케일 쪽은 타이포그래피를 참고하세요. 이를 보완하는 관련 도구 세 가지가 더 있습니다. 컨테이너 쿼리는 뷰포트가 아니라 컴포넌트 자신의 컨테이너 폭을 기준으로 컴포넌트를 스타일링해서, 카드가 페이지 전체가 아니라 자신이 놓인 열에 반응하게 해주는데, 이는 뷰포트 기반 미디어 쿼리에 대응하는 컴포넌트 기반 도구입니다. prefers-reduced-motion 쿼리는 시스템에서 애니메이션을 줄여달라고 요청한 사용자를 위해 애니메이션을 없앨 수 있게 해주고, prefers-color-scheme는 시스템의 라이트/다크 모드 설정을 존중할 수 있게 해주며, 둘 다 미디어 쿼리로 작성합니다.

css
@media (prefers-reduced-motion: reduce) {
  * { animation: none; transition: none; }  /* OS 설정을 존중함 */
}

@media (prefers-color-scheme: dark) {
  body { background: #111; color: #eee; }
}

이미지에 관해서는 반응형 처리가 HTML 쪽으로 넘어갑니다. srcset 속성을 쓰면 브라우저가 화면에 맞는 크기의 파일을 골라 쓸 수 있는데, 이는 CSS가 아니라 HTML 반응형 이미지 장에서 다룹니다.

Juno브레이크포인트 정하기 브레이크포인트는 레이아웃이 바뀌는 폭이며, 특정 휴대폰이나 태블릿에 맞춰 정하는 것이 함정입니다. 좇아가기에는 기기 크기가 너무 많고 매년 바뀝니다. 브라우저 폭을 천천히 넓혀보면서, 레이아웃이 실제로 나빠지기 시작하는 지점에 브레이크포인트를 두세요. 그 숫자가 무엇이든 상관없습니다.
Juno브레이크포인트 정하기 기기 이름이 아니라 콘텐츠가 브레이크포인트를 정하게 하세요. 줄이 너무 길어지거나 카드가 너무 답답해지는 것이 진짜 신호이고, 768px 같은 딱 떨어지는 기기 폭은 움직이는 목표라서 낡아갑니다. 디자인이 힘들어지는 지점까지 리사이즈하고 그곳에 브레이크포인트를 두세요. 사이트 전체를 통틀어 보통 두세 개면 충분합니다.
Juno브레이크포인트 정하기 콘텐츠 기반 브레이크포인트가 살아남는 이유는 화면 모집단이 매년 흘러가기 때문이며, 기기 치수에 맞춘 것은 무엇이든 예정대로 낡아갑니다. clamp()에 의존해서 글자 크기와 여백을 유동적으로 만들면 브레이크포인트가 아예 덜 필요해지고, 컨테이너 쿼리를 뷰포트 단위가 아닌 컴포넌트 단위 스타일링을 위해 기억해두고, prefers-reduced-motionprefers-color-scheme를 존중하세요. 이미지의 경우 반응형 처리는 CSS가 아니라 HTML srcset에 있습니다.