B-6: 반응형 웹
안녕하세요, 홍순구 튜터입니다. 지난 시간에 우리는 CSS Grid로 인스타그램 프로필의 사진 격자를 완성했어요. display: grid 한 줄로 부모를 격자 사령관으로 만들고, repeat(3, 1fr)로 똑같은 칸 3개를 그어, 사진 9개를 바둑판처럼 쫙 깔았죠. 헤더도 grid-template-areas로 이름표를 붙여 배치했고요.
그런데 지난 시간 끝에 솔직하게 인정한 한계가 하나 있었어요. 우리 격자는 항상 3열 고정이라는 거예요. 큰 모니터에서 보면 멀쩡한데, 휴대폰처럼 좁은 화면에서 3열을 고집하면 사진 한 장이 우표만큼 쪼그라들어 답답하죠. Step 6에서 auto-fit으로 칸 개수가 화면에 반응하는 맛은 봤지만, "폰에서는 1열, 태블릿에서는 2열, 데스크톱에서는 3열"처럼 화면 구간마다 디자인을 통째로 갈아끼우진 못했어요.
오늘 배울 반응형 웹(Responsive Web)이 바로 그걸 해결해요. 반응형은 "같은 HTML 한 벌로, 화면 크기에 따라 디자인이 알아서 변하는" 기법이에요. 휴대폰으로 봐도, 태블릿으로 봐도, 큰 모니터로 봐도 각 화면에 딱 맞는 모습으로요.
비유를 들어볼게요. 물은 담는 그릇에 따라 모양이 변하죠. 동그란 컵에 담으면 동그랗고, 네모난 통에 담으면 네모나고요. 반응형 웹도 똑같아요. 우리가 만든 콘텐츠가 "화면이라는 그릇"에 맞춰 형태를 바꾸는 거예요. 내용물(사진·글)은 그대로인데, 배치와 크기만 화면에 맞춰 흐르는 거죠.
고정 디자인 반응형 디자인
폰에서 봐도 데스크톱 그대로 폰 태블릿 데스크톱
┌─────────────┐ ┌───┐ ┌───────┐ ┌─────────────┐
│ ▪▪▪ 3열 고정 │ ← 답답 │ ▪ │ │ ▪ ▪ │ │ ▪ ▪ ▪ │
│ ▪▪▪ │ ← 가로 스크롤 │ ▪ │ │ ▪ ▪ │ │ ▪ ▪ ▪ │
└─────────────┘ │ ▪ │ │ ▪ ▪ │ │ ▪ ▪ ▪ │
└───┘ └───────┘ └─────────────┘
1열 2열 3열
화면이라는 그릇에 맞춰 형태가 변함
오늘은 화면 너비를 조건으로 거는 **미디어 쿼리(@media)**를 중심으로, 모바일을 먼저 생각하는 모바일 퍼스트 사고방식을 익히고, 지난 시간 만든 3열 고정 격자를 화면 구간별 1·2·3열로 갈아끼웁니다. 거기에 부모 칸 크기에 반응하는 컨테이너 쿼리(@container), 그리고 화면에 맞는 이미지를 골라 보내는 <picture>·srcset까지 배워, 모든 화면에서 예쁜 인스타그램을 완성할 거예요.
💡 오늘 수업의 핵심 — "@media로 화면 너비를 조건으로 걸어, 모바일 퍼스트로 3열 격자를 1·2·3열로 갈아끼우고, @container·<picture>까지 더해 모든 화면에 대응한다" 🎯
🎯 학습 목표
- 반응형 웹이 무엇이고 왜 필요한지, 그리고 뷰포트 메타 태그가 반응형의 전제인 이유를 이해합니다.
@media미디어 쿼리로 화면 너비를 조건으로 걸어, 조건마다 다른 CSS를 적용합니다.min-width와max-width의 의미를 구분하고, 브레이크포인트(화면 구간 경계)를 잡습니다.- 모바일 퍼스트(기본은 모바일,
min-width로 위로 쌓기) 사고방식을 익힙니다. - 지난 시간의 3열 고정 격자를, 화면 구간별로 1·2·3열로 바뀌는 반응형 격자로 만듭니다.
- 모바일에서 피드 카드를 화면 끝까지 채우는(full-bleed) 실전 반응형 규칙을 작성합니다.
- 화면이 아니라 부모 칸 크기에 반응하는 컨테이너 쿼리(
@container)를 이해하고,@media와의 차이를 구분합니다. - 반응형 이미지(
srcset/sizes로 해상도 전환,<picture>로 화면별 다른 이미지)를 적용합니다.
오늘도 외우려 하지 마세요. 반응형의 핵심 도구인 @media는 "화면이 이만큼 넓으면 이 CSS를 켜라"는 조건문일 뿐이에요. 브라우저 창의 너비를 마우스로 줄였다 늘였다 하면서 격자가 1열에서 2열, 3열로 갈아끼워지는 걸 직접 보면, 표로 외우는 것보다 훨씬 빨리 이해돼요. 자, 화면이라는 그릇에 콘텐츠를 맞춰볼까요?
Step 1: "반응형이 뭐죠? — 그리고 처음부터 있던 뷰포트 메타 태그의 정체"
반응형 웹을 한 문장으로 정의하면 이래요. 하나의 HTML과 CSS로, 화면 크기에 따라 레이아웃이 알아서 바뀌는 웹. 폰용 사이트, 데스크톱용 사이트를 따로 만드는 게 아니라, 같은 코드 한 벌이 화면에 맞춰 옷을 갈아입는 거죠.
왜 이게 중요할까요? 요즘 인스타그램에 접속하는 사람의 대부분은 휴대폰을 써요. 그런데 우리가 만든 3열 고정 격자는 데스크톱 기준이라, 폰에서 보면 사진이 너무 작거나 가로로 삐져나가요. 화면마다 보기 좋게 맞춰주지 않으면 절반 이상의 방문자가 불편을 겪는 거예요.
그런데 반응형을 시작하기 전에, 사실 우리는 첫날부터 반응형의 전제 조건을 깔아두고 있었어요. 모든 HTML 파일의 <head>를 열어보면 이 한 줄이 있어요.
<!-- instagram-clone-frontend/index.html -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Instagram - 로그인</title>
...
</head>
두 번째 줄, <meta name="viewport" ...>이 바로 뷰포트(viewport) 메타 태그예요. 뷰포트는 "브라우저가 웹페이지를 그리는 보이는 영역"을 뜻해요. 이 태그가 하는 일을 풀어보면 이래요.
width=device-width→ 페이지의 너비를 기기(device) 화면의 실제 너비에 맞춰라. 폰이면 폰 너비(약 390px), 데스크톱이면 그 창 너비로요.initial-scale=1.0→ 처음 열 때 확대·축소 없이 100% 크기로 보여줘라.
이게 왜 중요하냐면, 이 태그가 없으면 휴대폰 브라우저는 페이지를 약 980px짜리 데스크톱 화면이라고 가정하고 통째로 축소해서 보여줘요. 글씨가 깨알같이 작아지고, 손가락으로 확대해야 읽히는 옛날 모바일 웹이 바로 이래서 생겼죠. 뷰포트 메타 태그가 있어야 "이 화면은 폰 너비다"라고 브라우저에게 알려줘서, 우리가 앞으로 쓸 미디어 쿼리가 제대로 동작해요.
뷰포트 메타 태그 없음 뷰포트 메타 태그 있음
(폰을 980px로 가정·축소) (width=device-width)
┌───────────────┐ ┌───────────────┐
│ ┌───────────┐ │ │ 친구들의 일상에 │
│ │깨알같이작은 │ │ ← 손가락으로 │ 오신 것을 │
│ │데스크톱화면 │ │ 확대해야 읽힘 │ 환영합니다 │ ← 폰 너비에 딱 맞음
│ └───────────┘ │ │ [ 로그인 ] │
└───────────────┘ └───────────────┘
자, 오늘의 작업을 담을 새 CSS 파일을 하나 만들어요. 반응형 규칙만 따로 모을 css/responsive.css예요. 그리고 세 HTML 파일(index.html·feed.html·profile.html)의 <head>에 연결해요. 맨 마지막에 연결하는 게 포인트예요.
<link rel="stylesheet" href="css/base.css">
<link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/components.css">
<link rel="stylesheet" href="css/responsive.css">
왜 마지막이냐고요? CSS에는 같은 요소에 같은 속성이 여러 번 지정될 때, 우선순위가 같다면 나중에 적힌(나중에 로드된) 규칙이 이기는 규칙이 있어요. B-1에서 선택자 우선순위를 다룰 때 살짝 맛본 그 감각이에요. 이렇게 여러 규칙이 위에서 아래로 흘러 덮어쓰는 걸 캐스케이드(cascade)라고 불러요(CSS의 그 C가 바로 Cascading이죠). 반응형 규칙은 기존 규칙(layout.css의 3열 고정 등)을 덮어써야 하니까, 가장 마지막에 로드되어야 해요. 이건 다음 Step부터 진가를 발휘할 거예요.
💡 튜터의 결론: 반응형 웹은 하나의 코드로 모든 화면에 대응하는 기법이에요. 그 전제가 뷰포트 메타 태그(width=device-width)고, 이건 우리가 첫날부터 깔아둔 한 줄이었죠. 이제 responsive.css를 맨 마지막에 연결해 덮어쓸 준비를 끝냈어요. 그럼 "화면이 좁으면 이렇게, 넓으면 저렇게"를 어떻게 코드로 쓸까요? 다음 Step의 주인공, 미디어 쿼리예요.
Step 2: "@media 첫 만남 — 화면 너비를 조건으로 걸다"
반응형의 심장은 미디어 쿼리(Media Query)예요. 이름이 어렵게 들리지만, 하는 일은 단순한 조건문이에요. "화면이 어떤 조건일 때만 이 CSS를 적용해라"는 뜻이죠. 문법은 이래요.
@media (조건) {
/* 이 조건이 맞을 때만 적용할 CSS */
}
@media 뒤 괄호 안에 조건을 적고, 중괄호 { } 안에 그 조건일 때만 켜질 CSS 규칙을 넣어요. 가장 많이 쓰는 조건이 화면 너비예요. 두 가지를 기억하면 돼요.
(min-width: 600px)→ 화면 너비가 600px 이상일 때. min(최소)이 600이라는 뜻이니, 600부터 그 위로 전부예요.(max-width: 599px)→ 화면 너비가 599px 이하일 때. max(최대)가 599라는 뜻이니, 599부터 그 아래로 전부예요.
min과 max가 헷갈릴 수 있는데, "최소 이만큼은 돼야 한다(min) = 그 이상", "최대 이만큼까지다(max) = 그 이하"로 읽으면 돼요.
화면 너비 → 0 ─────── 599 │ 600 ─────────────→
│
(max-width: 599px) ◄────┤ 좁은 쪽(폰)에 적용
(min-width: 600px) ├────────► 넓은 쪽(태블릿↑)에 적용
눈으로 직접 확인해볼까요? 아래 코드를 잠깐 responsive.css에 넣어보면, 화면이 좁을 땐 배경이 분홍색, 넓을 땐 하늘색으로 변해요.
/* 실험용 — 파일에는 안 넣어요. 직접 넣고 창 너비를 바꿔보세요 */
@media (max-width: 599px) {
body { background-color: #ffe0e0; } /* 좁으면 분홍 */
}
@media (min-width: 600px) {
body { background-color: #e0f0ff; } /* 넓으면 하늘 */
}
브라우저 창의 가장자리를 마우스로 잡고 좌우로 줄였다 늘였다 해보세요. 너비가 600px을 넘나드는 순간 배경색이 탁 바뀌어요. 미디어 쿼리가 "지금 화면이 어느 구간인지"를 실시간으로 보고 CSS를 갈아끼우는 거예요. 이 배경색 실험은 미디어 쿼리가 동작하는지 확인하는 가장 빠른 방법이라, 실무에서도 디버깅할 때 종종 써요.
⚠️ 이 배경색 코드는 개념을 눈으로 보려는 실험용이라 우리 파일에는 넣지 않아요. 직접 넣어 확인한 뒤엔 지워주세요. 다음 Step부터 진짜로 파일에 남길 반응형 규칙을 작성합니다.
🙋 직접 해보세요 — "경곗값을 바꿔보기"
위 실험 코드에서 600px을 900px로 바꿔보세요. 이제 900px을 기준으로 색이 바뀌어요. 경계가 되는 숫자를 바꾸면 "색이 바뀌는 지점"이 그대로 따라 움직이는 게 보이죠? 이 경곗값을 브레이크포인트(breakpoint)라고 부르는데, 다음 Step에서 어떤 값을 고를지 배웁니다. 개발자 도구(F12)를 열고 왼쪽 위 기기 모양 아이콘(Toggle device toolbar)을 누르면, 폰·태블릿 화면을 흉내 내며 테스트할 수도 있어요.
💡 튜터의 결론: @media (조건) { }는 "화면이 이 조건일 때만 이 CSS를 켜라"는 조건문이에요. min-width는 그 값 이상, max-width는 그 값 이하를 뜻하죠. 배경색을 바꿔보면 미디어 쿼리가 실시간으로 동작하는 게 한눈에 보여요. 그런데 색이 바뀌는 경곗값(600px)은 제가 그냥 정한 거예요. 이 경계는 아무 숫자나 써도 될까요, 아니면 기준이 있을까요? 다음 Step에서 브레이크포인트를 잡아봅니다.
Step 3: "브레이크포인트 — 화면을 폰·태블릿·데스크톱으로 나누는 경계"
미디어 쿼리에서 화면을 나누는 경곗값을 브레이크포인트(breakpoint)라고 불러요. "여기서 레이아웃이 break(바뀌는) point(지점)"라는 뜻이죠. 그럼 이 경계를 어디에 둘까요?
세상엔 기기 화면 크기가 너무 많아서 "이 폰은 정확히 몇 px"을 다 맞출 순 없어요. 대신 대략적인 구간으로 나눠요. 가장 흔히 쓰는 세 구간은 이래요.
- 모바일(폰): 약 600px 미만
- 태블릿: 약 600px ~ 1023px
- 데스크톱: 약 1024px 이상
화면 너비 →
0 600 1024 →
│ 모바일 │ 태블릿 │ 데스크톱 │
│ (1열) │ (2열) │ (3열) │
└───────────┴────────────────┴───────────────────┘
브레이크포인트: 600px 브레이크포인트: 1024px
여기서 600과 1024라는 숫자는 법으로 정해진 게 아니에요. 흔히 쓰는 관습적인 값일 뿐이에요. 어떤 팀은 768px(옛날 태블릿 가로 너비)을 쓰고, 어떤 팀은 디자인에 맞춰 720px이나 960px을 쓰기도 해요. 중요한 건 정확한 숫자가 아니라, "내 콘텐츠가 어느 너비에서 답답해 보이기 시작하는가"를 보고 정하는 거예요.
우리 인스타그램은 사진 격자가 핵심이니까, "사진 한 칸이 너무 작아지지 않는 선"을 기준으로 600px(여기서 2열), 1024px(여기서 3열)을 브레이크포인트로 잡을게요. 폰에서는 1열, 태블릿에서는 2열, 데스크톱에서는 3열이 되도록요.
🙋 직접 해보세요 — "내 폰은 몇 px일까?"
개발자 도구(F12)를 열고 기기 도구 모음(Toggle device toolbar)을 켜보세요. 위쪽에 iPhone, iPad 같은 기기를 고르는 메뉴가 있어요. 기기를 바꿀 때마다 화면 너비(px)가 위에 표시돼요. iPhone은 보통 390~430px, iPad는 768~1024px 근처예요. 우리가 정한 600px·1024px 구간에 실제 기기들이 어떻게 들어가는지 직접 확인해보세요. "아, iPhone은 600 미만이니까 1열이 되겠구나" 하고요.
💡 튜터의 결론: 브레이크포인트는 레이아웃이 바뀌는 경곗값이고, 우리는 600px(태블릿)·1024px(데스크톱)으로 잡았어요. 정해진 정답이 아니라 콘텐츠가 답답해지는 지점을 보고 고르는 거죠. 그런데 구간이 셋이면, CSS를 "기본은 데스크톱"으로 쓰고 좁은 쪽을 덜어낼까요, 아니면 "기본은 폰"으로 쓰고 넓은 쪽을 더할까요? 이 출발점 선택이 코드의 깔끔함을 좌우해요. 다음 Step의 모바일 퍼스트입니다.
Step 4: "모바일 퍼스트 — 기본은 폰, 넓어질수록 더한다"
미디어 쿼리로 세 구간을 만들 때, 출발점을 어디에 둘지 두 가지 방법이 있어요.
- 데스크톱 퍼스트: 기본 CSS를 데스크톱(넓은 화면)으로 쓰고,
max-width로 "화면이 좁아지면 이렇게 줄여라"를 덜어내는 방식. - 모바일 퍼스트: 기본 CSS를 모바일(좁은 화면)으로 쓰고,
min-width로 "화면이 넓어지면 이렇게 더해라"를 쌓아가는 방식.
요즘 실무의 표준은 모바일 퍼스트예요. 기본을 폰으로 두고, 화면이 넓어질 때마다 한 겹씩 더하는 거죠.
모바일 퍼스트 = 기본(폰) 위에 한 겹씩 쌓기
기본 CSS + @media (min-width:600px) + @media (min-width:1024px)
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 1열 │ → │ 2열 │ → │ 3열 │
│ (폰) │ │ (태블릿) │ │(데스크톱)│
└─────────┘ └─────────┘ └─────────┘
가장 단순한 넓어지면 한 겹 추가 더 넓어지면 또 한 겹
상태가 기본
왜 모바일 퍼스트가 좋을까요? 세 가지 이유가 있어요.
첫째, 코드가 단순해져요. 폰 레이아웃은 보통 한 줄로 쭉 쌓이는 가장 단순한 형태예요. 이걸 기본으로 두면 기본 CSS가 깔끔하고, 복잡한 다단 배치는 넓은 화면에만 더하면 되거든요.
둘째, 점진적 향상(progressive enhancement)이에요. 가장 작은 화면에서도 일단 멀쩡히 보이는 걸 보장하고, 화면 여유가 생길 때마다 더 풍성하게 만드는 거죠. "최소한은 항상 동작한다"가 보장돼요.
셋째, 휴대폰 사용자가 다수예요. 인스타그램 방문자의 대부분이 폰이니, 그들이 보는 기본 화면을 먼저 챙기는 게 자연스럽죠.
그래서 우리 responsive.css는 이 형태로 갈 거예요. 조건 없는 기본 규칙(폰) → min-width로 위로 쌓기. max-width는 거의 안 쓰고요.
/* 기본 = 모바일 (조건 없음) */
.post-grid { /* 1열 */ }
/* 넓어지면 더한다 */
@media (min-width: 600px) { .post-grid { /* 2열 */ } }
@media (min-width: 1024px) { .post-grid { /* 3열 */ } }
💡 튜터의 결론: 모바일 퍼스트는 "기본은 폰, min-width로 넓은 화면을 한 겹씩 더하는" 방식이에요. 코드가 단순하고, 작은 화면을 항상 보장하고, 다수인 폰 사용자를 먼저 챙긴다는 세 가지 장점이 있죠. 이제 이론은 충분해요. 지난 시간 만든 3열 고정 격자를, 모바일 퍼스트로 1·2·3열로 갈아끼워봅시다.
Step 5: "게시물 격자를 1·2·3열로 — 드디어 반응형 격자"
자, 오늘의 하이라이트예요. 지난 시간 만든 layout.css의 게시물 격자는 이랬어요.
/* instagram-clone-frontend/css/layout.css (지난 시간) */
.post-grid {
display: grid;
grid-template-columns: repeat(3, 1fr); /* 3열 고정 */
gap: 0.5rem;
}
repeat(3, 1fr) 때문에 화면이 아무리 좁아져도 항상 3열이라, 폰에서 사진이 우표만큼 작아졌죠. 이걸 모바일 퍼스트로 갈아끼워요. layout.css는 그대로 두고, responsive.css에서 덮어쓸 거예요.
/* instagram-clone-frontend/css/responsive.css */
/* ===== 게시물 격자 — 화면 너비에 따라 1·2·3열 ===== */
/* 모바일 퍼스트: 기본은 1열(폰), min-width로 칸을 늘려간다 */
/* layout.css의 repeat(3, 1fr) 고정을 덮어쓴다 — 나중에 로드된 파일이 이긴다 */
.post-grid {
grid-template-columns: 1fr;
}
/* 태블릿 — 600px 이상이면 2열 */
@media (min-width: 600px) {
.post-grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* 데스크톱 — 1024px 이상이면 3열 */
@media (min-width: 1024px) {
.post-grid {
grid-template-columns: repeat(3, 1fr);
}
}
한 단계씩 읽어볼게요.
- 조건 없는 기본 규칙
.post-grid { grid-template-columns: 1fr }→ 모바일 기본은 1열이에요.1fr하나뿐이니 칸이 한 줄에 하나씩 세로로 쌓여요.responsive.css가 layout.css보다 나중에 로드되니, 이 한 줄이repeat(3, 1fr)을 덮어써요(Step 1에서 말한 캐스케이드죠). @media (min-width: 600px)→ 화면이 600px 이상(태블릿)이면repeat(2, 1fr), 즉 2열로 바뀌어요.@media (min-width: 1024px)→ 1024px 이상(데스크톱)이면repeat(3, 1fr), 3열로요.
grid-template-columns만 구간마다 바꿨다는 점에 주목하세요. display: grid나 gap은 layout.css에 이미 있으니 건드릴 필요가 없어요. 우리는 "열 개수"라는 딱 하나의 속성만 화면 구간별로 갈아끼운 거예요. 지난 시간 배운 Grid 위에 미디어 쿼리를 얹은 거죠.
폰 (~599px) 태블릿 (600~1023) 데스크톱 (1024~)
grid-template- repeat(2, 1fr) repeat(3, 1fr)
columns: 1fr
┌──────────┐ ┌─────┬─────┐ ┌────┬────┬────┐
│ 사진1 │ │사진1│사진2│ │사진1│사진2│사진3│
├──────────┤ ├─────┼─────┤ ├────┼────┼────┤
│ 사진2 │ │사진3│사진4│ │사진4│사진5│사진6│
├──────────┤ ├─────┼─────┤ ├────┼────┼────┤
│ 사진3 │ │사진5│사진6│ │사진7│사진8│사진9│
└──────────┘ └─────┴─────┘ └────┴────┴────┘
1열 2열 3열
지난 시간 끝에 던졌던 질문 기억하세요? "auto-fit만으로도 칸 개수가 화면에 반응하는데, 왜 미디어 쿼리가 필요할까?"였죠. 이제 답이 보여요. auto-fit은 "칸을 들어갈 수 있는 만큼 채워라"라서 우리가 정확히 "폰은 1열, 태블릿은 2열"이라고 구간을 못 박을 순 없었어요. 미디어 쿼리는 화면 구간을 직접 조건으로 걸어서, 각 구간마다 원하는 열 개수를 정확히 지정할 수 있죠. 그래서 정교한 반응형엔 미디어 쿼리가 필요한 거예요.
🙋 직접 해보세요 — "창을 줄이며 격자가 바뀌는 걸 보기"
profile.html을 열고 브라우저 창 너비를 천천히 줄여보세요(창 가장자리를 마우스로 드래그). 1024px → 그 아래로 내려가는 순간 3열이 2열로, 600px 아래로 가면 2열이 1열로 탁탁 바뀌어요. 개발자 도구의 기기 도구 모음에서 iPhone을 고르면 단번에 1열이 되는 것도 확인해보세요. 화면이라는 그릇에 격자가 맞춰지는 게 바로 이거예요.
(브레이크포인트 값 600·1024를 720·1200 같은 다른 값으로 바꿔보면, 격자가 바뀌는 지점도 따라 움직여요. 직접 바꿔보세요.)
💡 튜터의 결론: 모바일 퍼스트로 기본 1열을 깔고, min-width: 600px에서 2열, min-width: 1024px에서 3열로 쌓았어요. grid-template-columns 하나만 구간별로 갈아끼우면 되죠. 지난 시간의 숙제였던 "3열 고정의 답답함"이 이걸로 풀렸어요. 그런데 폰에서 피드를 보면, 카드가 여전히 가운데에 좁게 떠 있어요. 진짜 인스타그램 앱은 사진이 화면을 꽉 채우잖아요? 다음 Step에서 카드를 화면 끝까지 펼칩니다.
Step 6: "모바일에서 카드를 화면 끝까지 — full-bleed"
폰으로 진짜 인스타그램 앱을 떠올려보세요. 피드의 사진이 화면 좌우를 꽉 채우죠. 양옆에 여백이 거의 없어요. 그런데 우리 피드 카드(<article>)는 components.css에서 이렇게 만들어뒀어요.
/* instagram-clone-frontend/css/components.css (지난 모듈) */
article {
max-width: 32rem;
margin: 1rem auto;
border: 1px solid #dbdbdb;
border-radius: 8px;
...
}
max-width: 32rem에 margin: ... auto라, 카드가 항상 가운데에 32rem(약 512px) 폭으로 떠 있어요. 데스크톱에선 보기 좋지만, 폰에선 양옆에 어색한 여백이 남고 둥근 모서리도 답답해 보여요. 모바일에선 카드를 화면 끝까지 펼치고 싶어요. 이걸 full-bleed(풀블리드, 가장자리까지 꽉 채움)라고 불러요.
모바일 퍼스트로 작성해볼게요. 기본(폰)은 full-bleed, 태블릿 이상에서 카드 틀을 복원하는 거예요.
/* instagram-clone-frontend/css/responsive.css */
/* ===== 피드 카드 — 모바일에선 화면 끝까지(full-bleed) ===== */
/* 폰에선 좌우 여백·둥근 모서리를 없애 꽉 차게 (실제 인스타 느낌) */
article {
max-width: 100%;
margin-left: 0;
margin-right: 0;
border-radius: 0;
}
/* 태블릿 이상 — 카드 틀 복원 (가운데 정렬 + 둥근 모서리) */
@media (min-width: 600px) {
article {
max-width: 32rem;
margin-left: auto;
margin-right: auto;
border-radius: 8px;
}
}
읽어볼게요.
- 기본 규칙(폰) →
max-width: 100%로 카드가 화면 너비를 꽉 채우고, 좌우margin을0으로,border-radius: 0으로 둥근 모서리를 없애요. 화면 끝까지 펼쳐진 모양이 되죠. @media (min-width: 600px)→ 태블릿 이상에선 다시max-width: 32rem에margin: ... auto로 가운데 정렬하고, 둥근 모서리도 되살려요. 카드 틀이 복원되는 거예요.
이게 모바일 퍼스트의 실전 모습이에요. 가장 단순한 폰 상태(꽉 채움)를 기본으로 두고, 넓어지면 카드 틀이라는 장식을 한 겹 더하는 거죠.
폰 (~599px) full-bleed 태블릿 이상 (600px~)
┌────────────────┐ ┌──────────────────────┐
│사진이 화면 끝까지│ │ ┌────────┐ │
│████████████████│ │ │ 카드틀 │ │ ← 가운데 32rem
│████████████████│ │ │ 둥근모서리│ │
│████████████████│ │ └────────┘ │
└────────────────┘ └──────────────────────┘
여백 0, 모서리 0 max-width:32rem 복원
🙋 직접 해보세요 — "모바일과 데스크톱을 오가며 카드 보기"
feed.html을 열고 개발자 도구 기기 도구 모음에서 iPhone을 골라보세요. 사진 카드가 화면 좌우를 꽉 채우고 모서리가 각진 걸 확인하세요. 그다음 Responsive 모드로 바꿔 창을 600px 위로 넓히면, 카드가 가운데로 모이고 둥근 모서리가 다시 살아나요. 같은 카드가 화면 너비에 따라 두 가지 모습으로 변신하는 거예요. border-radius나 max-width 값을 바꿔 카드 느낌을 직접 조절해보세요.
💡 튜터의 결론: 모바일 퍼스트로 기본은 full-bleed(화면 끝까지), 태블릿 이상에선 카드 틀을 복원했어요. 폰용 인스타그램다운 모습이 됐죠. 그런데 지금까지 우리가 건 조건은 전부 "화면(뷰포트) 너비"였어요. 만약 어떤 요소를 "화면이 아니라, 그 요소가 담긴 부모 칸의 크기"에 반응하게 하고 싶으면 어떡할까요? 다음 Step의 컨테이너 쿼리예요.
Step 7: "컨테이너 쿼리(@container) — 화면 말고 '부모 칸' 크기에 반응"
미디어 쿼리(@media)는 항상 화면(뷰포트) 전체의 너비를 봐요. 그런데 가끔은 "화면이 아니라, 이 요소가 담긴 부모 칸이 얼마나 넓은가"에 반응하고 싶을 때가 있어요.
예를 들어 똑같은 사진 카드를 넓은 본문에도 넣고 좁은 사이드바에도 넣는다고 해봐요. 화면 크기는 똑같은데 카드가 담긴 칸의 너비는 다르죠. 이럴 때 "내가 담긴 칸이 좁으면 작게, 넓으면 크게"를 정하고 싶으면, 화면을 보는 @media로는 안 돼요. 그래서 나온 게 **컨테이너 쿼리(@container)**예요. "화면" 대신 "부모 컨테이너"의 크기를 조건으로 거는 거죠.
우리 게시물 격자로 실험해볼게요. 각 격자 칸(figure)을 컨테이너로 선언하고, "칸이 200px 이상으로 넓어지면 사진 설명 글씨를 키워라"는 규칙을 걸어요.
/* instagram-clone-frontend/css/responsive.css */
/* ===== 컨테이너 쿼리 — 화면이 아니라 '부모 칸' 크기에 반응 ===== */
/* 각 격자 칸(figure)을 컨테이너로 선언 */
.post-grid figure {
container-type: inline-size;
container-name: post-cell;
}
/* 칸 너비가 200px 이상으로 넓어지면 설명 글씨를 키운다 */
@container post-cell (min-width: 200px) {
.post-grid figcaption {
font-size: 1rem;
}
}
두 단계예요.
- 먼저 부모가 될 요소(
figure)에container-type: inline-size를 줘요. "이 요소를 가로 너비 기준 컨테이너로 삼겠다"는 선언이에요(inline-size = 가로축 크기).container-name: post-cell은 그 컨테이너에 이름을 붙인 거고요. - 그다음
@container post-cell (min-width: 200px) { }로 "post-cell이라는 컨테이너의 너비가 200px 이상이면" 안쪽figcaption의 글씨를 1rem으로 키워요(기본은 layout.css에서 0.8rem이었죠).
여기서 @media와의 차이가 또렷하게 드러나요. 프로필 게시물 영역은 데스크톱에서도 32rem(약 512px) 폭으로 가둬져 있어요. 그 안에서 3열이면 칸 하나가 약 165px이라 200px보다 좁죠. 그래서 데스크톱처럼 큰 화면인데도 글씨가 작게 유지돼요. 반대로 폰에서 1열이 되면 칸이 화면 너비만큼 넓어져 200px을 넘으니 글씨가 커지고요.
@media였다면 @container는
"화면이 크니까 글씨 키워" "이 칸이 좁으니까 글씨 작게 둬"
데스크톱 → 무조건 큰 글씨 데스크톱이어도 칸이 165px이면 작게
폰에서 칸이 넓어지면(1열) 크게
→ 화면을 본다 → 부모 칸을 본다 (정반대 결과!)
신기하죠? 같은 큰 화면인데도 @container는 "칸이 좁으니까"를 이유로 글씨를 작게 둬요. 화면이 아니라 부모 칸을 보기 때문이에요. 이게 컨테이너 쿼리가 미디어 쿼리로는 할 수 없는 일을 해내는 지점이에요. 컴포넌트(화면 조각)가 어디에 놓이든, 자기가 담긴 칸의 크기에 맞춰 스스로 모습을 바꿀 수 있는 거죠.
⚠️ 컨테이너 쿼리(
@container)는 2023년부터 모든 주요 브라우저(Chrome·Firefox·Safari·Edge)에서 안정적으로 지원돼요(전 세계 사용자의 93% 이상). 지금 실무에서 마음 놓고 써도 되는 표준이에요. 다만 "화면 전체"를 나누는 큰 레이아웃엔 여전히@media가 어울리고, "재사용되는 작은 조각"엔@container가 어울려요. 둘은 경쟁이 아니라 역할이 달라요.
🙋 직접 해보세요 — "칸 크기와 화면 크기를 따로 바꿔보기"
profile.html을 데스크톱 너비로 열어두고 게시물 설명 글씨 크기를 보세요(3열이라 칸이 좁아 작게 보일 거예요). 그다음 창을 600px 아래로 줄여 1열이 되게 하면, 화면은 더 작아졌는데도 칸이 넓어져서 글씨가 오히려 커져요. "화면 크기"와 "글씨 크기"가 거꾸로 움직이는 거죠. 만약 이게 @media (min-width:...)였다면 화면이 작아질 때 글씨도 작아졌을 거예요. @container라서 정반대로 동작한다는 걸 눈으로 확인해보세요.
💡 튜터의 결론: @container는 화면이 아니라 부모 컨테이너의 크기에 반응해요. 부모에 container-type: inline-size를 선언하고, @container 이름 (조건) { }으로 조건을 걸죠. 화면을 보는 @media와 정반대 결과가 나오기도 하는데, 그게 바로 재사용되는 조각을 다룰 때 컨테이너 쿼리가 필요한 이유예요. 이제 레이아웃은 모든 화면에 대응해요. 마지막으로, 레이아웃만이 아니라 이미지 자체도 화면에 맞게 보내봅시다.
Step 8: "반응형 이미지 ① — srcset과 sizes로 알맞은 해상도를"
레이아웃을 반응형으로 만들었으니, 이제 이미지를 봐요. 생각해보면 좀 낭비스러운 일이 벌어지고 있어요. 폰의 작은 화면에도, 큰 모니터에도 똑같이 큰 이미지 파일(600×600)을 보내고 있거든요. 폰에서는 그렇게 큰 이미지가 필요 없는데도요. 데이터도 낭비하고 로딩도 느려져요.
<img>의 srcset과 sizes 속성이 이 문제를 풀어줘요. 여러 해상도의 이미지를 후보로 주면, 브라우저가 화면 크기와 화질을 보고 알맞은 걸 골라 받는 거예요.
<!-- instagram-clone-frontend/feed.html -->
<img src="https://picsum.photos/seed/insta1/600/600"
srcset="https://picsum.photos/seed/insta1/400/400 400w,
https://picsum.photos/seed/insta1/600/600 600w,
https://picsum.photos/seed/insta1/800/800 800w"
sizes="(min-width: 600px) 32rem, 100vw"
alt="석양이 지는 제주도 협재 해변 — 하늘이 주황빛으로 물들었다"
width="600" height="600">
하나씩 풀어볼게요.
srcset→ 후보 이미지 목록이에요. 각 항목은 "이미지 주소 + 그 이미지의 실제 가로 픽셀"로 적어요.400w는 "이 파일은 가로 400px짜리"라는 뜻이에요(w는 width). 브라우저에게 "400·600·800px짜리 세 종류가 있어"라고 알려주는 거죠.sizes→ "이 이미지가 화면에서 실제로 얼마나 넓게 보일지"를 알려줘요.(min-width: 600px) 32rem은 "600px 이상 화면에선 이 이미지가 32rem 폭으로 보인다",100vw는 "그 외(폰)에선 화면 너비(viewport width)의 100%로 보인다"는 뜻이에요. 브라우저는 이 정보로 "그럼 몇 px짜리 파일이 적당하겠다"를 계산해요.src→srcset을 이해 못 하는 아주 오래된 브라우저를 위한 기본 이미지예요. 안전망인 셈이죠.
브라우저는 이 정보를 종합해서, 폰에선 작은 400짜리를, 큰 화면에선 800짜리를 알아서 받아요. 우리가 일일이 시키지 않아도요.
그런데 이건 눈에 잘 안 보여요. 화면엔 똑같이 사진이 보이니까요. 차이는 "어떤 파일이 실제로 다운로드됐는가"에 있어요. 이건 개발자 도구의 Network(네트워크) 탭에서 확인해요.
srcset = 후보 여러 개, 브라우저가 고름
400w ┐
600w ├─→ 브라우저가 화면 크기·화질 보고 → 폰: 400w 받음
800w ┘ 큰 화면: 800w 받음
(화면엔 똑같이 보이지만, 받아온 파일 크기가 다름)
🙋 직접 해보세요 — "어떤 이미지가 받아졌는지 Network 탭에서 보기"
feed.html을 열고 개발자 도구(F12)의 Network(네트워크) 탭을 켜요. 위쪽 필터에서 Img(이미지)를 고르고 새로고침(Ctrl+R / Cmd+R)하면, 어떤 이미지 파일이 받아졌는지 목록에 떠요. insta1로 시작하는 파일 이름 끝의 숫자(/400/400인지 /800/800인지)를 보세요. 그다음 기기 도구 모음에서 iPhone으로 바꾸고 다시 새로고침하면, 더 작은 해상도가 받아지는 걸 확인할 수 있어요. 화면엔 똑같아 보여도 받아온 파일이 다른 게 srcset의 핵심이에요.
💡 튜터의 결론: srcset은 여러 해상도의 이미지를 후보로 주고, sizes로 "화면에서 얼마나 크게 보일지"를 알려주면, 브라우저가 알맞은 파일을 골라 받아요. 폰엔 작은 파일, 큰 화면엔 큰 파일을 보내 데이터와 속도를 아끼는 거죠. 눈엔 안 보이니 Network 탭으로 확인하고요. 그런데 srcset은 "같은 사진의 다른 해상도"를 고를 뿐이에요. 만약 폰과 데스크톱에 아예 다른 사진(예: 폰엔 세로 크롭, 데스크톱엔 가로 와이드)을 보내고 싶으면? 다음 Step의 <picture>예요.
Step 9: "반응형 이미지 ② — <picture>로 화면별 다른 이미지"
srcset은 "같은 사진을 해상도만 다르게" 고르는 도구였어요. 그런데 진짜 인스타그램이나 뉴스 사이트를 보면, 폰에선 인물 위주로 꽉 찬 세로 사진을, 큰 화면에선 배경까지 넓게 보이는 가로 사진을 쓰기도 해요. 같은 장면이라도 화면에 따라 다른 크롭(잘라낸 모양)을 보내는 거죠. 이걸 아트 디렉션(art direction)이라고 불러요.
srcset은 "어떤 걸 고를지 브라우저가 알아서" 정하지만, 아트 디렉션은 "내가 화면 조건마다 어떤 이미지를 줄지 직접 지정"하는 거예요. 이걸 위한 태그가 <picture>예요.
<!-- instagram-clone-frontend/feed.html -->
<picture>
<!-- 큰 화면(600px↑): 가로로 넓은 풍경 컷 -->
<source media="(min-width: 600px)"
srcset="https://picsum.photos/seed/insta1wide/800/450">
<!-- 기본(폰): 정사각형 + 해상도별 3종 -->
<img src="https://picsum.photos/seed/insta1/600/600"
srcset="https://picsum.photos/seed/insta1/400/400 400w,
https://picsum.photos/seed/insta1/600/600 600w,
https://picsum.photos/seed/insta1/800/800 800w"
sizes="(min-width: 600px) 32rem, 100vw"
alt="석양이 지는 제주도 협재 해변 — 하늘이 주황빛으로 물들었다"
width="600" height="600">
</picture>
구조를 읽어볼게요.
<picture>는 여러 이미지 후보를 감싸는 상자예요. 그 안에<source>(조건별 후보)와<img>(기본)가 들어가요.<source media="(min-width: 600px)" srcset="...">→ "화면이 600px 이상이면 이 이미지(가로로 넓은 800×450)를 써라".media속성에 미디어 쿼리 조건을 그대로 적어요.- 맨 아래
<img>→ 위<source>조건에 안 맞으면(폰이면) 이 기본 이미지(정사각형)를 써요.<picture>안에서<img>는 반드시 있어야 해요. 실제 화면에 그려지는 건 결국 이<img>고,alt·width·height도 여기 적거든요.<source>는 "조건이 맞으면 이걸로 바꿔치기해"라고 거드는 역할이고요.
브라우저는 위에서부터 <source>의 조건을 확인해서, 처음 맞는 걸 써요. 다 안 맞으면 <img>로 떨어지고요. 그래서 데스크톱에선 가로 와이드 사진이, 폰에선 정사각형 사진이 나와요. 이번엔 크롭 모양이 다르니 눈에 확 보여요 — 창을 넓혔다 줄이면 사진이 통째로 바뀌거든요.
<picture> = 내가 화면별로 이미지를 직접 지정 (아트 디렉션)
폰 (~599px) 데스크톱 (600px↑)
<img> 기본 <source media="(min-width:600px)">
┌──────────┐ ┌────────────────────┐
│ 정사각형 │ │ 가로 와이드 800×450 │
│ 600×600 │ └────────────────────┘
└──────────┘ 창을 넓히면 사진이 통째로 바뀜
srcset과 <picture>, 둘을 한 표로 정리하면 경계가 또렷해져요.
srcset (img 속성) |
<picture> + <source> |
|
|---|---|---|
| 누가 고르나 | 브라우저가 알아서 | 내가 조건마다 지정 |
| 무엇이 다른가 | 같은 사진, 해상도만 | 화면별 다른 이미지·크롭 |
| 언제 쓰나 | 데이터·속도 최적화 | 아트 디렉션(폰/데스크톱 다른 그림) |
| 눈에 보이나 | 거의 안 보임(파일만 다름) | 확 보임(그림이 바뀜) |
🙋 직접 해보세요 — "창을 넓혔다 줄이며 사진이 바뀌는 걸 보기"
feed.html의 첫 게시물을 보면서 브라우저 창 너비를 600px 위아래로 넘나들게 줄였다 늘였다 해보세요. 600px을 넘는 순간 정사각형 사진이 가로로 넓은 사진으로 통째로 바뀌어요(picsum이 주는 임의 사진이라 그림 자체도 달라 보일 거예요). <source>의 media 조건을 (min-width: 900px)로 바꾸면, 사진이 바뀌는 지점도 900px로 옮겨가요. srcset과 달리 이건 눈에 바로 보이는 게 <picture>의 특징이에요.
💡 튜터의 결론: <picture>는 <source media="...">로 화면 조건마다 다른 이미지를 직접 지정하는 도구예요. srcset이 "브라우저가 해상도를 고르는" 것이라면, <picture>는 "내가 화면별로 그림을 바꾸는"(아트 디렉션) 것이죠. <picture> 안엔 기본 <img>가 반드시 있어야 하고요. 이걸로 레이아웃부터 이미지까지, 모든 화면에 대응하는 인스타그램이 완성됐어요. 정리해볼까요?
마무리
오늘은 화면 크기에 따라 디자인이 변하는 반응형 웹을 손에 넣었어요. 첫날부터 깔려 있던 뷰포트 메타 태그의 정체를 확인하고, 미디어 쿼리로 화면을 구간별로 나누고, 모바일 퍼스트로 3열 고정 격자를 1·2·3열로 갈아끼웠죠. 카드를 폰에서 화면 끝까지 펼치고, 컨테이너 쿼리로 부모 칸에 반응하는 글씨까지, 그리고 이미지도 화면에 맞게 골라 보냈고요.
오늘 배운 것
- 뷰포트 메타 태그 →
width=device-width로 "화면 = 기기 너비"를 선언. 반응형의 전제. @media (조건) { }→ 화면이 조건일 때만 CSS를 켜는 조건문.min-width는 그 이상,max-width는 그 이하.- 브레이크포인트 → 레이아웃이 바뀌는 경곗값(우리는 600px·1024px). 콘텐츠를 보고 정함.
- 모바일 퍼스트 → 기본은 폰,
min-width로 넓은 화면을 한 겹씩 더하기. - 반응형 격자 →
grid-template-columns만 구간별로 1fr → 2열 → 3열로 갈아끼움. - full-bleed → 모바일에선 카드를 화면 끝까지, 태블릿 이상에서 틀 복원.
@container→ 화면이 아니라 부모 칸 크기에 반응.container-type: inline-size선언 후 조건.srcset/sizes→ 같은 사진의 여러 해상도 후보, 브라우저가 골라 받음.<picture>/<source>→ 화면별로 다른 이미지를 직접 지정(아트 디렉션).
반응형, 무엇을 언제?
| 하고 싶은 것 | 도구 |
|---|---|
| 화면 구간별 레이아웃 전환 (폰/태블릿/데스크톱) | @media |
| 재사용 조각을, 담긴 칸 크기에 맞춰 | @container |
| 같은 사진, 화면에 맞는 해상도로 (속도·데이터) | srcset + sizes |
| 화면별로 아예 다른 이미지·크롭 | <picture> + <source> |
외울 필요 없어요. "화면 너비로 레이아웃 바꾸기 → @media, 부모 칸으로 조각 바꾸기 → @container, 이미지 골라 보내기 → srcset·<picture>"만 기억하면 돼요.
다음 시간 예고
여기까지 오면서 우리 인스타그램은 모든 화면에서 제대로 보이게 됐어요. 그런데 한 가지가 빠졌죠. 진짜 인스타그램은 좋아요 버튼을 누르면 하트가 통통 튀고, 메뉴가 스르륵 나타나잖아요? 지금 우리 화면은 멈춰 있어요. 정적이죠.
다음 시간엔 화면에 생동감을 불어넣는 애니메이션과 전환(Animation & Transition)을 배웁니다. 마우스를 올리면 부드럽게 색이 변하는 transition, 요소를 움직이고 키우고 돌리는 transform, 그리고 좋아요 하트가 통통 튀는 @keyframes까지요. CSS만으로 화면이 살아 움직이는 마법을 부려볼 거예요.
오늘 모든 화면에 대응하는 레이아웃을 완성했으니, 다음 시간엔 그 위에 움직임을 얹어봅시다!
과제
오늘 배운 반응형을 직접 익혀볼 차례예요. 기초 → 응용 → 탐구 순서로 풀어보세요. 모든 과제는 지금까지 배운 HTML과 CSS만으로 충분히 해낼 수 있어요.
[구현] 탐색 페이지 격자를 반응형으로 (기초)
지난 시간 과제에서 만든 탐색 페이지 4열 격자(.explore-grid)를, 화면 구간별로 칸 개수가 바뀌는 반응형 격자로 만들어보세요.
- 모바일 퍼스트로 작성하세요. 기본(폰)은 2열,
min-width: 600px에서 3열,min-width: 1024px에서 4열이 되도록요. grid-template-columns만 구간별로 갈아끼우면 돼요.display: grid나gap은 기본 규칙에 한 번만 쓰고요.- 브라우저 창을 줄였다 늘였다 하면서, 탐색 격자가 2 → 3 → 4열로 바뀌는지 확인하세요.
[구현] 프로필 헤더를 모바일에서 세로로 (응용)
지난 시간 만든 프로필 헤더(.profile-header)는 아바타가 왼쪽, 이름·통계·소개가 오른쪽인 가로 배치예요. 폰처럼 좁은 화면에선 이게 답답할 수 있어요. 모바일에선 아바타가 위, 글이 아래로 쌓이는 세로 배치로 바꿔보세요.
responsive.css에서 기본(폰)일 때.profile-header의grid-template-areas를 세로로 쌓이게 다시 적어보세요(예:"avatar""name""stats""bio"처럼 한 줄에 하나씩).grid-template-columns도 모바일에선 한 칸(1fr)으로 바꿔야 해요. 왜 그래야 하는지 생각하면서요.@media (min-width: 600px)에서 지난 시간의 가로 배치("avatar name"등)를 복원하세요.- 모바일 퍼스트 순서(기본 = 세로, min-width로 가로 복원)로 작성하는 게 핵심이에요.
[탐구] 반응형 이미지가 실제로 무엇을 받는지 측정하기
srcset은 눈에 안 보이지만, 개발자 도구로 측정할 수 있어요. feed.html의 첫 게시물 이미지가 화면 크기에 따라 어떤 파일을 받는지 직접 확인하고 정리해보세요.
- 개발자 도구
Network탭에서 이미지 필터를 켜고, 데스크톱 너비 / iPhone 너비 각각에서 새로고침해 받아진 이미지의 해상도(/400/인지/800/인지)를 적어보세요. - 받아진 파일이 다르다면,
sizes속성의 어떤 부분이 그 선택에 영향을 줬을지 한두 문장으로 설명해보세요. <picture>의 첫 게시물은 600px을 넘나들 때 그림이 통째로 바뀌죠.srcset(안 보임)과<picture>(보임)의 차이를 직접 관찰한 대로 정리해보세요.
생각해볼 주제
정답을 적는 문제가 아니에요. 오늘 배운 도구의 "왜"를 곱씹어보는 질문들이에요. 스스로 답을 만들어본 뒤, 예시답안과 비교해보세요.
1. 왜 데스크톱 퍼스트가 아니라 모바일 퍼스트를 권할까?
미디어 쿼리는 min-width(넓어지면 더하기)로도, max-width(좁아지면 덜어내기)로도 쓸 수 있어요. 그런데 요즘 실무는 대부분 모바일 퍼스트(min-width)를 표준으로 삼아요. 똑같은 결과를 만들 수 있는데도 왜 한쪽을 권할까요? 코드의 단순함, 작은 화면의 보장, 방문자의 다수가 누구인가 같은 관점에서 생각해보세요.
2. @media와 @container, 둘의 경계는 어디일까?
둘 다 "조건에 따라 CSS를 바꾼다"는 점은 같아요. 하지만 하나는 화면(뷰포트)을, 하나는 부모 칸을 봐요. 같은 카드라도 화면이 클 때 무조건 커지길 원하면 무엇을, 좁은 사이드바에 들어갔을 때 작아지길 원하면 무엇을 써야 할까요? "이 요소가 화면 전체의 크기를 따라야 하나, 자기가 담긴 칸의 크기를 따라야 하나"라는 질문에서 출발해보세요.
3. 이미지를 그냥 큰 것 하나만 쓰면 안 될까? 왜 srcset·<picture>까지 필요할까?
사실 큰 이미지 하나를 width: 100%로 줄여 쓰면, 화면이 작을 때도 그럭저럭 보이긴 해요. 그런데도 srcset과 <picture>를 쓰는 이유가 뭘까요? 폰 사용자가 큰 파일을 받을 때의 데이터·속도 비용, 그리고 폰의 작은 화면에선 가로로 넓은 사진보다 다른 크롭이 더 보기 좋을 수 있다는 점에서 생각해보세요. "보이기만 하면 된다"와 "잘 보이고 빠르다"의 차이예요.
✅ 예시 답안정답 보기
과제와 생각해볼 주제의 예시답안이에요. 정답이 하나만 있는 건 아니에요. 브레이크포인트 값이나 칸 개수는 취향대로 골라도 좋아요. 중요한 건 모바일 퍼스트로 기본을 깔고,
@media (min-width: ...)로 넓은 화면을 한 겹씩 더했는가 예요.
🎯 [과제 1 예시답안] 탐색 페이지 격자를 반응형으로
핵심 접근
지난 시간 만든 4열 고정 격자(.explore-grid)를, 화면 구간별로 칸 개수가 바뀌게 만드는 과제예요. 오늘 게시물 격자에서 한 것과 똑같은 패턴이에요. 기본(폰)을 가장 적은 열로 깔고, min-width로 열을 늘려가는 거죠. display: grid와 gap은 기본 규칙에 한 번만 쓰고, 구간마다 grid-template-columns만 갈아끼우는 게 포인트예요.
예시 구현
/* instagram-clone-frontend/css/responsive.css 에 추가 */
/* ===== 탐색 격자 — 모바일 2열, 태블릿 3열, 데스크톱 4열 ===== */
.explore-grid {
display: grid;
grid-template-columns: repeat(2, 1fr); /* 기본(폰): 2열 */
gap: 2px;
}
@media (min-width: 600px) {
.explore-grid {
grid-template-columns: repeat(3, 1fr); /* 태블릿: 3열 */
}
}
@media (min-width: 1024px) {
.explore-grid {
grid-template-columns: repeat(4, 1fr); /* 데스크톱: 4열 */
}
}
기본 규칙에 repeat(2, 1fr)로 폰에서 2열을 깔고, min-width: 600px에서 3열, min-width: 1024px에서 4열로 쌓았어요. display: grid와 gap: 2px는 기본 규칙에만 적었으니 모든 구간에 그대로 이어져요. 탐색 화면은 사진을 빽빽하게 보여주는 곳이라, 게시물 격자(1·2·3열)보다 한 단계씩 많은 2·3·4열로 잡았어요.
채점 포인트
| 항목 | 확인 내용 |
|---|---|
| 모바일 퍼스트 | 기본 규칙을 가장 적은 열(2열)로 깔았는가 |
| min-width 쌓기 | @media (min-width: ...)로 600px·1024px에서 열을 늘렸는가 |
| 중복 없음 | display: grid·gap을 기본에만 쓰고 반복하지 않았는가 |
| 확인 | 창을 줄였다 늘이며 2 → 3 → 4열 전환을 확인했는가 |
흔한 실수
- 데스크톱 퍼스트로 거꾸로 작성 — 기본을 4열로 깔고
max-width로 줄이면 동작은 하지만, 모바일 퍼스트 흐름과 섞이면 코드가 헷갈려요. 오늘 배운 대로 기본 = 가장 좁은 화면으로 통일하세요. - 구간마다
display: grid를 또 씀 —display: grid는 한 번만 켜면 모든 구간에 유지돼요. 미디어 쿼리 안에서 다시 쓸 필요가 없어요. 바꾸는 건grid-template-columns하나뿐이에요.
🎯 [과제 2 예시답안] 프로필 헤더를 모바일에서 세로로
핵심 접근
프로필 헤더는 지난 시간 grid-template-areas로 "왼쪽 아바타 + 오른쪽 글" 가로 배치를 만들었죠. 폰에선 이게 좁으니, 모바일에선 세로로 쌓고 태블릿 이상에서 가로를 복원하는 과제예요. 핵심은 두 가지. 모바일에선 grid-template-areas를 한 줄에 하나씩 세로로 다시 적고, grid-template-columns도 **한 칸(1fr)**으로 바꾸는 거예요. 열이 둘이면 세로로 안 쌓이거든요.
예시 구현
/* instagram-clone-frontend/css/responsive.css 에 추가 */
/* ===== 프로필 헤더 — 모바일에선 세로로 쌓기 ===== */
/* 기본(폰): 아바타·이름·통계·소개를 한 줄에 하나씩 세로로 */
.profile-header {
grid-template-columns: 1fr;
grid-template-areas:
"avatar"
"name"
"stats"
"bio";
justify-items: center;
}
/* 태블릿 이상 — 지난 시간의 가로 배치 복원 */
@media (min-width: 600px) {
.profile-header {
grid-template-columns: auto 1fr;
grid-template-areas:
"avatar name"
"avatar stats"
"avatar bio";
justify-items: stretch;
}
}
기본 규칙에서 grid-template-columns: 1fr로 열을 하나로 만들고, grid-template-areas를 "avatar" "name" "stats" "bio"처럼 한 줄에 하나씩 적었어요. 그러면 네 영역이 위에서 아래로 차곡차곡 쌓여요. justify-items: center로 가운데 정렬까지 하면 폰에서 보기 좋고요.
여기서 왜 grid-template-columns를 1fr 한 칸으로 바꿔야 하는지가 핵심이에요. 지난 시간엔 auto 1fr로 열이 둘(아바타 열 + 글 열)이었어요. 열이 둘인 채로 areas만 세로로 적으면 격자가 어긋나요. 세로로 쌓으려면 "열은 하나"여야 영역들이 한 줄씩 내려가거든요. 그래서 @media (min-width: 600px)에서 열을 다시 auto 1fr로, areas도 가로 배치로 복원한 거예요.
채점 포인트
| 항목 | 확인 내용 |
|---|---|
| 세로 areas | 모바일 기본에서 grid-template-areas를 한 줄에 하나씩 적었는가 |
| 열 1개로 | grid-template-columns를 모바일에서 1fr로 바꿨는가 |
| 가로 복원 | min-width: 600px에서 지난 시간 가로 배치를 되살렸는가 |
| 모바일 퍼스트 | 기본 = 세로, min-width로 가로 복원 순서로 작성했는가 |
흔한 실수
- areas만 세로로 바꾸고 열을 그대로 둠 —
grid-template-columns: auto 1fr(2열)인 채로 areas를 세로로 적으면 격자가 깨져요. 한 줄의 이름 개수 = 열 개수라는 지난 시간 규칙을 떠올리세요. 세로로 쌓으려면 열도 하나여야 해요. grid-area이름을 바꿔야 한다고 오해 — 자식의grid-area: avatar등은 그대로 둬도 돼요. 바뀌는 건 부모의 areas 그림뿐이에요. 이름표(자식)는 그대로, 그림(부모)만 다시 그리는 거죠.
🎯 [과제 3 예시답안] 반응형 이미지가 실제로 무엇을 받는지 측정하기
핵심 접근
srcset은 화면엔 똑같이 보여서 눈으로 확인이 안 돼요. 그래서 개발자 도구 Network 탭으로 "실제로 받아진 파일"을 측정하는 과제예요. 데스크톱과 폰 두 환경에서 새로고침해 받아진 해상도를 비교하고, 그 선택에 sizes 속성이 어떻게 작용했는지 설명하는 게 목표예요.
예시 측정 결과
Network 탭에서 이미지 필터를 켜고 두 환경에서 새로고침한 결과를 정리하면 이런 식이에요(정확한 값은 화면 화질 설정에 따라 조금 다를 수 있어요).
| 환경 | 화면 너비 | 받아진 이미지 | 이유 |
|---|---|---|---|
| 데스크톱 | 약 1280px | /insta1/800/800 (또는 /insta1wide/800/450) |
600px↑이라 <picture>의 가로 와이드 <source> 사용 |
| iPhone | 약 390px | /insta1/400/400 |
600px 미만이라 정사각형 <img>, sizes의 100vw로 계산해 작은 해상도 선택 |
설명을 붙이면 이래요.
sizes="(min-width: 600px) 32rem, 100vw"에서, 폰(600px 미만)은 뒤쪽 100vw가 적용돼요. "이 이미지는 화면 너비의 100%로 보인다"는 뜻이죠. 폰 화면이 390px이니 브라우저는 "390px 정도면 400짜리 파일이면 충분하다"고 판단해 400w를 받아요. 반면 데스크톱은 앞쪽 (min-width: 600px) 32rem이 적용돼, 32rem(약 512px)에 화질 여유까지 고려해 더 큰 파일을 받고요.
그리고 <picture>의 첫 게시물은 600px을 넘나들 때 정사각형 ↔ 가로 와이드로 그림이 통째로 바뀌어요(눈에 보임). 반면 srcset만 다른 게시물은 화면엔 똑같아 보이고 받아온 파일 크기만 달라요(Network 탭에서만 보임). 같은 "반응형 이미지"지만 <picture>는 보이고 srcset은 안 보인다는 게 둘의 결정적 차이예요.
채점 포인트
| 항목 | 확인 내용 |
|---|---|
| 측정 | 두 환경에서 받아진 해상도를 실제로 확인했는가 |
| sizes 연결 | sizes의 100vw/32rem이 선택에 작용한 걸 설명했는가 |
| 차이 정리 | srcset(안 보임)과 <picture>(보임)의 차이를 관찰대로 정리했는가 |
흔한 실수
- 캐시 때문에 안 바뀌는 걸로 오해 — 이미 받아둔 이미지가 캐시에 남아 새로 안 받을 때가 있어요.
Network탭의Disable cache(캐시 비활성화) 체크박스를 켜고 새로고침하면 매번 새로 받아 비교가 정확해져요. - 화면에 보이는 크기로 판단 —
srcset은 화면에 보이는 사진 크기로는 알 수 없어요. 반드시Network탭에서 "받아진 파일 이름"으로 확인해야 해요.
💭 [생각해볼 주제 예시답안]
1. 왜 데스크톱 퍼스트가 아니라 모바일 퍼스트를 권할까?
min-width(모바일 퍼스트)와 max-width(데스크톱 퍼스트)는 똑같은 결과를 만들 수 있어요. 그런데도 모바일 퍼스트를 권하는 데는 세 가지 이유가 있어요.
첫째, 코드가 단순해져요. 폰 레이아웃은 보통 한 줄로 쭉 쌓이는 가장 단순한 형태예요. 이걸 조건 없는 기본 규칙으로 두면 기본 CSS가 깔끔하고, 복잡한 다단 배치는 넓은 화면에만 min-width로 더하면 돼요. 반대로 데스크톱을 기본으로 두면 복잡한 레이아웃이 기본에 깔리고, 좁은 화면마다 그걸 일일이 덜어내야 해서 예외 처리가 많아져요.
둘째, 작은 화면이 항상 보장돼요. 모바일 퍼스트는 "최소한의 화면에서도 멀쩡히 보이는 것"을 기본으로 깔고, 여유가 생길 때마다 풍성하게 더하는 점진적 향상 방식이에요. 그래서 어떤 화면에서도 최소한은 동작해요.
셋째, 방문자의 다수가 폰이에요. 인스타그램 같은 서비스는 접속자의 대부분이 휴대폰이라, 그들이 보는 기본 화면을 먼저 챙기는 게 자연스럽죠.
🎯 면접관을 홀리는 핵심 멘트
"모바일 퍼스트는 가장 단순한 폰 레이아웃을 기본으로 깔고
min-width로 넓은 화면을 더해가는 방식이에요. 데스크톱 퍼스트는 복잡한 기본에서 예외를 덜어내야 해 코드가 지저분해지죠. 작은 화면을 항상 보장하는 점진적 향상이라는 점, 그리고 방문자의 다수가 모바일이라는 현실까지 더하면,min-width기반이 자연스러운 표준이 됩니다."
2. @media와 @container, 둘의 경계는 어디일까?
둘 다 "조건에 따라 CSS를 바꾼다"는 점은 같지만, 무엇을 기준으로 보는가가 달라요. @media는 화면(뷰포트) 전체의 크기를, @container는 그 요소가 담긴 부모 칸의 크기를 봐요.
기준은 이 질문 하나예요. "이 요소가 화면 전체를 따라야 하나, 자기가 담긴 칸을 따라야 하나?"
페이지 전체 골격(폰이면 1단, 데스크톱이면 3단 같은)은 화면 크기를 따라야 하니 @media가 맞아요. 화면이 바뀌면 페이지 전체가 같이 바뀌어야 하니까요.
반면 여러 곳에 재사용되는 작은 조각(카드 같은)은 @container가 맞아요. 똑같은 카드가 넓은 본문에도, 좁은 사이드바에도 들어갈 수 있는데, 화면 크기는 같아도 담긴 칸의 너비는 다르거든요. 카드가 "내가 지금 좁은 칸에 있으니 작게, 넓은 칸에 있으니 크게"를 스스로 판단하려면 부모 칸을 봐야 해요. 화면을 보는 @media로는 "넓은 본문의 카드"와 "좁은 사이드바의 카드"를 구별할 수 없죠.
🎯 면접관을 홀리는 핵심 멘트
"
@media는 화면 전체를,@container는 부모 칸을 봐요. 페이지 골격처럼 화면을 따라야 하는 건@media, 여러 곳에 재사용되는 카드처럼 '담긴 칸'에 맞춰야 하는 건@container죠. 똑같은 카드를 넓은 본문과 좁은 사이드바에 동시에 쓸 때, 화면 크기는 같아도 칸 크기가 다르니@media로는 구별이 안 돼요. 그래서 컴포넌트 단위 반응형엔@container가 필요합니다."
3. 이미지를 그냥 큰 것 하나만 쓰면 안 될까? 왜 srcset·<picture>까지 필요할까?
큰 이미지 하나를 width: 100%로 줄여 쓰면 화면엔 그럭저럭 맞게 보여요. 하지만 "보이는 것"과 "잘 보이고 빠른 것"은 달라요. 두 가지 비용이 숨어 있어요.
첫째, 데이터와 속도예요. 폰 화면은 400px 정도면 충분한데 800px짜리 큰 파일을 받으면, 보이지도 않는 픽셀을 위해 데이터를 두 배 넘게 쓰고 로딩도 느려져요. 모바일 데이터를 쓰는 사용자에겐 실제 비용이고, 느린 로딩은 이탈로 이어지죠. srcset은 화면에 맞는 작은 파일을 보내 이걸 아껴줘요.
둘째, 화면마다 더 잘 보여주는 거예요. 가로로 넓은 사진을 폰의 좁은 화면에 그대로 욱여넣으면 인물이 너무 작아지거나 핵심이 잘려요. 폰에선 세로로 꽉 찬 크롭이 더 보기 좋을 때가 많죠. <picture>는 화면별로 아예 다른 크롭을 보내 "각 화면에 가장 보기 좋은 그림"을 줄 수 있어요. 큰 이미지 하나로는 절대 못 하는 일이죠.
정리하면, srcset은 속도·데이터를 아끼고(같은 그림, 알맞은 해상도), <picture>는 화면마다 가장 보기 좋은 그림을 줘요(다른 크롭). "보이기만 하면 된다"를 넘어 "빠르고 잘 보이게"로 가는 도구들이에요.
🎯 면접관을 홀리는 핵심 멘트
"큰 이미지 하나를 줄여 쓰면 보이긴 하지만 두 가지를 잃어요. 폰이 안 보이는 픽셀까지 받느라 데이터·속도를 낭비하고, 좁은 화면엔 가로 사진이 어색하게 잘리죠.
srcset은 화면에 맞는 해상도를 골라 속도를 아끼고,<picture>는 화면별 다른 크롭으로 가장 보기 좋은 그림을 줘요. '보이기만 하면 된다'와 '빠르고 잘 보인다'의 차이입니다."