기본사용법

VanillaFront 튜토리얼 — 8. 테마

VanillaFront 2026. 9. 11. 09:32

https://vanillafront.com 참조

VanillaFront 튜토리얼 — 8. 테마

시리즈 마지막 편입니다. 화면 룩앤필을 결정하는 테마 시스템을 정리합니다.

VanillaFront는 11종의 기본 테마 + tight 변형을 제공하고, 그 아래엔 CSS 변수 기반 설계가 있어 프로젝트에 맞게 커스터마이즈하기도 쉽습니다. Shadow DOM 덕분에 화면별로 다른 테마를 적용하는 것도 가능해요.


테마 시스템 개요

두 종류의 CSS 파일이 짝을 이룹니다.

  • va.css — 프레임워크 공통 구조·레이아웃 (변수를 참조만 함)
  • va-{테마명}.css — 각 테마가 정의하는 CSS 변수 값 세트

앱 초기화 시 두 파일을 함께 로드해 Va.addStyleSheet()로 등록하면 이후 만들어지는 모든 View에 자동 적용됩니다.

// index.js
import theme from './assets/css/va-light.css' with { type: 'css' };
import sheet from './assets/css/va.css'       with { type: 'css' };
import Va    from './lib/va.js';

window.onload = function() {
    Va.addStyleSheet(theme);
    Va.addStyleSheet(sheet);
    // 이후 뷰 초기화
    const app = new App({ startApp: true });
    app.setArea(document.getElementById('app'));
};

with { type: 'css' } 문법은 표준 CSS Modules import — Chrome/Edge에서 네이티브 지원됩니다. 별도 번들러가 없어도 됩니다.


기본 제공 테마 11종 × tight 변형

밝은 계열

테마특징

light 기본 밝은 테마. 흰 배경, 파란 강조
light-grass 초록 계열 강조
light-fox 오렌지 계열 강조
light-sky 하늘색 강조
light-bee 노랑·검정 강조

어두운 계열

테마특징

dark 기본 어두운 테마
dark-sky 하늘색 강조의 다크
dark-bee 노랑 강조의 다크

회색·특수 계열

테마특징

gray-sky 회색 톤에 하늘색 강조
cupertino Apple macOS 스타일
titan 강한 대비의 실무용
paper 종이 질감 느낌
northstar 시원한 다크 톤
graphite 흑연빛 톤

-tight 변형

접미어 -tight가 붙은 파일은 여백·크기가 촘촘한 밀도 높은 버전입니다.

  • va-light.css  va-light-tight.css
  • va-cupertino.css  va-cupertino-tight.css

관리 화면·데이터 밀도가 중요한 대시보드에 적합합니다.

-extend 파일

각 테마마다 -extend 파일이 함께 있습니다 (va-light-extend.css, va-dark-extend.css, va-extend.css 등). 테마별 추가 확장 스타일로, 필요할 때 함께 로드합니다.


CSS 변수 시스템

테마의 정체는 CSS 변수 세트입니다. 프레임워크 컴포넌트는 하드코딩된 색을 쓰지 않고 항상 변수를 참조합니다.

주요 변수 카테고리

카테고리예시 변수

색상 --colorBackground, --colorForeground, --colorPrimary, --colorNeutralStroke, --colorBrandStroke1
크기·간격 --sizeFieldM, --sizeFontMN, --sizeSpacingM, --sizeIconM
폰트 --fontFamilyBase, --fontFamilyMonospace, --fontFamilyNumeric
스크롤바 --sizeScrollBarWidth, --colorScrollBarThumb
아이콘 URL --scrollbarArrowUp, --scrollbarArrowDown

변수 확인 방법

va-light.css 첫 부분을 열어보면 이런 식입니다.

* {
    --fontFamilyBase: 'Pretendard-Regular', 'Segoe UI Web', ...;
    --colorScrollBarThumb: #888;
    --sizeScrollBarWidth: 1.0rem;
    --colorBackground: #ffffff;
    --colorPrimary: #0078d4;
    /* ... 100+ 변수 */
}

한 테마 파일에 100개 이상의 변수가 정의되어 있어 프레임워크 전체 룩을 결정합니다.

커스텀 컴포넌트에서 변수 활용

직접 만드는 컴포넌트도 변수를 그대로 참조하면 테마 전환 시 자동 대응됩니다.

config() {
    return {
        tagName: 'div',
        style: {
            backgroundColor: 'var(--colorBackground)',
            color: 'var(--colorForeground)',
            padding: 'var(--sizeSpacingM)',
            borderRadius: '4px'
        }
    };
}

절대 색상값을 하드코딩하지 마세요. #ffffff 대신 var(--colorBackground) — 이 습관 하나로 다크/라이트 대응이 공짜로 됩니다.


런타임 테마 전환

세 가지 방식이 있습니다.

1) URL 쿼리 파라미터로 시작 테마 지정

https://example.com/index.html?theme=dark

index.html에서 파라미터를 읽어 그에 맞는 CSS를 동적 로드:

<script>
function getParam(name) {
    const params = location.search.substr(1).split('&');
    for (const p of params) {
        const [k, v] = p.split('=');
        if (k === name) return v;
    }
    return '';
}
window.onload = function() {
    let theme = getParam('theme') || 'light';
    const link = document.createElement('link');
    link.setAttribute('rel', 'stylesheet');
    link.setAttribute('href', './assets/css/va-' + theme + '.css');
    document.getElementById('app').appendChild(link);
};
</script>

이 패턴이 VanillaFront 홈페이지·문서 사이트에서 실제로 쓰는 방식입니다.

2) 테마 변경 버튼 → 페이지 리로드

onChangeTheme(btn) {
    const theme = btn.option.theme;
    window.location = window.location.pathname
        + '?theme=' + theme
        + window.location.hash;   // hash 보존 필수!
}

⚠️ hash 보존이 중요합니다. CLAUDE.md에도 명시된 주의사항으로, 라우터 서브뷰 상태를 유지하려면 반드시 window.location.hash를 새 URL에 붙여야 합니다.

3) 동적 스타일시트 교체

리로드 없이 실시간 전환도 가능하지만, 이미 그려진 뷰의 스타일 재계산 문제가 있어 실무에선 리로드 방식이 안정적입니다.


뷰별 독립 테마 — Shadow DOM의 잇점

VanillaFront View는 Shadow DOM으로 감싸져 있어, 화면 하나만 다른 테마를 적용하는 게 가능합니다.

사용 시나리오

  • 전체 앱은 라이트 테마인데 에디터/프리뷰 뷰만 다크 테마
  • 관리자 대시보드 안의 특정 위젯만 브랜드 컬러 테마
  • 사용자 콘텐츠 미리보기를 원본 사이트 테마로

코드

class DarkPreview extends Va.View {
    async mounted() {
        // 앞서 등록된 공용 stylesheet 제거
        this.clearAdoptedStyleSheets();

        // 이 뷰만의 스타일 로드
        await this.addIndependantStyleSheetUrl('../../assets/css/va-dark.css');
        await this.addIndependantStyleSheetUrl('../../assets/css/va.css');

        this.setIndependantStyleSheet();
    }
    config() {
        return {
            tagName: 'article',
            style: 'max-width:600px',
            tags: [
                { tagName: 'h2', innerHTML: '다크 프리뷰' },
                { tagName: 'dateField', label: 'Calendar' }
            ]
        };
    }
}

흐름:

  1. clearAdoptedStyleSheets() — 상위에서 물려받은 공용 stylesheet를 이 뷰에서 무효화
  2. addIndependantStyleSheetUrl(url) — 뷰 전용 CSS를 로드 (async)
  3. setIndependantStyleSheet() — 로드된 스타일을 적용

주의사항

⚠️ 뷰의 최상위 element는 Shadow DOM 바깥에 있어 독립 스타일이 적용되지 않습니다. 뷰 전체를 감쌀 태그를 하나 더 두거나, 최상위 element에는 별도로 인라인 style을 부여해야 합니다.

config() {
    return {
        tagName: 'div',
        style: 'background:#1e1e1e; color:#fff',    // ← 최상위엔 인라인
        tags: [
            // 이하 자식들은 Shadow DOM 안이라 dark 테마가 자동 적용됨
        ]
    };
}

홈페이지에서 실제 쓰는 테마 전환 패턴

VanillaFront 자체 홈페이지가 채택한 방식을 참고로:

// 상단 nav의 테마 버튼
let button = new Va.ResponsiveMenuButton({
    appearance: 'outline',
    icon: theme.indexOf('dark') !== -1 ? 'ico_moon_fill' : 'ico_sun_fill'
});

const themes = [
    'light', 'dark',
    'cupertino', 'cupertino-tight',
    'titan', 'titan-tight',
    'graphite', 'graphite-tight',
    'paper', 'paper-tight',
    'northstar', 'northstar-tight'
];

for (const t of themes) {
    button.append(new Va.ResponsiveMenuItem({
        key: t,
        innerHTML: t,
        onClick: 'onChangeTheme'
    }));
}
onChangeTheme(menuItem) {
    const theme = menuItem.option.key;
    window.location = window.location.pathname
        + '?theme=' + theme
        + window.location.hash;
}

간단하고 확실한 방식입니다.


커스텀 테마 만들기

기존 테마로 부족하다면 자체 테마를 만들면 됩니다.

1) 가장 가까운 기본 테마 복사

assets/css/va-mytheme.css   ← va-light.css를 복사하고 변수만 수정

2) 변수 값 조정

* {
    /* 브랜드 컬러로 변경 */
    --colorPrimary: #ff6600;
    --colorPrimaryHover: #ff8833;
    --colorPrimaryPressed: #cc4400;

    /* 폰트도 커스텀 */
    --fontFamilyBase: 'Noto Sans KR', sans-serif;

    /* 간격 축소 */
    --sizeSpacingM: 0.8rem;
    --sizeFieldM: 3.2rem;
}

3) 로드

import mytheme from './assets/css/va-mytheme.css' with { type: 'css' };
Va.addStyleSheet(mytheme);

4) -tight 변형이 필요하다면

동일 원본에서 크기 관련 변수만 촘촘하게 조정한 별도 파일:

assets/css/va-mytheme-tight.css

--sizeFieldM, --sizeSpacingM, --sizeFontMN 등 크기 변수만 조정하면 됩니다.

  • --extend 파일도 하나 만들어 두면 나중에 확장 스타일 넣기 편함
  • 변수 이름은 프레임워크 것과 통일 — 컴포넌트가 참조하는 이름이 정해져 있으므로 새 이름 도입하지 말 것
  • 새 변수를 추가하는 건 자유. 커스텀 컴포넌트가 참조하도록 하면 됨

자주 마주치는 이슈

1) 테마 전환 후 빈 화면

onChangeTheme에서 리다이렉트할 때 window.location.hash 누락이 원인. SPA 라우터의 sub-view가 리로드 후 초기 화면으로 리셋됨.

해결: hash를 새 URL에 반드시 붙일 것.

window.location = pathname + '?theme=' + theme + window.location.hash;

2) Input focus 시 사각형 표시

일부 테마에서 브라우저 기본 outline이 튀어나오는 경우.

해결: .field-wrapper input:focus-visible { outline: none } 추가 (CLAUDE.md 7장 FAQ 참조).

3) Input 초기 렌더링에 사각형 (titan 등)

input의 type 속성이 update() 전엔 없어서 input[type=text] 셀렉터에 매칭 안 됨.

해결: 테마 CSS의 셀렉터에 input:not([type])도 포함.

4) 다크 테마에서 텍스트 대비 부족

커스텀 컴포넌트에서 색을 하드코딩한 경우가 대부분. var(--colorForeground) / var(--colorBackground) 사용으로 수정.

5) 커스텀 테마 적용 안 됨

  • Va.addStyleSheet() 호출 순서 확인 — 테마 → va.css 순서가 표준
  • 파일이 정말 로드됐는지 DevTools Network 탭에서 확인
  • with { type: 'css' } 문법 지원 안 되는 브라우저에선 안 뜸 (Chromium 계열에서만 지원)

Adopted StyleSheets — 왜 이 방식인가

VanillaFront가 <link rel="stylesheet">가 아니라 Va.addStyleSheet() 방식을 쓰는 이유:

  • Shadow DOM 안까지 자동 적용  <link>는 Shadow DOM 경계를 못 넘음
  • 여러 뷰가 같은 스타일시트 공유 — 메모리 효율적 (실제 CSS 파싱은 1회)
  • JS 코드로 동적 제어 가능 — 조건부 로드, 뷰별 독립 스타일 등

이 방식은 Web Components 표준의 Adopted StyleSheets API를 사용합니다. Chromium 계열 브라우저에서 잘 동작하며, 표준이므로 앞으로도 안정적입니다.


요약 — 테마 5대 규칙

  1. 테마는 CSS 변수 세트  va-{테마명}.css + va.css 조합으로 완성
  2. 11종 기본 테마 × tight 변형 — light / dark / cupertino / titan / paper / northstar / graphite 등
  3. 컴포넌트 색상은 항상 변수로  var(--colorBackground) 등, 하드코딩 금지
  4. 런타임 전환은 hash 보존 + 리로드  ?theme=X#hash
  5. 뷰별 독립 테마 가능  clearAdoptedStyleSheets() + addIndependantStyleSheetUrl()