기술블로그 UX: 코드가 읽히는 블로그 만들기

이전 에피소드에서 Docker 인프라를 정비했습니다. 서버가 안정되었으니, 이제 블로그답게 보이게 만들 차례입니다.

기술블로그의 UX는 일반 블로그와 다릅니다. 코드 블록이 편하게 읽혀야 하고, 한국어·영어·코드가 한 문장에 섞여도 자연스러워야 합니다. 이 글은 yndl.dev의 CSS를 한 줄씩 쌓아가면서 배운 것들입니다.


한국어 기술블로그의 타이포그래피 과제

AI UI/UX 에이전트가 보고서에서 이런 분석을 했습니다.

“한국어 기술블로그는 세 가지 글자 체계가 혼재한다:
1. 한글 본문 — 설명, 개념 서술
2. 영문 기술 용어 — API, JavaScript, Docker 등
3. 코드 블록 — 순수 영문 + 기호”

이 세 가지가 한 문장에 섞입니다. “WordPress의 wp-config.php에서 FORCE_SSL_ADMIN을 true로 설정하면” 같은 문장에 한글, 영문 고유명사, 인라인 코드가 모두 들어갑니다.

폰트 하나로 세 가지를 다 커버할 수 없습니다. 그래서 역할별 폰트 분리가 필요합니다.


폰트 선택: Pretendard + Fira Code

본문: Pretendard Variable

body {
  font-family: "Pretendard Variable", "Pretendard",
    -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  letter-spacing: -0.02em;
}

Pretendard를 선택한 이유:

  1. 한글 + 영문 균형 — 한글과 영문의 x-height가 맞춰져 있어서 혼용해도 어색하지 않음
  2. Variable Font — 하나의 파일로 모든 굵기(100~900)를 커버. 네트워크 요청 최소화
  3. 검증된 조합 — toss.tech, velog 등 국내 기술블로그에서 이미 표준처럼 사용
  4. CDN 서브셋 — dynamic-subset 버전은 한글 유니코드를 분할 로드해서 초기 로딩이 빠름

letter-spacing: -0.02em은 한글에서 중요합니다. 기본 자간은 한글이 영문보다 넓어 보이는데, 미세한 마이너스 자간으로 시각적 밀도를 맞춥니다.

코드: Fira Code

pre, code, kbd, samp {
  font-family: "Fira Code", "JetBrains Mono",
    "Cascadia Code", monospace;
}

Fira Code의 핵심은 리거처(ligature)입니다. =>, !==, >= 같은 연산자가 하나의 글리프로 합쳐져 가독성이 높아집니다. VS Code에서 익숙한 패턴이라 개발자 독자에게 친숙합니다.


코드 블록: “항상 다크”

.wp-block-code, pre {
  background: #1e1e2e;
  color: #cdd6f4;
  border-radius: 8px;
  padding: 1.25rem 1.5rem;
  font-size: 0.9rem;
  line-height: 1.7;
  overflow-x: auto;
}

AI가 권장한 원칙 중 하나: 코드 블록은 라이트 모드에서도 항상 다크 배경. VS Code, GitHub, Stack Overflow에서 개발자들이 이미 익숙한 패턴입니다.

#1e1e2e은 Catppuccin Mocha 테마의 베이스 컬러입니다. Prismatic(Prism.js)의 Tomorrow Night 테마와도 어울립니다.

line-height: 1.7은 코드의 줄 간격입니다. 본문(1.8)보다 약간 좁게 잡았습니다. 코드는 밀도가 높아야 구조가 보이기 때문입니다. 하지만 1.5 이하로 줄이면 줄이 붙어 보여 오히려 가독성이 떨어집니다.

인라인 코드

:not(pre) > code {
  font-family: "Fira Code", monospace;
  font-size: 0.85em;
  background-color: #f3f4f6;
  color: #dc2626;
  padding: 0.2em 0.45em;
  border-radius: 4px;
  word-break: break-word;
}

인라인 코드는 코드 블록과 반대로 밝은 배경 + 빨간 텍스트입니다. 본문 속에서 “이건 코드야”라고 시각적으로 구분해주는 역할입니다. GitHub의 인라인 코드 스타일과 유사합니다.

font-size: 0.85em으로 본문보다 약간 작게 합니다. 모노스페이스 폰트는 같은 크기에서 산세리프보다 넓어 보이기 때문에, 줄여야 본문과 어울립니다.


콘텐츠 폭과 여백의 설계

720px 제한

.site-content .entry-content {
  max-width: 720px;
  margin-left: auto;
  margin-right: auto;
}

AI 보고서가 권장한 720px. 이유는 한 줄에 45~75자라는 타이포그래피 원칙입니다. 한글은 영문보다 글자당 폭이 넓어서, 720px에서 대략 35~45자가 한 줄에 들어갑니다. 한글 기술문서에 적합한 범위입니다.

다만 코드 블록은 800px까지 허용합니다. 코드는 가로로 길어지는 경향이 있고, 80컬럼(코딩 컨벤션)을 표시하려면 720px로는 부족합니다.

헤딩 간격

.entry-content h2 {
  margin-top: 3rem;
  margin-bottom: 1.25rem;
  padding-top: 1rem;
}
.entry-content h3 {
  margin-top: 2.25rem;
  margin-bottom: 1rem;
}

h2 위에 3rem + 1rem padding으로 섹션 구분을 시각적으로 강조합니다. 기술 글은 보통 길기 때문에, 섹션 간 공간이 충분해야 스크롤하면서 “지금 어디를 읽고 있는지” 파악할 수 있습니다.


모바일 대응: 코드 블록의 풀폭 전략

이게 가장 고민이 많았던 부분입니다.

문제: 모바일에서 코드 블록이 좁다

데스크톱에서는 720px 안에 코드 블록이 잘 들어갑니다. 하지만 모바일(375px 기준)에서 article 패딩(좌우 20px씩)을 빼면 실제 코드 영역이 335px밖에 안 됩니다. docker-compose.yml의 들여쓰기된 YAML을 표시하기에 턱없이 부족합니다.

해결: Negative Margin으로 풀폭 코드 블록

@media (max-width: 768px) {
  .wp-block-code, pre {
    border-radius: 0;
  }
  .entry-content .wp-block-code,
  .entry-content pre {
    margin-left: -20px !important;
    margin-right: -20px !important;
    max-width: none !important;
  }
}

margin-left: -20px으로 article 패딩만큼 바깥으로 밀어냅니다. 코드 블록이 화면 전체 폭을 사용하게 됩니다.

border-radius: 0도 중요합니다. 풀폭 코드 블록에 둥근 모서리가 있으면 양쪽 끝에 흰색 삼각형이 보여서 어색합니다. 화면 끝까지 딱 붙으면 모서리를 없애야 자연스럽습니다.


다크 모드: FOUC와의 전쟁

다크 모드 구현의 가장 큰 적은 FOUC(Flash of Unstyled Content)입니다.

FOUC란?

사용자가 다크 모드를 설정하고 페이지를 새로고침하면:

  1. 브라우저가 HTML을 로드 (배경: 흰색)
  2. CSS를 로드 (아직 다크 모드 아님)
  3. JavaScript를 실행 (localStorage에서 다크 모드 설정 읽기)
  4. 클래스를 추가 (이제 다크 모드!)

1~3 사이에 흰 화면이 번쩍 합니다. 이게 FOUC입니다. 밤에 화면을 보는 사용자에게는 눈이 아프고, UX 품질이 떨어져 보입니다.

해결: <head>에서 동기적으로 실행

(function(){
  var t = localStorage.getItem('theme');
  if (t === 'dark' || (t !== 'light'
    && window.matchMedia('(prefers-color-scheme:dark)').matches)) {
    document.documentElement.classList.add('dark-mode')
  }
})();

이 스크립트는 wp_head의 우선순위 1(가장 먼저)로 실행됩니다. <head> 안에 인라인으로 들어가기 때문에 외부 JS 파일 로드를 기다리지 않습니다.

렌더링이 시작되기 전에 html 요소에 dark-mode 클래스가 추가되므로, 사용자는 흰 화면을 볼 일이 없습니다.

트랜지션: 첫 인터랙션 후에만

html.dark-mode-ready,
html.dark-mode-ready *,
html.dark-mode-ready *::before,
html.dark-mode-ready *::after {
  transition: background-color 0.2s ease, color 0.2s ease,
    border-color 0.2s ease, box-shadow 0.2s ease;
}

dark-mode-ready 클래스는 페이지 로드 시에는 없습니다. 사용자가 토글 버튼을 처음 클릭할 때 추가됩니다. 왜 이렇게 했는가? 페이지 로드 시에 transition이 있으면, 다크 모드 CSS가 적용되면서 모든 요소가 0.2초 동안 애니메이션됩니다. FOUC를 막은 의미가 없어집니다.

“토글 버튼을 누를 때”만 트랜지션이 필요합니다. 페이지 로드 시에는 즉시(instant) 적용되어야 합니다.


GeneratePress와의 CSS 특이성 전투

다크 모드의 가장 어려운 부분은 CSS 작성이 아니라 기존 CSS를 이기는 것이었습니다.

문제: GeneratePress의 동적 CSS

GeneratePress는 Customizer 설정에 따라 인라인 CSS를 동적으로 생성합니다.

/* GeneratePress가 생성하는 CSS */
body { color: #111111; }
.entry-meta { color: #595959; }
.main-title a { color: #222222; }

이 스타일들은 <head> 안에 인라인으로 삽입되며, 다크 모드 CSS보다 높은 특이성을 가질 수 있습니다.

해결: CSS Custom Properties + !important

html.dark-mode {
  --contrast: #e2e8f0;
  --contrast-2: #94a3b8;
  --base: #1e293b;
  --base-2: #0f172a;
  --accent: #60a5fa;
}

html.dark-mode .entry-content,
html.dark-mode .entry-content p,
html.dark-mode .entry-content strong,
html.dark-mode .entry-content em,
html.dark-mode .entry-content span {
  color: #e2e8f0 !important;
}

!important를 남용하면 유지보수가 어려워진다는 건 알고 있습니다. 하지만 다크 모드에서는 “이 색상은 반드시 이 값이어야 한다”는 의도를 명확히 하는 것이므로, 합리적인 사용입니다.

가장 까다로운 케이스: 인라인 스타일

WordPress 블록 에디터는 인라인 style 속성을 자주 씁니다. 인라인 스타일은 CSS 특이성에서 가장 높은 순위입니다. !important를 써야만 이깁니다.

/* 모든 인라인 color 스타일 오버라이드 */
html.dark-mode .entry-content [style*="color:"] {
  color: #cbd5e1 !important;
}

/* 링크는 예외: 파란색 유지 */
html.dark-mode .entry-content a[style*="color:"] {
  color: #60a5fa !important;
}

[style*="color:"] — “style 속성에 color:가 포함된 모든 요소”를 매칭합니다. 링크는 예외로 파란색을 유지해서 “클릭 가능하다”는 시각적 단서를 보존합니다.


다크 모드에서 코드 블록이 깨지지 않는 이유

여기서 “코드 블록은 항상 다크” 원칙의 보너스 효과가 나옵니다.

라이트/다크 모드를 전환해도 코드 블록은 아무 변화가 없습니다. 이미 다크 배경(#1e1e2e)이니까요. 코드 블록을 위한 다크 모드 CSS를 추가로 작성할 필요가 없습니다.

인라인 코드만 모드에 따라 바꿉니다.

/* 라이트 모드 */
:not(pre) > code {
  background-color: #f3f4f6;
  color: #dc2626;
}

/* 다크 모드 */
html.dark-mode :not(pre) > code {
  background-color: #1e293b;
  color: #f87171;
}

라이트에서는 밝은 회색 배경 + 진한 빨강, 다크에서는 어두운 배경 + 밝은 빨강. 둘 다 인라인 코드가 본문에서 분명히 구분됩니다.


토글 버튼 디자인

.dark-mode-toggle {
  position: fixed;
  bottom: 24px;
  right: 24px;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  z-index: 9999;
}

44px는 Apple의 최소 터치 타겟 가이드라인입니다. 이보다 작으면 손가락으로 누르기 어렵습니다. position: fixed로 스크롤해도 항상 같은 위치에 있습니다. 우하단은 모바일에서 엄지가 자연스럽게 닿는 위치입니다.

아이콘은 이모지입니다. 라이트 모드에서는 🌙(달), 다크 모드에서는 ☀️(해). SVG 아이콘을 별도로 만들 필요 없이, 유니코드 이모지로 모든 플랫폼에서 직관적인 아이콘을 표시합니다.


mu-plugin으로 만든 이유

방법장점단점
Additional CSS간편JS 불가, FOUC 해결 불가
테마 functions.php코드 자유도 높음테마 변경 시 소실
자식 테마테마 업데이트 안전자식 테마 생성 필요
mu-plugin테마 독립적, 항상 활성관리자 UI에서 비활성화 불가

wp_content/mu-plugins/dark-mode.php — mu-plugin(Must-Use Plugin)은 WordPress가 무조건 로드합니다. 활성화/비활성화 과정이 없고, 테마를 바꿔도 유지됩니다.

이 블로그에서는 GeneratePress를 쓰고 있지만, 나중에 테마를 바꿀 수도 있습니다. 다크 모드가 mu-plugin이면 테마 변경에 영향을 받지 않습니다.


현재 CSS 스택 정리

레이어파일/위치역할크기
GeneratePress테마 내장레이아웃, 기본 색상테마 제공
Additional CSSCustomizer타이포그래피, 간격, 코드 블록~5.7KB
dark-mode.phpmu-plugins다크 모드 전체~8KB
Prismatic플러그인코드 구문 강조플러그인 제공

총 커스텀 CSS: ~14KB. 프레임워크(Tailwind, Bootstrap)를 도입하면 수십~수백 KB가 추가되는데, 필요한 스타일만 직접 작성하는 게 퍼포먼스 면에서도 낫습니다.


이 과정에서 배운 것

1. 다크 모드는 “색 반전”이 아니다

처음에는 단순히 배경을 어둡게, 텍스트를 밝게 하면 되는 줄 알았습니다. 실제로는 테이블 hover 색상, 인라인 코드의 배경/텍스트, blockquote의 왼쪽 테두리, 입력 필드의 배경, 스크롤바 색상 — 이 모든 요소를 개별적으로 설정해야 합니다. “밝은 배경에서 어두운 배경으로”가 아니라 “라이트 팔레트에서 다크 팔레트로” 전환하는 것입니다.

2. CSS 특이성은 실전에서 가장 골치 아프다

CSS를 아무리 잘 작성해도, GeneratePress의 동적 CSS와 WordPress 블록의 인라인 스타일이 다 이겨버립니다. !important를 쓰지 않겠다는 원칙은 이론적으로 옳지만, 서드파티 테마/플러그인 위에서 작업할 때는 현실적이지 않습니다.

3. 모바일 코드 블록의 negative margin은 필수다

기술블로그에서 코드 블록은 핵심 콘텐츠입니다. 모바일에서 코드가 잘려 보이면 독자가 이탈합니다. Negative margin으로 풀폭을 만드는 패턴은 Medium, dev.to 등 주요 기술 플랫폼에서도 사용합니다.


다음 에피소드 예고

UX가 갖춰졌으니, 이제 돌아보겠습니다. 다음 에피소드에서는 AI 코드 리뷰 에이전트가 찾아낸 22개 이슈를 하나씩 분석합니다. Critical 7개를 어떻게 수정했는지, 무시한 것은 왜 무시했는지.

예고: Episode 6 — AI 코드 리뷰어가 찾아낸 것들


시리즈 안내: AI 팀과 함께 기술블로그 만들기

회차제목상태
1Agent Teams란? 3인 AI 팀 구성기발행
2AI 3인이 내 블로그를 진단했다발행
3Caddy와 싸운 하루: 리다이렉트 루프 3연전발행
4Docker + WordPress + Redis: AI가 설계한 인프라 적용기발행
5기술블로그 UX: 코드가 읽히는 블로그현재 글
6AI 코드 리뷰어가 찾아낸 22개 이슈발행
7마크다운으로 글 쓰고 자동 발행하기예정
8AI 팀 vs 혼자 코딩: 비용 대비 효과 분석예정

이 글은 Claude Code를 사용하여 작성되었습니다.
프로젝트: yndl.dev

댓글 남기기