본문으로 건너뛰기

CSS 열두 구역

HTML·CSS 목차할 일 목록 화면 만들기레슨 92 / 93

목차

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으로 간 곳에 윤곽선마우스와 키보드 상태
    1. 빈 안내 구역에는 display를 적지 않습니다. hidden으로 숨긴 요소에 display를 주면 다시 보입니다(아래 자주 틀리는 곳).
    1. .sr-only는 top · left를 적지 않아 기준 상자를 찾지 않고 원래 자리에 1 × 1로 남습니다.

3목록 줄 — 한 줄을 flex로

  1. 목록 줄 구역은 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의 displaynone
  • label의 너비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의 줄 수1
  • li의 줄 수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의 줄 수2
  • li의 줄 수1
  • 시간과 할 일을 바로 담은 부모는 li입니다. ul에 준 flex는 직계 자식 li 둘을 옆으로 놓았고, 손자 p에는 닿지 않았습니다(브라우저 규칙 7 · flex가 후손까지라는 생각).
  • 할 일 화면의 목록 줄도 같은 꼴입니다 — #todo-list li에 flex를 줍니다.

컴퓨터에서 해 보기(선택) — 점수에 들지 않습니다. 폰으로만 보고 있다면 건너뛰세요. 다 쓴 style.css에서 1. 리셋 구역 세 줄을 지우면 넓은 창에서 카드 너비가 어떻게 될지 먼저 적고, 저장 · 새로고침한 뒤 Computed 창의 너비로 맞춰 봅니다. 확인했으면 되돌립니다.

끝까지 내려오면 저절로 표시됩니다. 이 기기에만 저장합니다.

HTML·CSS 목차로