Sass로 반응형 블로그 헤더 만들기
Sass 변수·믹스인·부분 파일·BEM을 조합해 반응형 블로그 헤더를 구현하고 컴파일 결과를 확인합니다.
실습: Sass로 반응형 블로그 헤더 만들기
이번 장에서 배운 Sass module, 변수, 믹스인과 BEM을 조합해 반응형 헤더 컴포넌트를 만들어 봅시다. 이 실습은 JavaScript 상호작용 없이, 화면 폭에 따라 메뉴 배치가 바뀌는 CSS 레이아웃에 집중합니다.
앞 절의 Node.js/npm 설치 확인을 마친 뒤 시작합니다. web-dev-practice가 이미 있다면 그 폴더로 이동해 기존 파일을 보존하고 아래 새 파일만 추가하세요. 빈 실습 폴더에서 시작할 때는 다음 명령으로 만들고 Sass를 프로젝트의 개발 의존성으로 설치합니다. 이후 npx sass는 이 로컬 버전을 사용하며, 생성된 lockfile이 실습 환경의 버전을 고정합니다.
mkdir web-dev-practice
cd web-dev-practice
npm init -y
npm install --save-dev sasssass_header.html 파일 작성<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sass 반응형 헤더 실습</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<header class="main-header">
<a href="#" class="main-header__logo">My Blog</a>
<nav class="main-nav" aria-label="주요 메뉴">
<ul class="main-nav__list">
<li class="main-nav__item"><a href="#" class="main-nav__link">홈</a></li>
<li class="main-nav__item"><a href="#" class="main-nav__link">카테고리</a></li>
<li class="main-nav__item"><a href="#" class="main-nav__link">글쓰기</a></li>
<li class="main-nav__item"><a href="#" class="main-nav__link">마이페이지</a></li>
</ul>
</nav>
</header>
<main class="content-wrapper">
<h1 class="page-title">반응형 웹과 Sass의 조화</h1>
<p class="content-copy">이 페이지는 Sass를 이용하여 헤더를 만들었습니다. 화면 크기를 줄여보세요!</p>
<p class="content-copy">변수, 얕은 중첩, 믹스인과 module로 역할과 의존성을 분명하게 관리할 수 있습니다.</p>
<div class="content-demo">
페이지 스크롤 영역
</div>
</main>
<footer class="site-footer">
<p>© 2026 Sass Practice.</p>
</footer>
</body>
</html>scss/style.scss 파일 작성 (entry 파일)// CSS를 출력하는 module만 순서대로 불러옵니다.
@use 'base';
@use 'layout';
@use 'header';@use로 불러온 변수와 믹스인은 현재 stylesheet 안에서만 보입니다. 따라서 style.scss가 모든 의존성을 전역으로 준비하는 대신, 각 partial이 자신에게 필요한 module을 직접 @use합니다.
SASS · BUILD TIME
Sass module은 사용하는 파일에서 직접 @use한다
style.scss의 @use가 변수와 mixin을 모든 partial에 전역으로 공급하지는 않는다. 각 stylesheet는 자신이 사용하는 module을 직접 불러온다.
공유 의존성이 많아도 module CSS를 네 번 출력하지 않는다
@use는 같은 compilation에서 module CSS를 한 번 포함한다. 그림의 화살표는 직접 @use 관계이며, entry의 namespace가 다른 stylesheet에 자동으로 전파되지는 않는다.
A → B는 A가 B를 직접 @use한다는 뜻이다. Dart Sass는 entry인 style.scss를 css/style.css로 컴파일한다.
scss/_variables.scss 파일 작성// _variables.scss
$primary-color: #3498db;
$text-color: #333;
$bg-color: #f0f2f5;
$surface-color: white;
$header-bg: #2c3e50;
$nav-link-color: white;
$nav-link-hover-color: #1abc9c; // 터쿼이즈
$mobile-breakpoint: 768px; // 767px 모바일 상한을 계산하는 기준값scss/_mixins.scss 파일 작성// _mixins.scss
@use 'variables' as vars;
$mobile-max:
vars.$mobile-breakpoint - 1px;
@mixin mobile-only {
@media screen and
(max-width: $mobile-max) {
@content;
}
}미디어 쿼리의 feature query에서는 Sass 변수와 연산식을 직접 사용할 수 있습니다. 여기서는 768px - 1px이 컴파일 시 767px이 되므로 별도의 인터폴레이션이 필요하지 않습니다.
아래 다이어그램은 Sass 믹스인이 @content와 함께 반응형 CSS로 펼쳐지는 흐름을 정리한 것입니다.
@content가 반응형 CSS로 컴파일되는 과정 — 상태 추적
본문 768px−1px과 @content의 header column 블록이 767px media CSS로 합쳐진다. 정의/호출/산출물/브라우저 조건의 현재 값을 고정한 컴파일 추적이 유용하다.
SASS · BUILD TIME
@content는 호출 블록을 CSS 출력에 삽입한다
예제 _variables.scss의 $mobile-breakpoint는 768px이다. 1px을 뺀 767px media 조건 안에 @include가 전달한 헤더 규칙을 컴파일한다.
-
정의 ·
_mixins.scss@use 'variables' as vars; $mobile-max: vars.$mobile-breakpoint - 1px; @mixin mobile-only { @media screen and (max-width: $mobile-max) { @content; } }공통 미디어 조건 안에 호출 블록을 받을
@contentinsertion point를 둔다. -
호출 ·
_header.scss핵심 발췌@use 'mixins' as mix; @include 호출 블록 { .main-header { flex-direction: column; } }@include에 전달한 같은 요소의 선언이 mixin의@content위치에 들어간다. -
출력 ·
css/style.css핵심 발췌@media screen and (max-width: 767px) { .main-header { flex-direction: column; } }변수, 연산, mixin 호출은 사라지고 브라우저가 해석할 표준 CSS만 남는다.
믹스인 호출은 사라지고 media 조건은 CSS에 남는다
컴파일 출력에는 .main-header의 flex-direction: column과 767px media query가 남는다. 브라우저가 그 조건을 평가하며, JavaScript 호출이나 runtime callback은 실행하지 않는다.
@content는 runtime callback이 아니라 호출 블록을 컴파일 시 삽입하는 자리다. 미디어 feature query는 SassScript 변수와 연산식을 직접 받으므로 이 예제에는 인터폴레이션이 필요하지 않다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 원도표 설명 | @include에 전달한 같은 요소의 선언이 mixin의 @content 위치에 들어간다. |
| 원도표 조건·주석 | 호출 · _header.scss 핵심 발췌 @use 'mixins' as mix; @include 호출 블록 { .main-header { flex-direction: column; } } @include에 전달한 같은 요소의 선언이 mixin의 @content 위치에 들어간다. |
| 원도표 설명 | @content는 runtime callback이 아니라 호출 블록을 컴파일 시 삽입하는 자리다. 미디어 feature query는 SassScript 변수와 연산식을 직접 받으므로 이 예제에는 인터폴레이션이 필요하지 않다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| mixin 정의 | Sass: 정의 상태 / CSS: 아직 없음 | @content를 받을 규칙 틀을 만든다 | 정의 자체가 브라우저 적용 규칙은 아니다 |
| include와 컴파일 | 경계: 768−1=767px / CSS: media 규칙 산출 | 호출 내용의 header column 규칙이 767px media 안에 놓인다 | Sass 계산과 browser query 판정은 다르다 |
| 브라우저 조건 | viewport: 조건 만족 여부 / header: 조건에 따른 배치 | 산출 CSS가 실제 viewport 조건에서만 적용된다 | include 호출 시점을 화면 resize 사건으로 해석하지 않는다 |
@content가 반응형 CSS로 컴파일되는 과정 — 상태 추적
본문 768px−1px과 @content의 header column 블록이 767px media CSS로 합쳐진다. 정의/호출/산출물/브라우저 조건의 현재 값을 고정한 컴파일 추적이 유용하다.
SASS · BUILD TIME
@content는 호출 블록을 CSS 출력에 삽입한다
예제 _variables.scss의 $mobile-breakpoint는 768px이다. 1px을 뺀 767px media 조건 안에 @include가 전달한 헤더 규칙을 컴파일한다.
-
정의 ·
_mixins.scss@use 'variables' as vars; $mobile-max: vars.$mobile-breakpoint - 1px; @mixin mobile-only { @media screen and (max-width: $mobile-max) { @content; } }공통 미디어 조건 안에 호출 블록을 받을
@contentinsertion point를 둔다. -
호출 ·
_header.scss핵심 발췌@use 'mixins' as mix; @include 호출 블록 { .main-header { flex-direction: column; } }@include에 전달한 같은 요소의 선언이 mixin의@content위치에 들어간다. -
출력 ·
css/style.css핵심 발췌@media screen and (max-width: 767px) { .main-header { flex-direction: column; } }변수, 연산, mixin 호출은 사라지고 브라우저가 해석할 표준 CSS만 남는다.
믹스인 호출은 사라지고 media 조건은 CSS에 남는다
컴파일 출력에는 .main-header의 flex-direction: column과 767px media query가 남는다. 브라우저가 그 조건을 평가하며, JavaScript 호출이나 runtime callback은 실행하지 않는다.
@content는 runtime callback이 아니라 호출 블록을 컴파일 시 삽입하는 자리다. 미디어 feature query는 SassScript 변수와 연산식을 직접 받으므로 이 예제에는 인터폴레이션이 필요하지 않다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 원도표 설명 | @include에 전달한 같은 요소의 선언이 mixin의 @content 위치에 들어간다. |
| 원도표 조건·주석 | 호출 · _header.scss 핵심 발췌 @use 'mixins' as mix; @include 호출 블록 { .main-header { flex-direction: column; } } @include에 전달한 같은 요소의 선언이 mixin의 @content 위치에 들어간다. |
| 원도표 설명 | @content는 runtime callback이 아니라 호출 블록을 컴파일 시 삽입하는 자리다. 미디어 feature query는 SassScript 변수와 연산식을 직접 받으므로 이 예제에는 인터폴레이션이 필요하지 않다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| mixin 정의 | Sass: 정의 상태 / CSS: 아직 없음 | @content를 받을 규칙 틀을 만든다 | 정의 자체가 브라우저 적용 규칙은 아니다 |
| include와 컴파일 | 경계: 768−1=767px / CSS: media 규칙 산출 | 호출 내용의 header column 규칙이 767px media 안에 놓인다 | Sass 계산과 browser query 판정은 다르다 |
| 브라우저 조건 | viewport: 조건 만족 여부 / header: 조건에 따른 배치 | 산출 CSS가 실제 viewport 조건에서만 적용된다 | include 호출 시점을 화면 resize 사건으로 해석하지 않는다 |
scss/_base.scss 파일 작성// _base.scss
@use 'variables' as vars;
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: vars.$bg-color;
color: vars.$text-color;
line-height: 1.6;
}
a {
text-decoration: none;
color: inherit;
}
ul {
list-style: none;
}scss/_layout.scss 파일 작성// _layout.scss
@use 'variables' as vars;
.content-wrapper {
max-width: 1200px;
margin: 20px auto;
padding: 20px;
background-color: vars.$surface-color;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.page-title {
color: vars.$primary-color;
margin-bottom: 15px;
}
.content-copy {
margin-bottom: 10px;
}
.content-demo {
min-height: 32rem;
padding: 20px;
background-color: vars.$bg-color;
}
.site-footer {
text-align: center;
background-color: #ccc;
padding: 15px;
margin-top: 50px;
color: #555;
}scss/_header.scss 파일 작성 (BEM + Sass 기능)// _header.scss
@use 'variables' as vars;
@use 'mixins' as mix;
.main-header {
background-color: vars.$header-bg;
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
.main-header__logo {
color: vars.$nav-link-color;
font-size: 1.8em;
font-weight: bold;
transition: color 0.2s ease;
&:hover {
color: vars.$nav-link-hover-color;
}
}
.main-nav__list {
display: flex;
gap: 25px;
}
.main-nav__link {
display: block;
color: vars.$nav-link-color;
font-weight: 500;
padding: 8px 0;
transition: color 0.2s ease, background-color 0.2s ease;
&:hover {
color: vars.$nav-link-hover-color;
}
}
@include mix.mobile-only {
.main-header {
flex-direction: column;
align-items: stretch;
gap: 12px;
padding: 12px 20px;
}
.main-header__logo {
text-align: center;
}
.main-nav__list {
flex-direction: column;
gap: 0;
}
.main-nav__item {
border-top: 1px solid rgba(255, 255, 255, 0.1);
&:last-child {
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
}
.main-nav__link {
width: 100%;
padding: 12px 8px;
text-align: center;
&:hover {
background-color: rgba(255, 255, 255, 0.1);
}
}
}BEM class를 각각 최상위 규칙으로 두고, &:hover와 &:last-child처럼 상태가 분명한 경우에만 한 단계 중첩했습니다. 모바일에서도 링크는 숨기지 않고 세로로 쌓이므로 JavaScript 상태 없이 완전한 메뉴를 유지합니다.
- 프로젝트 루트 폴더(
web-dev-practice)에서 터미널/명령 프롬프트를 열고 다음 명령을 실행합니다.npx sass --watch scss/style.scss:css/style.css scss/style.scss및 관련 파일들을 저장할 때마다css/style.css파일이 자동으로 업데이트되는 것을 확인합니다.sass_header.html파일을 웹 브라우저에서 열어 봅니다.- 브라우저 창의 너비를 조절해 보세요.
- 데스크톱/태블릿 크기에서는 내비게이션 메뉴가 가로로 보일 것입니다.
- 모바일 크기(
767px이하)에서는 헤더와 내비게이션 링크가 세로로 쌓이며, 모든 링크는 계속 표시됩니다.
출력 파일부터 화면까지 추적하기
watch를 실행한 터미널의 현재 폴더는 package.json과 sass_header.html이 있는 프로젝트 루트여야 합니다. 다른 폴더에서 실행하면 scss/style.scss 경로를 찾지 못할 수 있습니다. watch는 CSS를 만들고 Live Server는 HTML/CSS 변경 후 브라우저 새로고침을 돕습니다. 하나가 다른 하나를 대신하지 않습니다.
저장 후 css/style.css에서 .main-header { display: flex; ... }와 @media screen and (max-width: 767px) 안의 flex-direction: column을 찾아 보세요. SCSS의 $mobile-max와 @include mix.mobile-only 호출은 사라지고, 조건과 일반 CSS 선언이 남습니다. 컴파일 오류가 보이면 터미널이 알려 주는 파일/행을 먼저 고치세요. 출력 CSS가 오류 표시로 바뀔 수도 있으므로 옛 정상 파일이 항상 유지된다고 가정하지 않습니다. 링크 목적지는 모두 #라 실제 카테고리 이동이나 글쓰기 기능은 없습니다.
혼자 기준 폭 바꾸기
메뉴가 더 길어져 900px 이전부터 세로 배치가 필요하다고 가정하세요. _variables.scss 한 값을 바꾸어 max-width가 899px인 CSS를 만들고, 899px과 900px에서 예상 배치를 먼저 적은 뒤 저장·컴파일·브라우저 순서로 확인합니다. JavaScript나 HTML 링크 숨김은 추가하지 않습니다.
변경과 경계 풀이
$mobile-breakpoint: 900px로 바꾸면 mixin의 1px 뺄셈으로 899px 조건이 출력됩니다. 899px에는 세로 헤더와 메뉴가 적용되고 900px에는 기본 가로 배치가 적용됩니다. Sass 변수 변경만으로 모든 기기에 최적의 기준이 정해진 것은 아니므로 실제 메뉴 길이가 공간에 맞는지도 확인합니다.
마무리 점검
Sass 기능을 많이 사용하는 것보다 컴파일된 CSS가 예측 가능하고 관리 가능한지가 더 중요합니다. 실습을 마치기 전에 다음을 확인하세요.
- module 의존성: 각 stylesheet가 사용하는 변수와 믹스인을 자신의
@use로 명시했는가? - 선택자: BEM 규칙과 얕은 중첩이 짧고 의도한 specificity의 CSS로 컴파일되었는가?
- 출력 중복: 같은 선언이나 미디어 쿼리가 불필요하게 여러 번 생성되지 않았는가?
- 산출물: 브라우저가 SCSS가 아니라 갱신된
css/style.css를 불러오는가? - 화면 동작:
767px이하에서 헤더와 메뉴가 세로로 쌓이고, 모든 링크를 사용할 수 있는가?