두 가지 색 구성표, 네 가지 모드: 네이티브 CSS 테마 전환

발행: (2026년 2월 25일 오전 06:42 GMT+9)
7 분 소요
원문: Dev.to

Source: Dev.to

위에 제공된 소스 링크 외에 번역할 텍스트가 포함되어 있지 않습니다. 번역이 필요한 전체 내용을 제공해 주시면 한국어로 번역해 드리겠습니다.

The Idea

프론트엔드가 마침내 vanilla‑ization으로 나아가고 있으며, 이는 Wes Bos가 첫 강좌를 공개한 이후 프론트엔드에 일어난 최고의 일입니다. 테마에 대한 네이티브 브라우저 지원은 완전히 잘 작동하고 많은 프로젝트에 충분할 수 있지만, 저는 그냥 재미를 느끼고 싶었습니다. 저는 밝고 어두운 변형을 가진 하나의 색상 스키마만 원하지 않았습니다. 저는 각각 밝고 어두운 모드를 지원하는 개의 스키마, 즉 총 네 가지 변형을 원했고, 이를 오직 네이티브 브라우저 기능만으로 구현하고 싶었습니다. 그리고 성공했습니다.

The Requirements

  • 브라우저가 기본적으로 처리하는 라이트 및 다크 모드
  • 완전히 다른 팔레트를 가진 두 번째 “spring”(또는 기타) 색상 스킴
  • defaultspring 사이를 전환하는 토글 버튼
  • 새로 고침 시 잘못된 테마가 깜빡이지 않음
  • 전환을 위해 페이지 새로 고침이 필요 없음

The Specificity Hack

:root:root:root

만약 styled‑components를 사용한다면, 전역 CSS 변수가 styled‑components가 주입하는 스타일에 의해 덮어씌워지는 specificity 전쟁을 겪어봤을 것입니다. 해결 방법은 아주 단순합니다: :root를 세 번 반복하면 됩니다.

Note: styled‑components는 오래된 기술이며, 앞으로는 이를 없애고 싶지만 이 글의 주요 주제는 아닙니다.

:root:root:root {
  --color-background: light-dark(
    oklch(0.99 0.0105 320.98),
    oklch(13.709% 0.02553 268.319)
  );
  --color-primary: light-dark(
    oklch(70.61% 0.085 271.69),
    oklch(0.79 0.1233 266.14)
  );
  /* …etc */
}

브라우저가 사용자의 시스템 선호도에 따라 올바른 값을 자동으로 선택합니다 – JavaScript가 필요 없습니다. 이를 작동시키려면 color-scheme 선언이 필요합니다:

<!-- Example: declare color-scheme in HTML -->
<html color-scheme="light dark">

</html>

Important: 아직 light-dark()를 지원하지 않는 브라우저를 위해, 아래에 @media (prefers-color-scheme: …) 폴백 블록을 추가해 두세요. 캐스케이딩이 이를 부드럽게 처리합니다.

스프링 테마 – 하나의 CSS 파일, <html>에 클래스

두 번째 테마에서는 동적 CSS import를 피하고 싶었습니다(비동기이며 Vite를 사용한 프로덕션 빌드에서 신뢰성이 떨어지고 복잡성을 더합니다). 대신 두 테마를 하나의 CSS 파일에 포함시켰습니다.

스프링 테마는 <html> 요소에 .spring 클래스를 적용하여 기본 변수를 재정의합니다. 핵심 포인트는 특이성(specificity)이며, .spring:root:root:root는 추가된 클래스 때문에 :root:root:root보다 우선합니다.

/* default theme */
:root:root:root {
  --color-background: light-dark(
    oklch(0.99 0.0105 320.98),
    oklch(0.21 0.037 271.06)
  );
  --color-primary: light-dark(
    oklch(70.61% 0.085 271.69),
    oklch(0.79 0.1233 266.14)
  );
}

/* spring theme – wins when .spring is on <html> */
.spring:root:root:root {
  --color-background: light-dark(
    oklch(0.99 0.012 150),
    oklch(0.18 0.05 145)
  );
  --color-primary: light-dark(
    oklch(56.316% 0.10067 150.907),
    oklch(0.72 0.2 145)
  );
}

두 테마 모두에서 밝은 모드와 어두운 모드는 여전히 자동으로 작동하며, light-dark()는 활성화된 테마와 관계없이 계속 제 역할을 합니다.

토글

classList.toggle()는 클래스를 전환하고 새로운 불리언 값을 반환합니다. 이것이 전체 토글 로직이며, React 상태도, 컨텍스트도, 재렌더도 없습니다. CSS는 클래스 변경이 DOM에 즉시 반영되기 때문에 바로 반응합니다.

// UI component
function handleToggle() {
  const isSpring = document.documentElement.classList.toggle("spring");
  localStorage.setItem("theme", isSpring ? "spring" : "default");
}

로드 시 올바른 테마 적용

첫 번째 페인트 이전에 동기식으로 실행하여 플래시, 레이아웃 이동, 또는 useEffect 타이밍 문제를 방지하세요.

// main.tsx
import "./themes.css";

const savedTheme = localStorage.getItem("theme");
document.documentElement.classList.toggle("spring", savedTheme === "spring");

ReactDOM.createRoot(document.getElementById("root")!).render(
  <App />
);

작동하지 않은 시도

이 솔루션에 도달하기 전에 @container style() 쿼리를 실험해 보았습니다. 이는 부모 요소의 커스텀 속성 값에 따라 스타일을 적용할 수 있게 하는 최신 CSS 기능입니다:

@container style(--theme: spring) {
  body {
    --color-background: …;
  }
}

Chrome에서는 작동했지만 Firefox Developer Edition에서는 동작하지 않았고, Safari는 전혀 지원하지 않았습니다. 그래서 포기했지만, 앞으로는 눈여겨볼 가치가 있습니다. 더 우아한 접근 방식이 될 수 있었으니까요.

결과

  • 하나의 CSS 파일에 네 가지 테마 변형(light default, dark default, light spring, dark spring) 모두 포함
  • 색상 값을 위한 JavaScript 전혀 없음 – 모든 색상은 CSS에 존재
  • 테마 적용을 위한 React 컨텍스트나 상태 없음
  • 토글 시 페이지 새로고침 없음
  • 로드 시 잘못된 테마가 깜빡이는 현상 없음
  • 라이트/다크 선호도에 대한 브라우저 기본 처리
0 조회
Back to Blog

관련 글

더 보기 »

FSCSS 변수 대체 연산자 (||)

FSCSS Variable Fallback Operator의 커버 이미지 ||https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fd...