CSS 강좌 / 작은 학습 사이트를 반응형으로 꾸미는 종합 실습
이번 종합 실습에서는 헤더·소개 영역·강좌 카드·바닥글을 가진 작은 학습 사이트를 만듭니다. Flexbox로 헤더를 정리하고 Grid로 카드 열 수를 바꾸며, 공통 변수·유동 크기·초점 표시·움직임 감소를 하나의 화면에 적용합니다. index.html과 style.css 두 파일만으로 실행할 수 있고, 각 부분을 만든 뒤 실제 결과를 비교하는 순서로 진행합니다.
완성할 화면과 확인 기준 정하기
디자인하기 전에 내용과 동작을 정합니다. 작은 화면에서는 사이트 이름과 메뉴가 필요하면 줄을 바꾸고 카드는 한 열로 읽힙니다. 공간이 늘어나면 카드가 두 열·세 열이 됩니다. 콘텐츠 폭에는 최대값을 두어 큰 화면에서도 문장이 지나치게 길어지지 않게 합니다.
| 요구 사항 | 완료 기준 |
|---|---|
| 내용 구조 | 주 제목 하나, 소개 문장, 카드 세 개, 바닥글이 순서대로 읽힘 |
| 좁은 화면 | 전체 가로 스크롤 없이 메뉴와 카드를 읽을 수 있음 |
| 넓은 화면 | 본문의 최대 폭을 유지하며 카드가 여러 열로 배치됨 |
| 키보드 탐색 | 본문 바로가기와 메뉴·이동 링크에 초점 표시가 보임 |
| 내용 변경 | 긴 설명과 공백 없는 문자열을 넣어도 고정 높이 때문에 잘리지 않음 |
| 움직임 감소 | 부드러운 스크롤과 장식 전환 없이 필요한 이동이 가능함 |
이 예제는 정적 화면이며 카드 자체에는 링크가 없습니다. 소개·강좌 메뉴와 큰 이동 링크는 같은 문서의 구역으로 이동합니다. 실제 강좌 목록으로 확장하려면 카드 제목에 확인된 게시물 URL을 넣습니다. 이번 실습의 완료를 위해 사용하지 않는 폼이나 무한 애니메이션을 억지로 추가할 필요는 없습니다.
파일 준비와 HTML의 역할 나누기
빈 폴더에 index.html과 style.css를 같은 위치로 만듭니다. 파일을 만들 때 숨겨진 확장자 때문에 index.html.txt가 되지 않도록 확인하고 UTF-8로 저장하세요. index.html의 link 태그는 같은 폴더의 style.css를 가리킵니다. 별도 설치나 빌드는 필요하지 않습니다.
learning-site/ index.html style.css
먼저 아래 전체 HTML을 index.html에 저장합니다. CSS가 비어 있어도 제목과 링크·카드 내용은 순서대로 읽혀야 합니다. header는 사이트 머리 영역, nav는 탐색, main은 주 내용, footer는 바닥글입니다. 문서의 h1 아래에 학습 주제 h2와 카드 h3를 배치합니다. 이 HTML은 독립 파일의 예제이므로 워드프레스 본문에 그대로 붙이는 페이지 구조와 구분합니다.
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>작은 학습 사이트</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<a class="skip-link" href="#main">본문으로 바로가기</a>
<header class="site-header">
<div class="container header-inner">
<p class="site-name">배움 노트</p>
<nav aria-label="주 메뉴">
<a href="#intro">소개</a><a href="#lessons">강좌</a>
</nav>
</div>
</header>
<main id="main" tabindex="-1">
<section id="intro" class="hero" aria-labelledby="intro-title">
<div class="container">
<h1 id="intro-title">웹페이지 만들기를 차근차근 배워요</h1>
<p>HTML로 내용을 정리하고 CSS로 화면에 맞게 꾸밉니다.</p>
<a class="button" href="#lessons">강좌 살펴보기</a>
</div>
</section>
<section id="lessons" class="lessons container" aria-labelledby="lessons-title">
<h2 id="lessons-title">학습 주제</h2>
<ul class="card-grid" role="list">
<li><article class="card"><h3>HTML 구조</h3><p>제목과 문단으로 내용을 구성합니다.</p></article></li>
<li><article class="card"><h3>CSS 스타일</h3><p>색, 글꼴과 간격을 조절합니다.</p></article></li>
<li><article class="card"><h3>반응형 배치</h3><p>화면 폭에 맞춰 카드를 배치합니다.</p></article></li>
</ul>
</section>
</main>
<footer class="site-footer"><div class="container"><p>작은 학습 사이트 실습</p></div></footer>
</body>
</html>
본문 바로가기는 main의 id와 연결합니다. tabindex="-1"은 main을 일반적인 Tab 순서에 추가하지 않으면서 초점을 받을 수 있게 합니다. 메뉴나 바로가기를 눌러 해당 구역으로 이동한 뒤 키보드 탐색이 주 내용에서 이어지는지 확인합니다. 바로가기의 목적과 구성은 W3C 본문 바로가기 기법을 참고하세요.
공통 값과 콘텐츠 폭부터 작성하기
style.css에 다음 구역을 먼저 저장합니다. 색을 역할별 변수로 정하면 전체 강조색을 바꾸기 쉽습니다. border-box는 테두리와 안쪽 여백을 너비에 포함해 계산을 단순하게 합니다. 처음부터 모든 요소에 고정 크기를 주기보다 실제 내용이 늘어날 수 있는 기본 상태를 만듭니다. 공통 값을 변경하기 전에 CSS 변수의 선언·범위·상속을 구분하세요.
/* 공통 값과 기본 모양 */
:root {
--ink: #183044;
--accent: #166b92;
--surface: #eef6fa;
--line: #c9dbe4;
--focus: #164f73;
}
*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body { margin: 0; color: var(--ink); font-family: sans-serif; line-height: 1.6; }
a { color: var(--accent); text-underline-offset: .15em; }
:focus-visible { outline: 3px solid var(--focus); outline-offset: 3px; }
.container { width: min(100% - 2rem, 68rem); margin-inline: auto; }
.container, .card { overflow-wrap: anywhere; }
container의 너비는 “사용 가능한 폭에서 좌우 합계 2rem을 뺀 값”과 68rem 중 작은 값입니다. 좁은 화면에는 좌우 여백을, 넓은 화면에는 최대 폭을 제공합니다. margin-inline의 auto는 그 영역을 가운데 놓습니다. overflow-wrap: anywhere는 긴 공백 없는 주소나 식별자도 필요하면 줄을 바꿀 수 있게 합니다. 일반 문장 길이만 늘리는 실습과 긴 한 단어를 넣는 실습은 구분해야 합니다.
이 단계에서 새로고침하면 색·기본 글꼴·콘텐츠 폭은 적용되지만 헤더와 카드는 아직 세밀하게 배치되지 않습니다. 변화가 전혀 없으면 뒤 구역을 추가하기 전에 파일 경로를 확인하세요. 유동 크기와 최대 폭의 개념은 유동 너비와 글자 크기 강의와 연결됩니다.
헤더를 Flexbox로 배치하고 초점 경로 만들기
다음 구역을 style.css의 공통 규칙 아래에 추가합니다. 사이트 이름과 메뉴 사이의 배치는 header-inner가 관리하고, 메뉴 내부의 간격은 nav가 관리합니다. 두 위치의 gap을 분리하면 전체 헤더 간격을 바꿀 때 메뉴 링크 사이 간격까지 의도치 않게 바뀌는 일을 줄일 수 있습니다. 축의 방향과 아이템 배치의 기본 원리는 Flexbox의 주축과 기본 배치에서 확인할 수 있습니다.
/* 탐색과 헤더 */
.skip-link { position: absolute; left: 1rem; top: -6rem; padding: .5rem; background: white; }
.skip-link:focus { top: 1rem; z-index: 10; }
.site-header { border-bottom: 1px solid var(--line); }
.header-inner { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
gap: .5rem 1.5rem; padding-block: 1rem; }
.site-name { margin: 0; font-weight: 700; }
nav { display: flex; flex-wrap: wrap; gap: 1rem; }
flex-wrap은 공간이 부족하면 항목을 다음 줄에 놓습니다. 좁은 화면에서 메뉴가 두 줄이 되는 것은 실패가 아닙니다. 겹침·잘림·누를 수 없는 링크가 생기는지가 판단 기준입니다. 의미 있는 탐색 순서를 유지하려고 order로 화면 순서만 뒤집지 않습니다.
본문 바로가기는 평소 위쪽 화면 밖에 놓고 초점을 받을 때 보이게 합니다. display:none으로 숨기면 키보드 초점도 받을 수 없으므로 같은 목적을 달성하지 못합니다. 첫 Tab에서 바로가기가 나타나고 Enter로 이동한 뒤 다음 Tab이 소개의 큰 링크로 이어지는지 확인합니다. 진한 초점 윤곽선은 흰색과 소개 배경 양쪽에서 구분되도록 골랐습니다.
소개 영역에 유동 크기와 상태 적용하기
다음 구역을 추가하면 소개 배경과 제목, 큰 이동 링크가 완성됩니다. 제목 크기의 clamp는 최소값·유동값·최대값을 묶습니다. 폭이 늘어난다고 글자가 끝없이 커지지 않으며, rem 항을 포함해 글자 크기 설정도 고려합니다. 사용자가 확대했을 때 실제로 읽기 쉬운지는 별도 화면 확인이 필요합니다.
/* 소개와 이동 링크 */
.hero { padding-block: clamp(3rem, 8vw, 6rem); background: var(--surface); }
.hero h1 { max-width: 18ch; margin-block: 0 1rem;
font-size: clamp(1.8rem, 1.2rem + 2vw, 3rem); line-height: 1.25; }
.hero p { max-width: 50ch; }
.button { display: inline-block; margin-top: .5rem; padding: .7rem 1rem;
border-radius: .4rem; background: var(--accent); color: white; text-decoration: none;
transition: background-color 180ms ease; }
.button:hover { background: #104d6a; }
제목의 max-width 18ch는 사용 글꼴의 문자 폭을 기준으로 줄 길이를 제한합니다. 한글 18글자라는 뜻은 아닙니다. h1의 line-height는 제목 여러 줄의 간격을 조정하고, 소개 문단은 50ch로 긴 문장을 제한합니다. 이 예제는 시스템의 sans-serif 글꼴을 사용해 외부 글꼴 다운로드를 필요로 하지 않습니다.
큰 링크는 같은 문서의 lessons 구역으로 이동하므로 a를 사용합니다. 페이지 이동 없이 상태를 바꾸는 조작이었다면 button을 검토해야 합니다. 호버에는 배경색 전환만 사용하며 장식적인 이동이나 확대를 필수로 넣지 않습니다. 초점 상태는 공통 윤곽선으로 확인할 수 있습니다.
Grid로 카드 열 수와 내용 높이 맞추기
다음 구역을 추가하면 강좌 카드와 바닥글이 완성됩니다. 목록의 기본 여백과 글머리표는 없애되 HTML에는 ul과 li 구조를 남깁니다. list-style: none인 목록이 nav 밖에 있으면 Safari의 접근성 트리에서 목록으로 인식되지 않을 수 있습니다. 예제의 ul에는 role="list"를 지정해 목록 역할을 명시합니다. 이 조건은 MDN의 목록 접근성 설명에서도 확인할 수 있습니다. Grid의 행과 열 배치를 위해 HTML의 읽는 순서를 바꾸지는 않습니다.
/* 카드와 바닥글 */
.lessons { padding-block: 3rem; }
.lessons h2 { margin-block: 0 1.5rem; }
.card-grid { display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
gap: 1rem; margin: 0; padding: 0; list-style: none; }
.card-grid > li { min-width: 0; }
.card { height: 100%; padding: 1.5rem; border: 1px solid var(--line); border-radius: .5rem; }
.card h3 { margin-block: 0 .75rem; }
.card p { margin-block: 0; }
.site-footer { padding-block: 2rem; border-top: 1px solid var(--line); }
.site-footer p { margin: 0; }
auto-fit은 들어갈 수 있는 열을 만들고, minmax()는 각 열의 최소 크기와 늘어날 수 있는 크기를 정합니다. 최소값의 min(100%, 16rem)은 부모가 16rem보다 좁을 때 카드도 그 폭까지 줄어들게 합니다. 여유 공간은 1fr로 나누며 실제 카드가 세 개이므로 더 넓다고 네 번째 내용이 만들어지지 않습니다. MDN repeat 함수와 minmax 함수를 참고하세요.
최상위 글자 크기가 16px이고 사용 가능한 페이지 폭이 뷰포트와 같다는 조건에서는 카드 최소 너비가 256px, gap은 16px, 좌우 여백 합계는 32px입니다. 두 열에는 560px, 세 열에는 832px 이상의 공간이 필요합니다. 스크롤바·부모 폭·글자 크기가 달라지면 실제 전환 폭도 달라지므로 기기 이름을 기준으로 외우지 않습니다.
카드의 height 100%는 같은 행의 목록 항목 높이를 채우는 값입니다. 고정된 픽셀 높이가 아니므로 설명이 늘어나면 행도 커질 수 있습니다. 긴 단어가 Grid 항목의 크기를 밀어내지 않게 li의 min-width를 0으로 정하고, 카드의 줄바꿈 규칙을 함께 사용합니다.
넓은 화면과 움직임 감소 조건 더하기
아래 구역을 style.css 끝에 추가합니다. 64rem 이상에서 소개 배경만 그라데이션으로 바꾸고, 카드 열 수는 계속 Grid가 결정합니다. 이 미디어 쿼리가 카드 열 수를 바꾸는 규칙이라고 혼동하지 마세요.
/* 조건에 따른 조정 */
@media (min-width: 64rem) {
.hero { background: linear-gradient(100deg, var(--surface), #fff); }
}
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
.button { transition: none; }
}
움직임 감소 조건에서는 스크롤을 즉시 이동으로 바꾸고 배경색 전환도 없앱니다. 링크의 목적과 초점 표시·내용은 유지됩니다. 조건부 규칙의 적용은 미디어 쿼리 강의, 움직임과 접근성 점검은 모바일 화면 점검 강의와 연결됩니다.
완성된 style.css와 실행 방법
단계별로 추가한 규칙을 모두 합친 파일입니다. 코드의 순서를 점검하거나 새로 시작할 때 이 전체 파일을 사용하세요. index.html과 같은 폴더에 저장한 뒤 index.html을 브라우저로 열고 새로고침하면 됩니다.
/* 공통 값과 기본 모양 */
:root {
--ink: #183044;
--accent: #166b92;
--surface: #eef6fa;
--line: #c9dbe4;
--focus: #164f73;
}
*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body { margin: 0; color: var(--ink); font-family: sans-serif; line-height: 1.6; }
a { color: var(--accent); text-underline-offset: .15em; }
:focus-visible { outline: 3px solid var(--focus); outline-offset: 3px; }
.container { width: min(100% - 2rem, 68rem); margin-inline: auto; }
.container, .card { overflow-wrap: anywhere; }
/* 탐색과 헤더 */
.skip-link { position: absolute; left: 1rem; top: -6rem; padding: .5rem; background: white; }
.skip-link:focus { top: 1rem; z-index: 10; }
.site-header { border-bottom: 1px solid var(--line); }
.header-inner { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
gap: .5rem 1.5rem; padding-block: 1rem; }
.site-name { margin: 0; font-weight: 700; }
nav { display: flex; flex-wrap: wrap; gap: 1rem; }
/* 소개와 이동 링크 */
.hero { padding-block: clamp(3rem, 8vw, 6rem); background: var(--surface); }
.hero h1 { max-width: 18ch; margin-block: 0 1rem;
font-size: clamp(1.8rem, 1.2rem + 2vw, 3rem); line-height: 1.25; }
.hero p { max-width: 50ch; }
.button { display: inline-block; margin-top: .5rem; padding: .7rem 1rem;
border-radius: .4rem; background: var(--accent); color: white; text-decoration: none;
transition: background-color 180ms ease; }
.button:hover { background: #104d6a; }
/* 카드와 바닥글 */
.lessons { padding-block: 3rem; }
.lessons h2 { margin-block: 0 1.5rem; }
.card-grid { display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
gap: 1rem; margin: 0; padding: 0; list-style: none; }
.card-grid > li { min-width: 0; }
.card { height: 100%; padding: 1.5rem; border: 1px solid var(--line); border-radius: .5rem; }
.card h3 { margin-block: 0 .75rem; }
.card p { margin-block: 0; }
.site-footer { padding-block: 2rem; border-top: 1px solid var(--line); }
.site-footer p { margin: 0; }
/* 조건에 따른 조정 */
@media (min-width: 64rem) {
.hero { background: linear-gradient(100deg, var(--surface), #fff); }
}
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
.button { transition: none; }
}
값을 바꿔 비교하고 문제를 해결하기
- 원래 값을 유지한 채 화면 폭을 390px 정도로 줄였다가 넓힙니다. 작은 폭에서는 한 열, 넓은 폭에서는 두 열·세 열이 되어야 합니다. 앞서 설명한 조건이 같다면 559px와 560px, 831px와 832px의 차이도 비교합니다.
- card-grid의 16rem만 20rem으로 바꿉니다. 같은 화면에서 열을 추가하려면 더 넓은 공간이 필요해집니다. 확인 후 16rem으로 복원합니다.
- container의 68rem만 50rem으로 바꿉니다. 큰 화면에서도 전체 내용 폭이 더 좁아집니다. 확인 후 68rem으로 복원합니다.
- accent만 다른 충분히 진한 색으로 바꿉니다. 링크와 버튼 배경이 함께 바뀌는지 보고 원래 색으로 복원합니다. 새 배경에서 흰 버튼 글자와 초점 표시도 읽혀야 합니다.
- 카드 설명 하나를 여러 문장으로 늘립니다. 높이가 늘어나도 내용이 잘리지 않고 같은 행의 카드 높이가 정리되는지 확인합니다. 실습 후 원래 문장을 복원합니다.
- 공백 없는 영문 문자열을 카드에 넣습니다. anywhere를 유지하면 내부에서 줄이 바뀝니다. 해당 줄바꿈 규칙만 잠시 제거하면 넘침이 나타날 수 있습니다. 비교 후 규칙과 문장을 모두 복원합니다.
- Tab과 Shift+Tab으로 이동하고 각 링크에 Enter를 누릅니다. 마우스 없이도 소개와 학습 주제로 이동할 수 있어야 합니다.
- 움직임 감소 조건을 재현합니다. 링크 이동은 유지되고 부드러운 스크롤·전환은 사라져야 합니다. 확인 후 재현 설정을 기본으로 복원합니다.
브라우저 확대와 글자 크기 설정에서도 메뉴·카드·초점·문단이 겹치거나 잘리지 않는지 확인합니다. 개발자 도구에서 html에 큰 font-size를 임시 적용하는 비교는 rem 크기의 영향을 확인하는 방법이지만 브라우저 확대와 같은 검사는 아닙니다. 한 검사 결과만으로 모든 확대 환경을 통과했다고 판단하지 않습니다.
| 문제 | 먼저 확인할 것 |
|---|---|
| 스타일이 전혀 없음 | 파일 확장자·저장 여부·link의 상대 경로 |
| 가로 스크롤 발생 | 긴 단어, 최소 열 크기, 부모의 실제 폭, 누락된 border-box |
| 카드 높이가 들쭉날쭉함 | 같은 행인지와 card의 height 설정; 다른 행끼리 같을 필요는 없음 |
| 초점 표시가 안 보임 | outline을 덮어쓴 규칙, overflow로 윤곽선이 잘리는지 |
| 원치 않는 이동 효과 | 실제 미디어 조건과 적용된 scroll-behavior·transition 값 |
개발자 도구에서 문제 요소를 선택해 실제 계산된 너비·선택자·Grid 열을 확인하세요. 줄바꿈을 강제로 모두 막거나 body에 overflow:hidden을 넣어 문제를 가리는 대신, 넘친 요소의 크기와 내용부터 고칩니다. 요구 사항별로 기본 상태·변경 상태·복원 상태를 확인하면 완성 코드를 복사하는 데서 한 단계 더 나아가 다른 내용에도 적용할 수 있습니다.










