블로그 단일 테마 디자인, Tailwind v4만으로 끝내기
2026.04.22
모드를 두 개 만들지 않기로 한 결정
이 블로그는 테마가 하나다. 토글 버튼이 없고, prefers-color-scheme도 안 본다. 언제 열어도 같은 화면이다.
이 결정에 망설임이 있었다. 일반적으로는 라이트/다크 둘 다 지원하는 게 친절하다. 그래도 하나만 고른 이유가 셋 있다.
- 두 모드를 둘 다 잘 만드는 비용이 크다 — 색상 두 세트, 컴포넌트 두 세트를 매번 같이 검증해야 한다
- 개인 블로그라서 시각적 정체성이 중요하다 — 한 화면으로 고정되면 인상이 단단해진다
- 깨질 자리가 절반으로 준다 — 모드가 하나면 «저쪽 모드에서만 안 보이는» 버그가 아예 생기지 않는다
고른 쪽은 밝은 화면이다. 개발 블로그라 어두운 쪽이 자연스러워 보이지만, 이 사이트는 처음 온 사람이 몇 분 읽고 나가는 곳이다. 내가 몇 시간씩 들여다보는 터미널과는 용도가 다르다. 터미널은 눈이 덜 피로한 쪽으로 골랐고 사이트는 글자가 잘 읽히는 쪽으로 골랐다.
이 글은 그 단일 테마 사이트를 Tailwind v4로 어떻게 짰는지에 대한 정리다.
Tailwind v4의 @theme
Tailwind v4부터는 tailwind.config.ts가 사라졌다. 대신 CSS 파일에서 @theme로 직접 테마를 정의한다.
지금 이 사이트의 globals.css 첫머리가 통째로 이렇다.
/* src/app/globals.css */
@import "tailwindcss";
@theme inline {
--color-bg: #ffffff;
--color-card-bg: #ffffff;
--color-border: #e5e7eb;
--color-text-primary: #23272f;
--color-text-secondary: #4b5563;
--color-text-muted: #9ca3af;
--color-tag-bg: #f3f4f6;
--color-accent: #2563eb;
--color-accent-soft: #eff6ff;
--font-sans: "Pretendard", system-ui, sans-serif;
--font-display: "Outfit", "Pretendard", system-ui, sans-serif;
}@theme 안의 변수가 곧 Tailwind 토큰이 된다. --color-bg라고 적으면 bg-bg, text-bg 같은 유틸리티가 자동으로 생긴다.
<div className="bg-bg text-text-primary">
...
</div>@theme과 @theme inline의 차이가 처음에 헷갈렸다. 값에 다른 CSS 변수를 참조할 계획이면 inline이고, 색상 값을 직접 박을 거면 일반 @theme으로도 된다. 여기서 inline을 쓴 건 나중에 모드를 하나 더 붙일 여지를 남겨두려던 것인데, 결국 안 붙였다.
색상 시스템 설계
색은 배경 계열과 글자 계열을 각각 몇 단계로 나눌지부터 정한다. 여기서는 이렇게 갈랐다.
배경
--color-bg(#ffffff) — 페이지 배경--color-card-bg(#ffffff) — 카드, 코드 블록 배경--color-border(#e5e7eb) — 경계선--color-tag-bg(#f3f4f6) — 태그 칩 같은 작은 면
밝은 테마에서는 배경 단계를 늘리는 대신 경계선으로 구분한다. 배경 자체는 흰색 하나로 두고 카드와 페이지를 테두리로 나눈다. 어두운 테마와 반대되는 지점이다 — 어두운 쪽은 면의 밝기 차이로 층을 만드는 편이 자연스럽다.
텍스트 3단계
--color-text-primary(#23272f) — 본문, 제목--color-text-secondary(#4b5563) — 설명, 목록 본문--color-text-muted(#9ca3af) — 날짜, 부가 정보
순수 검정(#000)은 흰 배경에서 대비가 지나치게 강하다. 살짝 푸른 기가 도는 진회색이 덜 딱딱하다.
강조 색상
--color-accent(#2563eb) — 링크, 버튼, 강조--color-accent-soft(#eff6ff) — 강조 색의 옅은 배경
강조 색은 하나로 제한했다. 배경이 밝으면 색이 어두운 배경에서만큼 튀지 않아서, 여러 색을 쓰면 오히려 흐릿해진다.
색상 대비 검증
색을 정했으면 WCAG 대비 비율을 실제로 계산해 본다. 본문 텍스트의 최소 기준은 4.5:1이다.
지금 값으로 재보면 이렇다.
#23272fon#ffffff= 15.0:1 (충분)#4b5563on#ffffff= 7.6:1 (충분)#2563ebon#ffffff= 5.2:1 (통과)#9ca3afon#ffffff= 2.5:1 (기준 미달)
마지막 줄은 그냥 넘기면 안 되는 항목이라 적어둔다. muted 색은 본문에 안 쓰고 날짜나 부가 정보에만 쓰지만, 2.5:1은 큰 글자 기준인 3:1도 못 넘는다. 밝은 배경에서 회색을 옅게 잡으면 이렇게 되기 쉽다. 어두운 배경에서는 같은 회색이 오히려 대비가 나온다.
WebAIM Contrast Checker로 확인할 수 있고, 계산식이 간단해서 스크립트로 한 번에 훑어도 된다. 색을 고를 때가 아니라 고르고 난 뒤에 재봐야 이런 게 나온다.
코드 블록 색상
개발 블로그면 화면의 절반이 코드 블록이다. 코드 색이 본문과 어우러져야 한다.
rehype-pretty-code + shiki 조합에서 테마를 지정한다. 밝은 테마에는 github-light가 무난하다.
// MDX rehype 설정에서
[rehypePrettyCode, { theme: "github-light" }]여기서 실수하기 쉬운 게 있다. 사이트 테마와 코드 테마는 따로 논다. 사이트를 밝게 만들어 놓고 코드 테마만 어두운 걸 그대로 두면, 글 중간중간에 검은 덩어리가 박힌 화면이 된다. 둘을 맞추는 걸 잊기 쉽다.
코드 블록은 배경보다 테두리로 분리했다.
pre {
background-color: var(--color-card-bg);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
padding: 1rem;
overflow-x: auto;
}폰트와 word-break
한국어 사이트에서 가독성을 결정하는 또 다른 요소가 폰트와 줄바꿈이다.
폰트는 두 가지를 쓴다. 본문은 Pretendard, 제목은 Outfit이다. 제목용 폰트를 따로 두면 한글 본문과 영문 제목의 인상이 갈려서 화면에 리듬이 생긴다.
@theme inline {
--font-sans: "Pretendard", system-ui, sans-serif;
--font-display: "Outfit", "Pretendard", system-ui, sans-serif;
}word-break: keep-all은 한국어에서 음절 단위로 끊기는 어색한 줄바꿈을 막아준다. 다만 body에 통째로 걸지 않았다.
h1, h2, h3, h4, h5, h6 {
word-break: keep-all;
text-wrap: balance;
}
p, li, blockquote {
word-break: keep-all;
}글자가 들어가는 요소에만 건 이유는 코드 블록 때문이다. body에 걸면 코드까지 단어 단위로 끊으려 들어서, 긴 명령어가 이상한 자리에서 접힌다.
제목에는 text-wrap: balance를 같이 걸었다. 두 줄짜리 제목의 아랫줄이 한 단어만 남는 걸 막아준다. 이 한 줄이 생각보다 크게 바뀐다.
prose 플러그인을 안 쓴 이유
마크다운 본문 스타일은 보통 @tailwindcss/typography의 prose 클래스로 처리한다. 처음엔 그렇게 하려다 결국 안 썼다.
대신 MDX 컴포넌트 매핑에서 태그별로 직접 스타일을 지정한다.
const components: MDXComponents = {
h2: (props) => (
<h2 className="mt-12 mb-4 font-display text-xl font-bold ..." {...props} />
),
p: (props) => (
<p className="my-4 max-w-[65ch] leading-[1.8] text-text-secondary" {...props} />
),
// ...
};prose를 안 쓴 이유는 하나다. 이 블로그의 MDX에는 표 대신 직접 만든 시각화 컴포넌트가 들어간다. prose는 자기 안의 모든 요소에 스타일을 씌우려 들어서, 커스텀 컴포넌트가 들어오면 예상 못 한 여백과 색이 끼어든다. 그걸 하나씩 무력화하는 것보다 처음부터 필요한 태그만 지정하는 쪽이 짧았다.
본문이 순수 마크다운이면 prose가 훨씬 빠르다. 컴포넌트를 섞기 시작하면 갈린다.
단일 테마라 안 해도 되는 것들
두 모드를 다 만들 때 신경 써야 하는 것들이 통째로 빠진다.
dark:variant를 안 쓴다- 색상 토큰을 두 세트로 관리하지 않는다
- 토글 버튼, localStorage 동기화, 새로고침 때 잠깐 다른 색이 번쩍이는 문제를 안 잡아도 된다
prefers-color-scheme미디어 쿼리를 안 쓴다
대신 배경색은 CSS에서 직접 못 박아 둔다.
body {
background-color: var(--color-bg);
color: var(--color-text-primary);
}흔한 함정
함정 1: 회색을 너무 옅게 잡는다
밝은 배경에서 옅은 회색은 대비가 급격히 떨어진다. 위에서 본 2.5:1이 그 예다. 어두운 테마에서 쓰던 회색 감각을 그대로 가져오면 반드시 걸린다.
함정 2: 코드 테마를 안 맞춘다
사이트만 바꾸고 rehype-pretty-code의 테마를 그대로 두면 글마다 검은 덩어리가 박힌다.
함정 3: 회색 단계를 너무 많이 만든다
배경과 글자를 합쳐 예닐곱 단계가 한계다. 늘릴수록 서로 구분이 안 되고, 새 컴포넌트를 만들 때 어느 단계를 써야 할지 매번 고민하게 된다.
함정 4: 색만으로 구분한다
강조를 빨강과 초록으로만 하면 색각 이상이 있는 사람에게는 같은 색이다. 색 말고 아이콘이나 글자로도 구분한다. 시각화 컴포넌트 글에서도 다뤘다.
함정 5: 이미지 배경을 신경 안 쓴다
글에 이미지를 넣을 때 배경이 사이트와 다르면 사각형 덩어리가 그대로 보인다. 이 블로그는 이미지에 테두리와 둥근 모서리를 둘러서 «면»으로 보이게 처리했다.
직접 겪은 일: 이 글이 사이트와 어긋나 있었다
솔직하게 적어둔다. 이 글은 원래 다크모드 기준으로 쓰여 있었다. 배경이 #0a0a0a고, 글자가 #ededed고, 코드 테마는 github-dark고, prose-invert를 쓴다고 적혀 있었다.
그런데 이 사이트는 그때도 밝은 화면이었다. git 이력을 뒤져보니 팔레트를 밝은 쪽으로 잡은 커밋이 4월 2일이고, 이 글은 4월 22일에 올라갔다. 검은 배경 값이 들어간 적은 한 번도 없다.
글을 쓸 당시에 무슨 생각이었는지는 이제 정확히 기억나지 않는다. 다만 이런 어긋남이 생기는 구조는 짐작이 간다. 디자인 글은 코드를 보고 쓰는 게 아니라 머릿속 그림을 보고 쓰기 쉽다. 개발 블로그면 어두운 게 어울린다는 인상이 먼저 있었고, 그 인상 위에 글을 얹은 것이다.
스무 날 동안 아무도 이상하다고 말해주지 않았고, 나도 다시 안 읽었다. 자기 글에서 이런 걸 발견하는 방법은 하나뿐인 것 같다 — 글에 적힌 값을 실제 파일에서 다시 찾아보는 것. 이 글을 고치면서 전부 그렇게 다시 확인했고, 그 과정에서 muted 색이 기준 미달이라는 것도 같이 나왔다.
정리
단일 테마 블로그를 만든다면 Tailwind v4의 @theme이 답이다. CSS 변수와 Tailwind 유틸리티가 한 자리에서 관리되니 색상 시스템이 단순해진다.
핵심 원칙 다섯 개:
- 배경과 글자를 합쳐 예닐곱 단계 안에서 끝낸다
- 순수 흑백 대신 살짝 색을 섞는다
- 대비를 실제로 계산해서 확인한다 — 눈으로 보고 판단하지 않는다
- 코드 테마를 사이트 테마와 맞춘다
- 한국어는
word-break: keep-all을 글자 요소에만 건다
라이트/다크 둘 다 지원하는 게 정답이라는 통념이 있지만, 단일 모드가 명확한 정체성을 줄 때도 있다. 두 모드를 적당히 만드는 것보다 한 모드를 단단하게 만드는 게 깔끔할 때가 있다.
다만 하나 더 배웠다. 한 모드를 골랐으면 글도 그 모드로 써야 한다. 그러지 않으면 이 글처럼 스무 날 동안 자기 사이트와 반대되는 이야기를 하고 있게 된다.
같이 읽으면 좋은 글
추첨기에 귀여운 걸 붙였더니 물리가 거짓말을 했다
코인 밀기 추첨기에 캐릭터 스킨을 붙였다. 토끼 귀를 그리는 순간 "닿지도 않았는데 밀렸다"가 보이기 시작했고, 회전각에 표정을 걸었더니 눈이 지글거렸다. 겉모습만 바꾸는 작업에서 지켜야 했던 네 가지.
게임캔버스디자인2026.07.30MDX에서 표 대신 React 컴포넌트로 시각화하기: 설계와 패턴
마크다운 표는 모바일에서 깨진다. MDX의 진짜 가치는 표를 시각화 컴포넌트로 대체하는 데 있다. CompareCard, PlanCard, FlowCard 같은 시각화 컴포넌트의 설계 원칙을 정리한다.
MDX디자인React2026.04.22