Astro + Tailwind 블로그에 완벽한 다크 모드 적용하기

#Astro #TailwindCSS #Frontend #Design #Blog

블로그의 가독성을 높이고 개발자 블로그 특유의 느낌을 주기 위해 다크 모드 를 도입했다. 단순한 배경색 변경을 넘어, 화면 깜빡임(FOUC) 방지, 카드의 호버(Hover) 상태 처리, 그리고 마크다운 렌더링 스타일까지 다듬어본 과정을 정리해 본다.

1. Tailwind CSS 다크 모드 활성화

Tailwind에서 제공하는 다크 모드 기능을 사용하기 위해, 가장 먼저 설정 파일을 수정했다. 시스템 설정에 의존하지 않고 사용자가 직접 토글할 수 있도록 class 전략을 선택했다.

// tailwind.config.mjs
export default {
  darkMode: 'class',
  // ... 생략
};

2. 새로고침 깜빡임(FOUC) 방지 및 전체 레이아웃 적용

React 환경에서 클라이언트 사이드 자바스크립트가 로드되기 전까지는 로컬 스토리지의 테마 값을 읽을 수 없다. 이로 인해 새로고침을 하거나 페이지를 옮기면 화면이 하얗게 깜빡이는 현상이 발생한다.

이를 막기 위해 Astro의 최상위 레이아웃 파일의 <head> 태그 안에 인라인 스크립트 를 추가하여, HTML 파싱 단계에서 즉시 dark 클래스를 주입하도록 처리했다. dark 클래스를 주입한다는 것은 <html class="dark"> 형태로 가장 상위 html에 dark 클래스를 정의한다는 뜻이다. tailwind의 dark:~는 조건부 스타일이기 때문에 html에 dark 클래스가 추가되면 그에 맞는 스타일을 적용한다. 더불어 body 태그에는 자연스러운 색상 전환을 위해 transition-colors 를 적용했다.

---
// Layout.astro
---
<!doctype html>
<html lang="ko">
  <head>
    <script is:inline>
      const theme = (() => {
        if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
          return localStorage.getItem('theme');
        }
        return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
      })();
      
      if (theme === 'dark') document.documentElement.classList.add('dark');
      else document.documentElement.classList.remove('dark');
    </script>
  </head>
  <body class="bg-gray-50 dark:bg-zinc-900 text-gray-900 dark:text-zinc-100 transition-colors duration-300">
    </body>
</html>

3. 커스텀 아이콘을 더한 토글 버튼 구현 (React)

헤더 컴포넌트에서는 useStateuseEffect 를 사용해 테마 상태를 관리했다. 특히 이모지(☀️,🌙)를 사용하던 기존 방식을 버리고, SVG 아이콘 에 직접 색상을 입혀서 원하는 느낌을 살렸다.

예시에서는 색상 코드를 바로 썼지만, svg 태그에서도 tailwind 클래스를 사용하여 className="stroke-campfire-red" 형식으로 사용할 수 있다.

// Header.tsx
<button 
  onClick={toggleTheme}
  className="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-zinc-800 transition-colors"
>
  {theme === 'light' ? (
    // 노란색으로 채운 달 아이콘
    <svg fill="#facc15" stroke="#facc15" /* ... */ />
  ) : (
    // 레드로 포인트를 준 해 아이콘
    <svg fill="none" stroke="#E25822">
      <circle cx="12" cy="12" r="4" fill="#E25822" />
      {/* ... */}
    </svg>
  )}
</button>

4. 다크 모드에서의 Hover 우선순위 해결

게시글 목록 카드에서 마우스를 올렸을 때 제목 색상이 변경되는 효과를 주었다. 하지만 다크 모드에서는 hover: 클래스보다 dark: 클래스의 우선순위가 높아 색상이 변하지 않는 문제가 있었다.

이 문제를 해결하기 위해, 단순히 hover: 가 아닌 다크 모드 전용 호버 클래스dark:group-hover: 를 명시적으로 추가하여 우선순위 충돌을 해결했다.

<h2 class="text-gray-900 dark:text-zinc-100 group-hover:text-campfire-red dark:group-hover:text-campfire-red">
  {post.data.title}
</h2>

5. Tailwind Typography 및 인라인 코드 커스텀

마크다운 본문은 @tailwindcss/typography 플러그인의 prosedark:prose-invert 클래스를 사용하여 한 번에 반전시켰다.

추가로, 일반 코드 블록(pre)은 기본 테마를 유지하면서 인라인 코드 에만 고유의 색상과 D2Coding 폰트를 적용하고 싶었다. tailwind.config.mjstypography 설정에서 :not(pre) > code 선택자를 사용하여 아주 깔끔하게 분리해 낼 수 있었다.

// tailwind.config.mjs
typography: {
  DEFAULT: {
    css: {
      ':not(pre) > code': {
        backgroundColor: '#fbf1cf',
        color: '#8c4646',
        fontFamily: 'D2Coding, monospace',
      },
    },
  },
  invert: {
    css: {
      ':not(pre) > code': {
        backgroundColor: '#db6b5c',
        color: '#f7e4ab',
      },
    },
  },
}

마무리

다크 모드 구현은 단순한 색상 반전이라고 생각하기 쉽지만, FOUC 방지, CSS 우선순위, 마크다운 내부 요소 제어 등 세심하게 챙겨야 할 디테일이 많았다. 결과적으로 가독성 높은 노란색과 강렬한 레드 포인트 컬러가 조화롭게 어우러지는 나만의 다크 모드를 완성할 수 있었다.