VanillaFront 튜토리얼 — 8. 테마
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' }
]
};
}
}
흐름:
- clearAdoptedStyleSheets() — 상위에서 물려받은 공용 stylesheet를 이 뷰에서 무효화
- addIndependantStyleSheetUrl(url) — 뷰 전용 CSS를 로드 (async)
- 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대 규칙
- 테마는 CSS 변수 세트 — va-{테마명}.css + va.css 조합으로 완성
- 11종 기본 테마 × tight 변형 — light / dark / cupertino / titan / paper / northstar / graphite 등
- 컴포넌트 색상은 항상 변수로 — var(--colorBackground) 등, 하드코딩 금지
- 런타임 전환은 hash 보존 + 리로드 — ?theme=X#hash
- 뷰별 독립 테마 가능 — clearAdoptedStyleSheets() + addIndependantStyleSheetUrl()