CSS 열두 구역은 할 일 화면의 style.css를 구역 주석 열둘로 나눠, 공통 규칙을 먼저 쓰고 영역마다의 규칙을 화면 차례로 뒤에 쓰는 일입니다. 240줄 가까운 CSS를 한 덩어리로 쓰면 어느 줄이 어느 영역을 꾸미는지 찾기 어렵고, 명시도가 같은 규칙끼리는 쓴 차례가 이김을 가릅니다. 구역마다 10~16단원에서 배운 레슨을 하나씩 꺼내 씁니다.
이 레슨은 60분 안팎입니다. 구역 하나를 쓸 때마다 저장하고 새로고침해 그 구역의 확인 한 줄을 맞춰 봅니다. 구역마다 멈췄다가 이어 써도 됩니다. 폰으로만 보고 있다면 구역 표와 예제를 읽으며 확인 문제로 따라옵니다.
앞 레슨에서 완성한 index.html
<!-- index.html -->
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width"
/>
<title>오늘 할 일</title>
<link
rel="stylesheet"
href="style.css"
/>
</head>
<body>
<main class="app">
<h1>오늘 할 일</h1>
<p id="quote" class="quote">
오늘도 한 걸음씩.
</p>
<form id="todo-form">
<label
class="sr-only"
for="todo-input"
>
할 일
</label>
<input
id="todo-input"
type="text"
placeholder="예: 장보기"
autocomplete="off"
maxlength="100"
/>
<button type="submit">
추가
</button>
</form>
<div
id="filters"
class="filters"
>
<button
type="button"
data-filter="all"
class="active"
>
전체
</button>
<button
type="button"
data-filter="active"
>
진행 중
</button>
<button
type="button"
data-filter="done"
>
완료
</button>
</div>
<ul id="todo-list">
<li>
<input
type="checkbox"
aria-label="완료 표시"
/>
<span class="text">
우유 사기
</span>
<button
type="button"
class="delete"
aria-label="삭제"
>
×
</button>
</li>
<li class="done">
<input
type="checkbox"
aria-label="완료 표시"
checked
/>
<span class="text">
HTML·CSS 복습하기
</span>
<button
type="button"
class="delete"
aria-label="삭제"
>
×
</button>
</li>
<li>
<input
type="checkbox"
aria-label="완료 표시"
/>
<span class="text">
산책하기
</span>
<button
type="button"
class="delete"
aria-label="삭제"
>
×
</button>
</li>
</ul>
<p
id="empty"
class="empty"
hidden
>
할 일이 없습니다.
</p>
<footer class="footer">
<p
id="count"
class="count"
>
전체 3개 중 2개 남음
</p>
<button
type="button"
id="clear-done"
class="clear"
>
완료한 일 지우기
</button>
</footer>
</main>
</body>
</html>그림 준비 중
1구역 차례 — 공통 먼저, 상태는 맨 끝
구역은 열둘이고 이 차례로 씁니다. 1~3(리셋 · 페이지 · 카드)은 화면 전체에 닿는 공통 규칙이고, 4~10은 한마디부터 아랫줄까지 영역마다 화면 위에서 아래 차례입니다. 11은 숨긴 라벨 .sr-only, 12는 마우스를 올리거나 포커스가 갔을 때의 상태입니다.
이 차례의 까닭은 둘입니다. 하나는 이김입니다. 명시도가 같은 선언끼리는 나중에 쓴 쪽이 이겨(브라우저 규칙 5), 공통 규칙을 위에 두면 뒤의 구역 규칙이 그 값을 이길 수 있습니다. 상태 구역을 맨 끝에 두는 것도 같은 까닭입니다 — 기본 모양과 명시도가 같은 상태 규칙을 쓰더라도 상태 쪽이 이깁니다. 또 하나는 찾기입니다. 화면의 한 곳이 이상하면 그 영역의 구역 주석부터 봅니다.
2구역 열둘 — 확인 한 줄과 배운 곳
구역 코드는 예제의 style.css 전체에서 구역 주석(/* 7. 목록 줄 */) 하나씩 따라 칩니다. 한 구역을 치면 확인 한 줄을 봅니다.
| 구역 | 다 되면 보이는 것 | 배운 곳 |
|---|---|---|
| 1. 리셋 | Computed 창의 box-sizing이 border-box | 너비와 box-sizing |
| 2. 페이지 | 바탕이 연회색, 어두운 화면에서는 짙은 남색 | CSS 변수 · 변수만 바꾸는 다크 모드 |
| 3. 카드 | 흰 카드가 가운데, 넓은 창에서 너비 520px | 가운데 모으기 · 반투명한 색 |
| 4. 한마디 | 한마디가 옅은 띠 안에 작은 글자로 | 여백 짧게 적기 · 글자 크기는 rem |
| 5. 폼 | 입력 칸이 늘어나 '추가'와 한 줄 | gap과 flex: 1 |
| 6. 필터 | 단추 셋이 한 줄, '전체'만 짙게 | 조건 겹치기와 속성 선택자 |
| 7. 목록 줄 | 줄마다 셋이 한 줄, ×는 오른쪽 끝 | 교차축 정렬 |
| 8. 끝낸 상태 | 둘째 줄 글이 흐리고 취소선 | 하위·자식·형제 선택자 |
| 9. 빈 안내 | 화면에 여전히 없음 | id와 class |
| 10. 아랫줄 | 개수는 왼쪽 끝, 지우기 단추는 오른쪽 끝 | 주축 정렬 |
11. .sr-only | 라벨 '할 일'이 사라지고 칸의 읽는 이름은 그대로 | .sr-only 한 줄씩 |
| 12. 상태 | 마우스를 올린 단추의 색이 바뀜 · Tab으로 간 곳에 윤곽선 | 마우스와 키보드 상태 |
-
- 빈 안내 구역에는
display를 적지 않습니다.hidden으로 숨긴 요소에display를 주면 다시 보입니다(아래 자주 틀리는 곳).
- 빈 안내 구역에는
-
.sr-only는top·left를 적지 않아 기준 상자를 찾지 않고 원래 자리에 1 × 1로 남습니다.
3목록 줄 — 한 줄을 flex로
- 목록 줄 구역은 10 · 12단원이 가장 많이 모인 곳입니다. 목록의 기본 점과 여백을 지우고, 줄 하나를 flex로 놓습니다.
<ul id="todo-list">
<li>
<input
type="checkbox"
aria-label="완료 표시"
/>
<span class="text">
우유 사기
</span>이어서 × 단추로 줄을 닫고 CSS를 붙입니다.
<button
type="button"
class="delete"
aria-label="삭제"
>
×
</button>
</li>
</ul>
#todo-list {
margin: 0;
padding: 0;
list-style: none;
}
#todo-list li {
display: flex;
align-items: center;
gap: 10px;
}
.text {
flex: 1;
}#todo-list의list-style-typenone#todo-list li의displayflex.text의flex-grow1
display: flex는 셋을 나란히 놓을 부모 li에 줍니다. 셋을 바로 담은 것은 ul이 아니라 li입니다(flex는 부모에). 글 .text가 flex: 1으로 남는 자리를 차지해 × 단추가 줄 오른쪽 끝으로 갑니다. 이 display: flex 한 줄이 빠지면 단추가 글 바로 뒤에 붙고, 체크박스와 글의 세로 가운데도 어긋납니다.
예제
완성한 style.css
구역 열둘을 다 쓴 style.css와, 앞 레슨의 index.html에 입혀 그린 결과 화면입니다.
index.html · style.css 전체와 결과 화면
<!-- index.html -->
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width"
/>
<title>오늘 할 일</title>
<link
rel="stylesheet"
href="style.css"
/>
</head>
<body>
<main class="app">
<h1>오늘 할 일</h1>
<p id="quote" class="quote">
오늘도 한 걸음씩.
</p>
<form id="todo-form">
<label
class="sr-only"
for="todo-input"
>
할 일
</label>
<input
id="todo-input"
type="text"
placeholder="예: 장보기"
autocomplete="off"
maxlength="100"
/>
<button type="submit">
추가
</button>
</form>
<div
id="filters"
class="filters"
>
<button
type="button"
data-filter="all"
class="active"
>
전체
</button>
<button
type="button"
data-filter="active"
>
진행 중
</button>
<button
type="button"
data-filter="done"
>
완료
</button>
</div>
<ul id="todo-list">
<li>
<input
type="checkbox"
aria-label="완료 표시"
/>
<span class="text">
우유 사기
</span>
<button
type="button"
class="delete"
aria-label="삭제"
>
×
</button>
</li>
<li class="done">
<input
type="checkbox"
aria-label="완료 표시"
checked
/>
<span class="text">
HTML·CSS 복습하기
</span>
<button
type="button"
class="delete"
aria-label="삭제"
>
×
</button>
</li>
<li>
<input
type="checkbox"
aria-label="완료 표시"
/>
<span class="text">
산책하기
</span>
<button
type="button"
class="delete"
aria-label="삭제"
>
×
</button>
</li>
</ul>
<p
id="empty"
class="empty"
hidden
>
할 일이 없습니다.
</p>
<footer class="footer">
<p
id="count"
class="count"
>
전체 3개 중 2개 남음
</p>
<button
type="button"
id="clear-done"
class="clear"
>
완료한 일 지우기
</button>
</footer>
</main>
</body>
</html>
/* style.css */
/* 1. 리셋 */
* {
box-sizing: border-box;
}
/* 2. 페이지 */
:root {
--bg: #f5f5f7;
--surface: #ffffff;
--text: #1d1d1f;
--muted: #6e6e73;
--line: #d2d2d7;
--accent: #005bb5;
--danger: #d70015;
color-scheme: light dark;
}
@media (
prefers-color-scheme: dark
) {
:root {
--bg: #121820;
--surface: #1d2633;
--text: #f1f4f8;
--muted: #a3adb8;
--line: #3a4552;
--accent: #9ac7ff;
--danger: #ff6b6b;
}
}
body {
margin: 0;
padding: 40px 16px;
background: var(--bg);
color: var(--text);
font-family:
system-ui,
sans-serif;
line-height: 1.6;
}
/* 3. 카드 */
.app {
max-width: 520px;
margin: 0 auto;
padding: 24px;
background: var(--surface);
border-radius: 12px;
box-shadow:
0 1px 3px rgb(0 0 0 / 0.1);
}
h1 {
margin: 0 0 8px;
font-size: 1.5rem;
}
/* 4. 한마디 */
.quote {
margin: 0 0 20px;
padding: 12px;
background: var(--bg);
border-radius: 8px;
color: var(--muted);
font-size: 0.875rem;
}
/* 5. 폼 */
#todo-form {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
#todo-input {
flex: 1;
padding: 10px 12px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--surface);
color: var(--text);
font-size: 0.9375rem;
font-family: inherit;
}
button {
padding: 10px 16px;
border: none;
border-radius: 8px;
background: var(--accent);
color: var(--surface);
font-size: 0.9375rem;
font-family: inherit;
cursor: pointer;
}
/* 6. 필터 */
.filters {
display: flex;
gap: 6px;
margin-bottom: 16px;
}
.filters button {
padding: 6px 12px;
background: var(--bg);
color: var(--text);
font-size: 0.875rem;
}
.filters button.active {
background: var(--text);
color: var(--surface);
}
/* 7. 목록 줄 */
#todo-list {
margin: 0;
padding: 0;
list-style: none;
}
#todo-list li {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 4px;
border-bottom:
1px solid var(--line);
}
#todo-list li:last-child {
border-bottom: none;
}
#todo-list input {
width: 18px;
height: 18px;
cursor: pointer;
}
.text {
flex: 1;
cursor: pointer;
overflow-wrap: anywhere;
}
.delete {
padding: 4px 10px;
background: transparent;
color: var(--muted);
font-size: 1.125rem;
line-height: 1;
}
/* 8. 끝낸 상태 */
.done .text {
color: var(--muted);
text-decoration: line-through;
}
/* 9. 빈 안내 */
.empty {
padding: 32px 0;
color: var(--muted);
text-align: center;
}
/* 10. 아랫줄 */
.footer {
display: flex;
align-items: center;
justify-content:
space-between;
margin-top: 16px;
padding-top: 16px;
border-top:
1px solid var(--line);
}
.count {
margin: 0;
color: var(--muted);
font-size: 0.875rem;
}
.clear {
padding: 6px 12px;
background: transparent;
color: var(--muted);
font-size: 0.875rem;
}
/* 11. .sr-only */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
/* 12. 상태 */
button:hover {
background: var(--text);
color: var(--surface);
}
.filters button:hover {
background: var(--line);
color: var(--text);
}
.filters button.active:hover {
background: var(--text);
color: var(--surface);
}
.delete:hover {
background: var(--bg);
color: var(--danger);
}
.clear:hover {
background: var(--bg);
color: var(--text);
}
#todo-input:focus,
button:focus-visible,
#todo-list input:focus-visible {
outline:
2px solid var(--accent);
outline-offset: 1px;
}.app의max-width520px#todo-list li의displayflex.done .text의text-decoration-lineline-through#empty의displaynonelabel의 너비1px
결과 화면은 밝은 화면입니다. 어두운 화면과 넓은 창은 다음 레슨에서 점검 목록으로 봅니다.
자주 틀리는 곳
틀린 생각: 공통 규칙을 맨 아래에 두면 앞의 구역 규칙을 모두 이긴다.
<button
type="button"
class="delete"
>
×
</button>
.delete {
color: #6e6e73;
}
button {
color: #ffffff;
}.delete의 글자색rgb(110, 110, 115)
뒤에 둔 button이 이기지 못했습니다. .delete(0-1-0)가 button(0-0-1)보다 명시도가 높아 차례를 보지 않습니다(뒤가 늘 이긴다는 생각).
맞는 규칙: 차례는 명시도가 같을 때만 이김을 가릅니다(브라우저 규칙 5). 공통 규칙을 위에 두는 것은 같은 명시도의 구역 규칙이 뒤에서 이기게 하려는 것이고, 명시도가 다르면 자리를 옮겨도 이김이 바뀌지 않습니다.
틀린 생각: 빈 안내도 다른 구역처럼 display를 정해 꾸민다.
<p
class="empty"
hidden
>
할 일이 없습니다.
</p>
.empty {
display: block;
padding: 32px 0;
}.empty의displayblock.empty의 보이는 글할 일이 없습니다.
hidden을 붙였는데 '할 일이 없습니다.'가 보입니다. hidden은 기본 스타일의 display: none이라, 우리 CSS의 display: block에 집니다.
맞는 규칙: hidden으로 감출 요소에는 display를 정하는 규칙이 닿지 않게 합니다. 9. 빈 안내 구역은 여백 · 색 · 가운데 맞춤만 적습니다.
틀린 생각: 구역 주석은 꾸밈이라 빼고 한 덩어리로 써도 된다.
화면은 같게 그려집니다. 하지만 아랫줄 단추가 이상할 때 240줄 가운데 .clear 규칙이 어디 있는지, 그 앞뒤에 같은 단추를 고르는 규칙이 또 있는지 처음부터 훑어야 합니다.
맞는 규칙: 구역마다 번호를 단 주석 한 줄(/* 10. 아랫줄 */)을 둡니다. 이상한 영역의 번호로 구역을 찾고, 그 구역부터 찾는 차례대로 봅니다.
확인 문제
1. 10. 아랫줄 구역의 .footer에서 justify-content 줄만 지웠습니다. '완료한 일 지우기' 단추는 어디에 놓일까요?
- ① 오른쪽 끝 그대로
- ② 개수 글 바로 옆
- ③ 개수 글 아래 줄
힌트
남은 두 선언 가운데 놓는 방식을 정하는 것을 보고, 그 방식의 주축 처음 값을 떠올리세요.
정답
② — 주축의 처음 정렬은 시작 쪽이라, 단추가 개수 글 바로 옆에 붙습니다.
<footer class="footer">
<p id="count">
전체 3개 중 2개 남음
</p>
<button
type="button"
class="clear"
>
완료한 일 지우기
</button>
</footer>
.footer {
display: flex;
align-items: center;
}.footer의justify-contentnormal.footer의displayflex
display: flex가 남아 둘은 여전히 한 줄입니다. 양 끝으로 벌리던space-between이 사라져 남는 자리가 모두 단추 뒤로 갑니다.- ③은 flex까지 사라졌다고 본 것입니다. 지운 것은 정렬 한 줄입니다.
2. 구역 차례를 다시 짚습니다.
(가) 다섯 구역이 섞였습니다. style.css에서 위에서부터의 차례로 기호를 늘어놓으세요.
- ㉠ 상태
- ㉡ 목록 줄
- ㉢ 리셋
- ㉣ 아랫줄
- ㉤ 카드
(나) 목록 줄 구역에서 체크박스 · 글 · × 단추를 한 줄로 놓는 선언을 비웠습니다. ____ 자리에 쓰세요.
#todo-list li {
____;
align-items: center;
gap: 10px;
}
힌트
(가)는 1절의 세 묶음(공통 · 영역 · 끝)을 떠올립니다. (나)는 셋을 바로 담은 부모에 줄 놓는 방식입니다.
정답
(가) ㉢ ㉤ ㉡ ㉣ ㉠ — 리셋 · 카드 · 목록 줄 · 아랫줄 · 상태입니다. 공통 규칙이 먼저, 영역은 화면 위에서 아래로, 상태가 맨 끝입니다.
(나) display: flex
- 흔한 오답은
#todo-list(ul)에display: flex를 주는 것입니다. 그러면 줄들이 옆으로 늘어서고, 줄 안의 셋은 그대로입니다.
3. 여행 일정에서 줄마다 시간과 할 일을 나란히 두려 했는데, 일정 두 줄이 옆으로 늘어서고 줄 안의 시간과 할 일은 위아래로 놓였습니다. 무엇을 고칠까요? 고친 규칙을 쓰세요.
<ul class="schedule">
<li>
<p>09:00</p>
<p>기차 타기</p>
</li>
<li>
<p>13:00</p>
<p>숙소 도착</p>
</li>
</ul>
.schedule {
display: flex;
gap: 8px;
}.schedule의 줄 수1li의 줄 수2
힌트
나무를 그려 display: flex가 걸린 요소에 표시하고, 시간과 할 일이 무엇 안에 드는지 적어 보세요.
힌트 둘
display: flex는 직계 자식만 나란히 놓습니다. 지금 나란히 놓인 직계 자식이 무엇인지 보세요.
정답
선택자를 .schedule에서 .schedule li로 바꿉니다.
<ul class="schedule">
<li>
<p>09:00</p>
<p>기차 타기</p>
</li>
<li>
<p>13:00</p>
<p>숙소 도착</p>
</li>
</ul>
.schedule li {
display: flex;
gap: 8px;
}.schedule의 줄 수2li의 줄 수1
- 시간과 할 일을 바로 담은 부모는
li입니다.ul에 준 flex는 직계 자식li둘을 옆으로 놓았고, 손자p에는 닿지 않았습니다(브라우저 규칙 7 · flex가 후손까지라는 생각). - 할 일 화면의 목록 줄도 같은 꼴입니다 —
#todo-list li에 flex를 줍니다.
컴퓨터에서 해 보기(선택) — 점수에 들지 않습니다. 폰으로만 보고 있다면 건너뛰세요.
다 쓴 style.css에서 1. 리셋 구역 세 줄을 지우면 넓은 창에서 카드 너비가 어떻게 될지 먼저 적고, 저장 · 새로고침한 뒤 Computed 창의 너비로 맞춰 봅니다. 확인했으면 되돌립니다.