테마 전환

Tailwind 다크 모드: class와 data-theme 방식 비교

Easton editorial illustration: three-option fit selector

화면에서 dark:bg-gray-900 한 줄이 깜박이는 순간, 이런 의문이 듭니다. Tailwind 다크 모드는 class와 data-theme 중 어떤 방식을 써야 할까요?

이 두 방식을 한동안 직접 다뤄 봤습니다. 문서를 검색할 때마다 조각난 설명만 나와서 한데 모아도 어딘가 부족하게 느껴졌습니다. 결국 공식 문서와 GitHub 토론, 인기 컴포넌트 라이브러리 몇 곳의 소스 코드까지 직접 살펴본 뒤에야 생각이 정리됐습니다. 이 글에서는 그동안 겪은 시행착오와 고민 끝에 내린 판단을 모두 풀어보겠습니다.


Tailwind 다크 모드의 세 가지 전략

먼저 한 가지를 분명히 해두겠습니다. Tailwind가 기본으로 제공하는 다크 모드 전략은 두 가지가 아니라 세 가지입니다.

Media 전략: 시스템 설정 자동 추종

Media 전략은 Tailwind의 기본 설정입니다. 솔직히 이것이 기본값이라는 사실을 모르는 사람도 많을 겁니다. prefers-color-scheme CSS 미디어 쿼리로 사용자의 시스템 다크 모드 설정을 자동 감지합니다.

<!-- 별도 설정 없이 시스템 설정에 자동 대응 -->
&lt;div class="bg-white dark:bg-gray-900"&gt;
  콘텐츠가 시스템 설정에 따라 자동으로 전환됩니다
&lt;/div&gt;

장점은 분명합니다. 설정이 전혀 필요 없고 사용자가 따로 조작하지 않아도 익숙한 화면을 볼 수 있습니다. 하지만 단점도 뼈아픕니다. 사용자가 직접 선택할 수 없습니다. 밝은 환경에서도 다크 모드를 쓰고 싶은 사람에게는 만족스럽지 않은 경험입니다.

Class 전략: 수동 전환 제어

Class 전략은 상위 요소(보통 &lt;html&gt;)에 .dark 클래스를 추가해 다크 모드를 활성화합니다. 개발자가 충분한 제어권을 가지므로 사용자의 수동 전환과 설정 저장을 모두 구현할 수 있습니다.

<!-- JavaScript로 클래스 이름 제어 -->
&lt;html class="dark"&gt;
  &lt;body class="bg-white dark:bg-gray-900"&gt;
    다크 모드가 적용됩니다
  &lt;/body&gt;
&lt;/html&gt;

현재 가장 널리 쓰이는 방식입니다. 커뮤니티 문서가 풍부하고 여러 서드파티 라이브러리와도 매끄럽게 통합됩니다.

Data-theme 전략: 의미가 명확한 속성 선택자

Data-theme 전략은 클래스 이름 대신 data-theme="dark" 속성을 사용합니다. 의미가 더 명확하고 여러 테마로 자연스럽게 확장할 수 있습니다.

&lt;html data-theme="dark"&gt;
  &lt;body class="bg-white dark:bg-gray-900"&gt;
    다크 모드가 적용됩니다
  &lt;/body&gt;
&lt;/html&gt;

테마를 더 추가하기도 매우 쉽습니다. data-theme="oled"data-theme="sepia"처럼 원하는 대로 정의하면 됩니다. 여러 표시 모드를 지원해야 할 때 특히 유용합니다.


Class 전략 자세히 알아보기

구현 원리

Class 전략의 핵심 원리는 간단합니다. DOM 트리의 어떤 상위 요소에 .dark 클래스가 있으면 모든 dark:* 수정자 스타일이 적용됩니다.

Tailwind v3에서는 설정 파일에서 활성화합니다.

// tailwind.config.js
module.exports = {
  darkMode: 'class',
  // ...
}

생성되는 CSS 선택자 구조는 다음과 같습니다.

.dark .dark:bg-gray-900 {
  background-color: #111827;
}

Tailwind v4에서는 완전히 새로운 CSS-first 설정 방식을 도입해 @custom-variant 지시어를 사용합니다.

/* global.css */
@import 'tailwindcss';
@custom-variant dark (&:where(.dark, .dark *));

여기서 :where() 의사 클래스를 눈여겨보세요. specificity를 0으로 낮춰 다른 스타일의 우선순위 계산을 방해하지 않습니다. 꽤 중요한 세부 사항입니다.

JavaScript 전환 로직

사용자 전환 기능은 짧은 JavaScript 코드만으로 충분합니다.

// 현재 테마 가져오기
function getTheme() {
  return localStorage.getItem('theme') ||
    (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
}

// 테마 설정
function setTheme(theme) {
  localStorage.setItem('theme', theme);
  document.documentElement.classList.toggle('dark', theme === 'dark');
}

// 초기화
setTheme(getTheme());

이 코드는 세 가지 일을 합니다. localStorage에서 사용자 설정을 읽고, 설정이 없으면 시스템을 따르며, 테마를 전환한 뒤 저장합니다. 이것이면 충분합니다.

흰 화면 깜박임 방지

페이지를 불러올 때 잠깐 흰 화면이 깜박이는 문제는 저도 겪었습니다. 원인은 단순합니다. JavaScript가 실행되기 전에 HTML이 기본 라이트 모드로 이미 렌더링되기 때문입니다.

해결 방법은 &lt;head&gt; 안에 동기 실행 스크립트를 넣어 DOM이 렌더링되기 전에 테마를 설정하는 것입니다.

&lt;head&gt;
  &lt;script&gt;
    // 동기 실행으로 깜박임 방지
    if (localStorage.theme === 'dark' ||
        (!('theme' in localStorage) &&
         window.matchMedia('(prefers-color-scheme: dark)').matches)) {
      document.documentElement.classList.add('dark');
    }
  &lt;/script&gt;
&lt;/head&gt;

이 스크립트는 반드시 동기식이어야 합니다. deferasync를 사용하면 안 됩니다.

장단점

장점:

  • 구현이 단순하고 직관적이어서 빠르게 익힐 수 있습니다.
  • 커뮤니티 자료가 많고 프레임워크별로 검증된 방식이 있습니다.
  • next-themes 같은 도구 라이브러리와 잘 연동됩니다.
  • specificity가 조금 더 높아 스타일 재정의가 확실합니다.

단점:

  • .dark 클래스 이름만으로는 의미가 명확하지 않아 코드를 볼 때 다크 모드인지 한 번 생각해야 합니다.
  • 여러 테마로 확장하려면 클래스 이름도 여러 개 필요해 관리가 다소 복잡해집니다.
  • CSS 변수 방식과 결합할 때는 추가 대응이 필요합니다.

Data-theme 전략 자세히 알아보기

구현 원리

Data-theme 전략의 핵심은 클래스 선택자 대신 속성 선택자를 사용하는 것입니다. Tailwind v4에서는 다음과 같이 설정합니다.

@import 'tailwindcss';
@custom-variant dark (&:where([data-theme='dark'], [data-theme='dark'] *));

생성되는 CSS 선택자는 다음과 같습니다.

[data-theme='dark'] .dark:bg-gray-900 {
  background-color: #111827;
}

Tailwind v3에서도 지원하지만 배열로 설정해야 합니다.

// tailwind.config.js
module.exports = {
  darkMode: ['selector', '[data-theme="dark"]'],
}

CSS 변수 방식과 결합하기

솔직히 data-theme 전략과 CSS 변수 방식은 찰떡궁합입니다. 각 data-theme 아래에서 서로 다른 변수 값을 정의할 수 있습니다.

/* globals.css */
:root {
  --background: 0 0% 100%;
  --foreground: 222 84% 5%;
}

[data-theme='dark'] {
  --background: 222 84% 5%;
  --foreground: 210 40% 98%;
}

[data-theme='oled'] {
  --background: 0 0% 0%;  /* 완전한 검은색 */
  --foreground: 0 0% 100%;
}

그런 다음 Tailwind 설정에서 이 변수들을 참조합니다.

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        background: 'hsl(var(--background))',
        foreground: 'hsl(var(--foreground))',
      }
    }
  }
}

이제 data-theme 속성만 바꾸면 이 변수들을 사용하는 모든 스타일이 자동으로 전환됩니다. 각 컴포넌트에 dark: 수정자를 쓸 필요가 없습니다. 실제로 써보면 무척 편합니다.

shadcn/ui의 실전 방식

shadcn/ui 컴포넌트 라이브러리는 기본적으로 data-theme과 CSS 변수 조합을 사용합니다. 스타일 파일을 살펴보면 다음과 같은 정의가 많이 보입니다.

@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 222.2 84% 4.9%;
    --card: 0 0% 100%;
    --card-foreground: 222.2 84% 4.9%;
    --primary: 222.2 47.4% 11.2%;
    --primary-foreground: 210 40% 98%;
    /* ... 더 많은 변수 */
  }

  .dark,
  [data-theme='dark'] {
    --background: 222.2 84% 4.9%;
    --foreground: 210 40% 98%;
    --card: 222.2 84% 4.9%;
    --card-foreground: 210 40% 98%;
    --primary: 210 40% 98%;
    --primary-foreground: 222.2 47.4% 11.2%;
    /* ... 더 많은 변수 */
  }
}

흥미로운 점은 .dark 클래스와 [data-theme='dark'] 속성을 동시에 지원한다는 사실입니다. 서로 다른 사용자의 습관을 모두 호환하기 위한 것입니다. shadcn/ui를 쓴다면 어떤 방식으로 다크 모드를 활성화해도 괜찮습니다.

여러 테마로 확장하는 능력

Data-theme 방식의 가장 큰 장점은 바로 여러 테마를 지원하기 쉽다는 것입니다. OLED 모드나 눈 보호 모드도 간단히 정의할 수 있습니다.

&lt;html data-theme="oled"&gt;
  <!-- OLED 화면에 적합한 완전한 검은색 배경 -->
&lt;/html&gt;

&lt;html data-theme="sepia"&gt;
  <!-- 읽기에 적합한 연한 노란색 배경 -->
&lt;/html&gt;

전환 로직에서는 속성값만 바꾸면 됩니다.

function setTheme(theme) {
  localStorage.setItem('theme', theme);
  document.documentElement.dataset.theme = theme;
}

이런 유연성은 class 전략으로 구현하기가 쉽지 않습니다.

장단점

장점:

  • 의미가 명확합니다. data-theme="dark"만 봐도 다크 모드임을 바로 알 수 있습니다.
  • 여러 테마로 자연스럽게 확장할 수 있습니다.
  • CSS 변수 방식과 특히 매끄럽게 결합됩니다.
  • shadcn/ui, daisyUI 같은 라이브러리가 기본으로 호환됩니다.

단점:

  • Tailwind v3에서는 selector를 직접 설정해야 합니다.
  • 일부 서드파티 라이브러리는 별도 대응이 필요할 수 있습니다.
  • 커뮤니티 문서가 상대적으로 적지만 점차 나아지고 있습니다.

두 방식 비교표

핵심 기준을 한눈에 볼 수 있도록 비교표로 정리했습니다.

낮음
Class 구현 복잡도
설정이 간단함
보통
Data-theme 구현 복잡도
속성 선택자를 이해해야 함
높음
Class 커뮤니티 지원
문서가 풍부함
보통
Data-theme 커뮤니티 지원
보급되는 중
어려움
Class 다중 테마 확장
클래스 이름이 여러 개 필요함
쉬움
Data-theme 다중 테마 확장
속성값만 바꾸면 됨
Source: 방식 비교 분석
비교 기준Class 전략Data-theme 전략
구현 복잡도낮음, 설정이 간단함보통, 속성 선택자를 이해해야 함
의미의 명확성보통, .dark의 의미를 생각해야 함높음, data-theme이 직관적임
다중 테마 확장어려움, 여러 클래스 이름이 필요함쉬움, 속성값만 바꾸면 됨
커뮤니티 지원높음, 문서가 풍부함보통, 보급되는 중
CSS 변수 통합추가 대응 필요자연스럽게 잘 맞음
Tailwind v3darkMode: 'class'darkMode: ['selector', '...']
Tailwind v4@custom-variant@custom-variant
서드파티 라이브러리 호환호환성 확인 필요shadcn/ui 등과 기본 호환
Specificity약간 높음(클래스 선택자)같음(속성 선택자)

Class 전략은 언제 선택할까요?

  • 라이트·다크 두 가지 모드만 필요한 간단한 프로젝트
  • Next.js와 next-themes 조합을 사용하는 경우
  • 팀이 Tailwind v3 설정에 익숙한 경우
  • 커뮤니티 사례를 많이 참고해야 하는 경우

Data-theme 전략은 언제 선택할까요?

  • OLED, 눈 보호 모드 등 여러 테마를 지원해야 하는 경우
  • shadcn/ui나 비슷한 컴포넌트 라이브러리를 사용하는 경우
  • CSS 변수 방식과 깊이 결합하려는 경우
  • 프로젝트에서 의미가 명확한 마크업을 중요하게 여기는 경우

프레임워크 통합 실전

Astro 통합 방식

Astro와 Tailwind의 통합 자체는 간단하지만 View Transitions 처리라는 함정이 하나 있습니다.

기본 설정:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  vite: {
    plugins: [tailwindcss()]
  }
});

다크 모드 스크립트:

&lt;!-- BaseLayout.astro의 head 안에 배치 --&gt;
&lt;script is:inline&gt;
  // 흰 화면 깜박임을 막는 동기 스크립트
  const theme = localStorage.getItem('theme') ||
    (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');

  if (theme === 'dark') {
    document.documentElement.classList.add('dark');
    // 또는 data-theme 사용
    // document.documentElement.dataset.theme = 'dark';
  }
&lt;/script&gt;

View Transitions 처리:

Astro의 View Transitions는 페이지 전환 시 DOM을 다시 렌더링하므로 다크 모드 상태가 쉽게 사라집니다. astro:after-swap 이벤트를 수신해 테마를 다시 설정해야 합니다.

&lt;script&gt;
  document.addEventListener('astro:after-swap', () => {
    const theme = localStorage.getItem('theme');
    if (theme === 'dark') {
      document.documentElement.classList.add('dark');
    }
  });
&lt;/script&gt;

이 단계는 상당히 중요합니다. 많은 개발자가 놓치기 쉽고 저 역시 같은 문제를 겪었습니다.

Next.js + next-themes 통합

Next.js 프로젝트에는 next-themes 라이브러리를 권합니다. 테마 전환에 필요한 전체 로직이 이미 들어 있어 SSR 호환성과 hydration 처리까지 신경 쓰지 않아도 됩니다.

설치:

npm install next-themes

Provider 설정:

// components/ThemeProvider.tsx
import { ThemeProvider } from 'next-themes';

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  return (
    &lt;ThemeProvider
      attribute="class"        // class 전략 사용
      defaultTheme="system"    // 기본값은 시스템 설정 추종
      enableSystem={true}      // 시스템 감지 활성화
      disableTransitionOnChange  // 전환 시 깜박임 방지
    &gt;
      {children}
    &lt;/ThemeProvider&gt;
  );
}

data-theme 전략으로 바꾸고 싶나요? attribute 속성만 바꾸면 됩니다.

&lt;ThemeProvider attribute="data-theme" defaultTheme="system"&gt;

layout에서 사용하기:

// app/layout.tsx
import { ThemeProvider } from './components/ThemeProvider';

export default function RootLayout({ children }) {
  return (
    &lt;html lang="zh"&gt;
      &lt;body&gt;
        &lt;ThemeProvider&gt;
          {children}
        &lt;/ThemeProvider&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  );
}

전환 버튼 컴포넌트:

// components/ThemeToggle.tsx
import { useTheme } from 'next-themes';

export function ThemeToggle() {
  const { theme, setTheme } = useTheme();

  return (
    &lt;button
      onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
      className="p-2 rounded-lg"
    &gt;
      {theme === 'dark' ? '☀️' : '🌙'}
    &lt;/button&gt;
  );
}

next-themes는 localStorage 저장, 시스템 설정 감지, hydration 문제를 자동으로 처리합니다. 무척 편합니다.


Tailwind v4의 새로운 기능

Tailwind v4에는 완전히 새로운 CSS-first 설정 방식이 도입됐으며 다크 모드 설정도 달라졌습니다.

@custom-variant 지시어

이전에는 JavaScript 설정 파일에 정의하던 variant를 이제 CSS에서 바로 선언할 수 있습니다.

@import 'tailwindcss';

/* Class 전략 */
@custom-variant dark (&:where(.dark, .dark *));

/* Data-theme 전략 */
@custom-variant dark (&:where([data-theme='dark'], [data-theme='dark'] *));

설정 변경을 위해 JavaScript를 다시 빌드하지 않아도 되므로 더 직관적입니다.

@theme 지시어로 변수 정의하기

data-theme 전략과 함께 @theme 지시어로 테마 변수를 정의합니다.

@import 'tailwindcss';
@custom-variant dark (&:where([data-theme='dark'], [data-theme='dark'] *));

@theme {
  --color-primary: oklch(0.65 0.2 150);
  --color-muted: oklch(0.9 0.02 200);
}

/* 다크 모드에서 변수 재정의 */
[data-theme='dark'] {
  --color-primary: oklch(0.7 0.15 180);
  --color-muted: oklch(0.3 0.02 200);
}

이제 이 색상을 바로 사용하면 됩니다.

&lt;button class="bg-primary text-white"&gt;버튼&lt;/button&gt;

data-theme을 바꾸면 색상도 자동으로 바뀝니다. dark:bg-primary-dark처럼 장황한 스타일을 쓸 필요가 없습니다.

3단계 전환 구현

light/dark/system의 3단계 전환은 window.matchMedia API와 함께 구현해야 합니다.

function setTheme(theme) {
  if (theme === 'system') {
    localStorage.removeItem('theme');
    const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
    document.documentElement.dataset.theme = isDark ? 'dark' : 'light';
  } else {
    localStorage.setItem('theme', theme);
    document.documentElement.dataset.theme = theme;
  }
}

// 시스템 설정 변경 감지
window.matchMedia('(prefers-color-scheme: dark)')
  .addEventListener('change', (e) => {
    if (!localStorage.getItem('theme')) {
      document.documentElement.dataset.theme = e.matches ? 'dark' : 'light';
    }
  });

이렇게 하면 사용자가 특정 테마를 고정하거나 항상 시스템 설정을 따르도록 선택할 수 있습니다.


모범 사례 정리

추천 방식 선택

대부분의 프로젝트에는 다음과 같은 선택을 권합니다.

  1. 간단한 프로젝트: class 전략과 간단한 전환 스크립트면 충분합니다.
  2. shadcn/ui 사용: data-theme과 CSS 변수 조합을 그대로 사용합니다.
  3. 여러 테마 필요: 반드시 data-theme 전략을 사용합니다.
  4. Next.js 프로젝트: next-themes를 사용하고 필요에 따라 attribute를 선택합니다.
  5. Astro 프로젝트: View Transitions 처리를 반드시 주의합니다.

실전 팁

흰 화면 깜박임을 막는 전체 방식:

&lt;head&gt;
  &lt;script is:inline&gt;
    // 렌더링 전에 실행되는 동기 스크립트
    (function() {
      const theme = localStorage.getItem('theme');
      const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;

      if (theme === 'dark' || (!theme && systemDark)) {
        document.documentElement.classList.add('dark');
        // 또는
        document.documentElement.dataset.theme = 'dark';
      }
    })();
  &lt;/script&gt;
&lt;/head&gt;

SSR 프로젝트 처리 방법:

Next.js 같은 SSR 프로젝트에서는 hydration mismatch를 피해야 합니다. next-themes는 이미 이 문제를 처리합니다. 직접 구현하고 싶다면 다음 사항을 유의하세요.

// useEffect로 SSR 불일치 방지
import { useEffect, useState } from 'react';

function useTheme() {
  const [theme, setTheme] = useState('light');

  useEffect(() => {
    const saved = localStorage.getItem('theme');
    setTheme(saved || 'light');
  }, []);

  return theme;
}

CSS 변수의 의미 중심 이름 짓기:

색상 이름 대신 의미가 드러나는 변수 이름을 사용하세요.

/* 권장 */
:root {
  --background: ...;
  --foreground: ...;
  --primary: ...;
  --muted: ...;
}

/* 비권장 */
:root {
  --white: ...;
  --black: ...;
  --gray-900: ...;
}

의미 중심의 이름을 사용하면 테마를 전환할 때 더 직관적이며 나중에 새 테마를 추가하기도 쉽습니다.


마무리

결국 핵심은 한 문장으로 정리됩니다. class 전략은 단순하고 성숙해 대부분의 프로젝트에 적합하며, data-theme 전략은 의미가 명확해 여러 테마와 CSS 변수를 깊이 결합하는 경우에 더 적합합니다.

Tailwind v4의 @custom-variant 지시어 덕분에 두 방식의 설정이 모두 간결하고 직관적으로 바뀌었습니다. 어떤 방식을 선택할지는 결국 요구사항에 달려 있습니다. shadcn/ui를 쓴다면 data-theme 방식이 더 자연스럽고, 간단한 다크 모드 전환만 필요하다면 class 전략도 여전히 믿을 만한 선택입니다.

한 가지 세부 사항은 놓치지 마세요. 프레임워크와 통합할 때 Astro의 View Transitions, Next.js의 SSR hydration 같은 함정을 제대로 처리해야 합니다. 이런 부분이 빠지면 사용자 경험이 나빠집니다.



참고 자료

FAQ

Tailwind v4의 @custom-variant는 v3 설정과 무엇이 다른가요?
가장 큰 차이는 설정 위치입니다. v3에서는 JS 설정 파일(tailwind.config.js)에 정의하고, v4에서는 CSS 파일에서 @custom-variant 지시어로 선언합니다. 기능은 완전히 같으며, v4 방식이 'CSS-first' 설계 철학에 더 잘 맞습니다.
class와 data-theme을 동시에 사용할 수 있나요?
가능하지만 그럴 필요는 없습니다. 두 방식의 기능은 완전히 같고, 동시에 사용하면 복잡성만 늘어납니다. shadcn/ui가 .dark 클래스와 [data-theme="dark"] 속성을 모두 지원하는 이유는 서로 다른 사용자의 선택 방식을 호환하기 위해서입니다. 둘 중 하나만 선택해 사용하면 됩니다.
dark: 수정자가 너무 많아 코드가 장황해지면 어떻게 하나요?
CSS 변수 방식을 사용하세요. 변수를 정의한 뒤 속성값만 바꾸면 해당 변수를 사용하는 모든 스타일이 자동으로 갱신되므로 각 요소에 dark: 수정자를 쓸 필요가 없습니다.

구체적인 방법은 다음과 같습니다.
1. globals.css에서 @theme으로 변수를 정의합니다.
2. 각 [data-theme]에서 변수 값을 재정의합니다.
3. tailwind.config.js에서 해당 변수를 참조합니다.

이렇게 하면 bg-primary가 테마 전환에 자동으로 대응합니다.
Astro 프로젝트에서 다크 모드 상태가 사라지면 어떻게 하나요?
Astro의 View Transitions는 페이지 전환 시 DOM을 다시 렌더링하므로 다크 모드 상태가 사라질 수 있습니다. astro:after-swap 이벤트를 수신해 테마를 다시 설정하면 해결됩니다.

document.addEventListener('astro:after-swap', () => {
const theme = localStorage.getItem('theme');
if (theme === 'dark') {
document.documentElement.classList.add('dark');
}
});

많은 개발자가 놓치기 쉬운 단계입니다.
페이지를 불러올 때 흰 화면이 깜박이는 문제는 어떻게 해결하나요?
&lt;head&gt; 안에 동기 실행 스크립트를 넣어 DOM이 렌더링되기 전에 테마를 설정하세요.

&lt;script&gt;
if (localStorage.theme === 'dark' ||
(!('theme' in localStorage) &&
window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
&lt;/script&gt;

주의: 스크립트는 반드시 동기식이어야 하며 defer나 async를 사용하면 안 됩니다.

4분 읽기 · 게시일: 2026년 3월 28일 · 수정일: 2026년 9월 4일

댓글

GitHub로 로그인하여 댓글을 남기세요

Easton BlogEaston Blog