CSSTailwindCSSNext.js다크모드

저녁만 되면 화면이 깨진다? prefers-color-scheme와 Tailwind v4 레이어 함정

June 24, 20261 min read

어느 날 저녁, 내 서비스를 켰는데 화면이 이상했다. 분명 낮에는 멀쩡한 하얀 배경이었는데, 배경만 시커멓게 깔리고 글씨랑 카드는 라이트 모드 그대로라 어딘가 깨진 듯한 모습이었다. 다크모드를 만든 적도 없는데 말이다.

더 이상한 건 **"저녁이 되면 그런 것 같다"**는 패턴이었다. 이 단서가 결국 범인을 잡는 결정적 힌트가 됐다. 이 글은 "다크모드를 만든 적도 없는데 다크모드가 깨지는" 황당한 버그를 추적한 기록이다.

증상: 만든 적 없는 다크모드

상황을 정리하면 이랬다.

  • 낮에는 정상 (하얀 배경, 검은 글씨)
  • 저녁에는 배경만 검정, 콘텐츠는 라이트용 그대로 → 가독성 엉망
  • 코드에 다크모드 토글이나 dark: 클래스를 단 한 번도 쓴 적 없음

"저녁"이라는 키워드에서 바로 떠오른 게 있었다. macOS의 자동 외관 전환이다. 시스템 설정에서 외관을 "자동"으로 두면, 일몰 후 OS가 알아서 다크모드로 바뀐다. 그리고 브라우저는 이 OS 설정을 CSS에 그대로 전달한다 — 바로 prefers-color-scheme 미디어 쿼리로.

원인 1: create-next-app이 심어둔 다크모드 잔재

globals.css를 열어보니 범인이 있었다. Next.js 프로젝트를 처음 만들 때 자동 생성되는 기본 템플릿에 이런 코드가 들어있었다.

:root {
  --background: #ffffff;
  --foreground: #171717;
}

/* ← 이 블록이 범인 */
@media (prefers-color-scheme: dark) {
  :root {
    --background: #0a0a0a;   /* 거의 검정 */
    --foreground: #ededed;   /* 거의 흰색 */
  }
}

body {
  background: var(--background);
  color: var(--foreground);
}

create-next-app으로 프로젝트를 시작하면 이 다크모드 미디어 쿼리가 기본으로 깔려 있다. OS가 다크모드가 되면 --background#0a0a0a로 바뀌고, body 배경이 검정이 된다.

"아, 그럼 이거 지우면 되겠네"라고 생각했는데, 한 가지 더 의문이 남았다. 내 body에는 분명 Tailwind로 밝은 배경을 줬는데?

<body className="min-h-full flex flex-col bg-zinc-50 text-zinc-900">

bg-zinc-50이면 밝은 회색 배경이다. 그런데 왜 globals.css의 검정 배경이 이걸 이기는 걸까? 클래스 선택자(.bg-zinc-50)가 요소 선택자(body)보다 우선순위가 높은데도?

원인 2: Tailwind v4의 레이어 우선순위 (진짜 함정)

여기가 이번 버그의 핵심이자, 모르면 평생 헷갈릴 부분이다.

Tailwind CSS v4는 @import "tailwindcss"로 불러오는데, 이때 모든 유틸리티 클래스(bg-zinc-50 등)는 CSS @layer 안에 들어간다. 반면 내가 globals.css에 직접 쓴 body { background: ... }는 **레이어 밖(unlayered)**에 있다.

그리고 CSS 캐스케이드 규칙에 이런 게 있다:

레이어에 속하지 않은 스타일은, 레이어 안의 스타일을 항상 이긴다. (선택자 우선순위와 무관하게)

즉:

규칙위치우선순위
body { background: var(--background) }레이어 밖이김 👑
.bg-zinc-50 { background: ... }@layer utilities

선택자만 보면 .bg-zinc-50(클래스)이 body(요소)를 이겨야 정상이다. 하지만 레이어 규칙이 선택자 우선순위보다 먼저 적용되기 때문에, 레이어 밖의 body 규칙이 이긴다.

결론적으로:

  1. 저녁 → OS 다크모드 → --background#0a0a0a
  2. body { background: var(--background) }(레이어 밖)가 bg-zinc-50(레이어 안)을 이김
  3. 배경만 검정, 나머지 라이트 → 깨진 화면

두 가지 원인이 맞물려서 생긴 버그였다.

해결: 라이트 전용으로 고정하기

이 앱은 애초에 라이트 전용 디자인이다(모든 컴포넌트가 밝은 색 기준). 그러니 다크모드를 어설프게 지원하려 하지 말고, 아예 라이트로 고정하는 게 맞는 처방이다.

globals.css에서 다크 미디어 쿼리를 제거하고, color-scheme: light를 추가했다.

:root {
  --background: #ffffff;
  --foreground: #171717;
  /* OS 다크모드(저녁 자동 전환)에도 폼/스크롤바까지 라이트 고정 */
  color-scheme: light;
}

/* @media (prefers-color-scheme: dark) { ... } ← 통째로 삭제 */

body {
  background: var(--background);  /* 이제 항상 #ffffff */
  color: var(--foreground);
}

핵심 두 가지:

  1. @media (prefers-color-scheme: dark) 블록 삭제 → 이제 OS가 다크모드여도 --background는 항상 흰색
  2. color-scheme: light 추가 → 배경뿐 아니라 브라우저 네이티브 UI(인풋 박스, 셀렉트, 스크롤바 등)까지 라이트로 강제. 이걸 안 하면 폼 컨트롤이 OS 다크 스타일로 렌더링돼서 또 어색해진다.

배포 후 저녁에 다시 확인하니 멀쩡한 라이트 화면이 유지됐다.

트러블슈팅

  • 배포했는데 안 바뀐다 → 브라우저 CSS 캐시 때문이다. 강력 새로고침(Mac은 Cmd+Shift+R)으로 해결.
  • 인풋/드롭다운만 여전히 어둡다color-scheme: light를 빠뜨린 경우다. 배경색만 고치면 네이티브 폼 컨트롤은 여전히 OS 다크를 따른다.
  • 재현이 안 된다 → 본인 OS를 다크모드로 직접 바꿔서 테스트하면 된다. macOS는 시스템 설정 → 디스플레이 → 외관을 "다크"로. 굳이 저녁까지 기다릴 필요 없다.

정리

"만든 적도 없는 다크모드가 깨진다"의 전말:

  1. **create-next-app 기본 globals.css**에 prefers-color-scheme: dark 미디어 쿼리가 숨어 있었다
  2. 저녁에 OS가 자동 다크모드로 바뀌며 이게 발동
  3. Tailwind v4 레이어 규칙 때문에, 레이어 밖의 body 배경이 bg-zinc-50을 이겨버림
  4. 해결: 다크 미디어 쿼리 삭제 + color-scheme: light 고정

두 가지 교훈을 얻었다. 첫째, 스타터 템플릿이 깔아둔 기본 코드를 한 번은 들여다보자. 내가 안 쓴 코드가 버그를 일으킬 수 있다. 둘째, Tailwind v4에서는 globals.css에 직접 쓴 요소 스타일이 유틸리티 클래스를 이긴다. 레이어 개념을 모르면 "분명 클래스를 줬는데 왜 안 먹지?"로 한참 헤맬 수 있다.

참고: 만약 진짜 다크모드를 지원하고 싶다면 이건 별도의 큰 작업이다. 모든 컴포넌트에 dark: 변형 클래스를 입히고 토글 UI를 다는 일이라, 라이트 전용으로 고정하는 것과는 차원이 다르다. 필요해지면 그때 제대로 하자.

PM

backtodev

40대 PM, 다시 개발자로 돌아갑니다. 실패하고 배우며 성장하는 기록.

저녁만 되면 화면이 깨진다? prefers-color-scheme와 Tailwind v4 레이어 함정 | backtodev