본문으로 건너뛰기

안동민 개발노트

본문 시작

Sass로 반응형 블로그 헤더 만들기

Sass 변수·믹스인·부분 파일·BEM을 조합해 반응형 블로그 헤더를 구현하고 컴파일 결과를 확인합니다.

실습: Sass로 반응형 블로그 헤더 만들기

이번 장에서 배운 Sass module, 변수, 믹스인과 BEM을 조합해 반응형 헤더 컴포넌트를 만들어 봅시다. 이 실습은 JavaScript 상호작용 없이, 화면 폭에 따라 메뉴 배치가 바뀌는 CSS 레이아웃에 집중합니다.

프로젝트 초기화와 폴더 구조

먼저 실습 폴더를 만들고 Sass를 프로젝트의 개발 의존성으로 설치합니다. 이후 npx sass는 이 로컬 버전을 사용하며, 생성된 lockfile이 실습 환경의 버전을 고정합니다.

프로젝트 초기화
mkdir web-dev-practice
cd web-dev-practice
npm init -y
npm install --save-dev sass
package.json
package-lock.json
sass_header.html
_variables.scss
_mixins.scss
_base.scss
_layout.scss
_header.scss
style.scss (entry 파일)
style.css (컴파일될 파일)
sass_header.html 파일 작성
sass_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>&copy; 2026 Sass Practice.</p>
    </footer>
</body>
</html>
scss/style.scss 파일 작성 (entry 파일)
scss/style.scss
// CSS를 출력하는 module만 순서대로 불러옵니다.
@use 'base';
@use 'layout';
@use 'header';

@use로 불러온 변수와 믹스인은 현재 stylesheet 안에서만 보입니다. 따라서 style.scss가 모든 의존성을 전역으로 준비하는 대신, 각 partial이 자신에게 필요한 module을 직접 @use합니다.

style.scss가 CSS를 출력하는 Sass module을 불러오고 각 module이 variables와 mixins를 자신의 @use로 명시하는 의존성
Sass module의 명시적 의존성 entry인 style.scss는 base, layout, header를 불러오며, 이 CSS module들과 mixins는 필요한 variables와 mixins를 각 stylesheet에서 직접 불러온다. ENTRY 0 IN style.scss CSS 1 IN _base.scss CSS 1 IN _layout.scss CSS 1 IN _header.scss VALUE 4 IN _variables.scss MIXIN 1 IN _mixins.scss
style.scss ENTRY · base, layout, header를 직접 @use
  • CSS를 출력하는 Sass module
    • _base.scss_variables.scss
    • _layout.scss_variables.scss
    • _header.scss_variables.scss, _mixins.scss
  • 공유 module
    • _mixins.scss_variables.scss
    • _variables.scss는 네 module이 직접 사용한다.
A → B는 A가 B를 직접 @use한다는 뜻이다. IN은 해당 module을 직접 사용하는 파일 수이며, Dart Sass는 entry인 style.scsscss/style.css로 컴파일한다.
scss/_variables.scss 파일 작성
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 파일 작성
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로 펼쳐지는 흐름을 정리한 것입니다.

mobile-only 믹스인의 정의, include 호출, 767px 미디어 쿼리 CSS 출력으로 이어지는 세 단계 코드 변환
  1. 정의 · _mixins.scss

    @use 'variables' as vars;
    
    $mobile-max:
        vars.$mobile-breakpoint - 1px;
    
    @mixin mobile-only {
        @media screen and
            (max-width: $mobile-max) {
            @content;
        }
    }

    공통 미디어 조건 안에 호출 블록을 받을 @content insertion point를 둔다.

  2. 호출 · _header.scss 핵심 발췌

    @use 'mixins' as mix;
    
    @include mix.mobile-only {
        .main-header {
            flex-direction: column;
        }
    }

    @include에 전달한 같은 요소의 선언이 mixin의 @content 위치에 들어간다.

  3. 출력 · css/style.css 핵심 발췌

    @media screen and
        (max-width: 767px) {
        .main-header {
            flex-direction: column;
        }
    }

    변수, 연산, mixin 호출은 사라지고 브라우저가 해석할 표준 CSS만 남는다.

@content는 runtime callback이 아니라 호출 블록을 컴파일 시 삽입하는 자리다. 미디어 feature query는 SassScript 변수와 연산식을 직접 받으므로 이 예제에는 인터폴레이션이 필요하지 않다.
scss/_base.scss 파일 작성
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 파일 작성
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 기능)
scss/_header.scss
// _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 상태 없이 완전한 메뉴를 유지합니다.

Sass 컴파일 및 결과 확인
  • 프로젝트 루트 폴더(web-dev-practice)에서 터미널/명령 프롬프트를 열고 다음 명령을 실행합니다.
    npx sass --watch scss/style.scss:css/style.css
  • scss/style.scss 및 관련 파일들을 저장할 때마다 css/style.css 파일이 자동으로 업데이트되는 것을 확인합니다.
  • sass_header.html 파일을 웹 브라우저에서 열어봅니다.
  • 브라우저 창의 너비를 조절해 보세요.
    • 데스크톱/태블릿 크기에서는 내비게이션 메뉴가 가로로 보일 것입니다.
    • 모바일 크기(767px 이하)에서는 헤더와 내비게이션 링크가 세로로 쌓이며, 모든 링크는 계속 표시됩니다.

마무리 점검

Sass 기능을 많이 사용하는 것보다 컴파일된 CSS가 예측 가능하고 관리 가능한지가 더 중요합니다. 실습을 마치기 전에 다음을 확인하세요.

  • module 의존성: 각 stylesheet가 사용하는 변수와 믹스인을 자신의 @use로 명시했는가?
  • 선택자: BEM 규칙과 얕은 중첩이 짧고 의도한 specificity의 CSS로 컴파일되었는가?
  • 출력 중복: 같은 선언이나 미디어 쿼리가 불필요하게 여러 번 생성되지 않았는가?
  • 산출물: 브라우저가 SCSS가 아니라 갱신된 css/style.css를 불러오는가?
  • 화면 동작: 767px 이하에서 헤더와 메뉴가 세로로 쌓이고, 모든 링크를 사용할 수 있는가?