<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Design on kenji.blog</title><link>http://kenji.blog/ko/categories/design/</link><description>Recent content in Design on kenji.blog</description><generator>Hugo -- gohugo.io</generator><language>ko</language><copyright>kenjinote</copyright><lastBuildDate>Sun, 13 Sep 2026 03:00:00 +0900</lastBuildDate><atom:link href="http://kenji.blog/ko/categories/design/index.xml" rel="self" type="application/rss+xml"/><item><title>Hugo 테마 커스터마이징: Tailwind CSS 도입 방법</title><link>http://kenji.blog/ko/p/hugo-tailwind-css-customization-guide/</link><pubDate>Sun, 13 Sep 2026 03:00:00 +0900</pubDate><guid>http://kenji.blog/ko/p/hugo-tailwind-css-customization-guide/</guid><description>&lt;img src="http://kenji.blog/p/hugo-tailwind-css-customization-guide/img/eyecatch.jpg" alt="Featured image of post Hugo 테마 커스터마이징: Tailwind CSS 도입 방법" />&lt;h1 id="들어가며-정적-사이트-생성기-hugo와-tailwind-css의-강력한-시너지">들어가며: 정적 사이트 생성기 Hugo와 Tailwind CSS의 강력한 시너지
&lt;/h1>&lt;p>현대 웹 프론트엔드 개발에서 성능과 개발자 경험(DX: Developer Experience)의 양립은 모든 프로젝트에서 가장 중요한 과제 중 하나입니다. 정적 사이트 생성기(SSG) 중에서도 세계 최고 수준의 빌드 속도를 자랑하는 &lt;strong>Hugo&lt;/strong>와, 유틸리티 퍼스트라는 혁신적인 패러다임을 도입한 &lt;strong>Tailwind CSS&lt;/strong>를 결합하는 것은 이 과제에 대한 하나의 궁극적인 해답이라고 할 수 있습니다.&lt;/p>
&lt;p>Hugo는 Go 언어로 작성되어 있어, 수천 페이지의 사이트라도 단 몇 초 혹은 밀리초 단위로 빌드를 완료하는 경이로운 성능을 가지고 있습니다. 한편, Tailwind CSS는 사전에 정의된 무수히 많은 유틸리티 클래스(&lt;code>flex&lt;/code>, &lt;code>text-center&lt;/code>, &lt;code>mt-4&lt;/code> 등)를 HTML에 직접 작성함으로써, CSS 파일과 HTML 파일 사이를 오가는 컨텍스트 스위칭을 없애고 디자인 이터레이션을 가속화합니다.&lt;/p>
&lt;p>본 기사에서는 Hugo 테마에 Tailwind CSS를 도입하고, 나아가 PostCSS를 이용한 고도화된 에셋 파이프라인(Hugo Pipes)을 구축하는 절차를 아키텍처의 근간부터 수학적인 성능 최적화 관점에 이르기까지 철저하고 상세하게 해설합니다.&lt;/p>
&lt;hr>
&lt;h2 id="1-유틸리티-퍼스트-css와-컴포넌트-지향의-변천">1. 유틸리티 퍼스트 CSS와 컴포넌트 지향의 변천
&lt;/h2>&lt;p>Tailwind CSS의 도입 절차에 들어가기 전에, 왜 우리가 Tailwind CSS를 사용해야 하는지, 그 배경에 있는 CSS 설계 사상의 역사와 진화에 대해 깊이 이해해 두는 것은 매우 유익합니다.&lt;/p>
&lt;h3 id="기존-css-설계bem이나-oocss의-한계">기존 CSS 설계(BEM이나 OOCSS)의 한계
&lt;/h3>&lt;p>과거의 웹 개발에서는 시맨틱한 클래스명을 부여하는 것이 베스트 프랙티스로 여겨졌습니다. 예를 들어, 카드 컴포넌트를 작성할 경우 다음과 같이 HTML과 CSS를 분리했습니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;card&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">img&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;card__image&amp;#34;&lt;/span> &lt;span class="na">src&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;image.jpg&amp;#34;&lt;/span> &lt;span class="na">alt&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;thumbnail&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;card__content&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">h2&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;card__title&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>제목&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">h2&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">p&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;card__description&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>설명문이 이곳에 들어갑니다.&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">p&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="cl">&lt;span class="p">.&lt;/span>&lt;span class="nc">card&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">border-radius&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">8&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">box-shadow&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="mi">4&lt;/span>&lt;span class="kt">px&lt;/span> &lt;span class="mi">6&lt;/span>&lt;span class="kt">px&lt;/span> &lt;span class="nb">rgba&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="mf">0.1&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">background-color&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#ffffff&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">overflow&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">hidden&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">.&lt;/span>&lt;span class="nc">card__title&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">font-size&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">1.5&lt;/span>&lt;span class="kt">rem&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">font-weight&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">bold&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">color&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#333333&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">/* 이후 상세한 스타일이 계속됨 */&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>이러한 BEM(Block Element Modifier) 기반 설계는 프로젝트 규모가 작을 때는 기능하지만, 다음과 같은 문제를 일으키기 쉽습니다.&lt;/p>
&lt;ol>
&lt;li>&lt;strong>이름 짓기의 고갈과 피로&lt;/strong>: 비슷한 컴포넌트를 만들 때마다 새로운 클래스명을 고민해야 합니다(예: &lt;code>card-news&lt;/code>, &lt;code>card-featured&lt;/code> 등).&lt;/li>
&lt;li>&lt;strong>CSS의 비대화&lt;/strong>: 새로운 기능을 추가할 때마다 CSS의 줄 수가 계속 늘어나고, 한 번 작성된 CSS는 &amp;ldquo;어디서 사용되는지 모른다&amp;quot;는 두려움 때문에 삭제되는 일이 적어 데드 코드가 축적됩니다.&lt;/li>
&lt;li>&lt;strong>컨텍스트 스위칭&lt;/strong>: HTML의 구조와 CSS의 스타일을 별도의 파일로 관리하기 때문에 에디터 상에서 탭을 오가는 횟수가 기하급수적으로 증가합니다.&lt;/li>
&lt;/ol>
&lt;h3 id="tailwind-css에-의한-패러다임-시프트">Tailwind CSS에 의한 패러다임 시프트
&lt;/h3>&lt;p>Tailwind CSS는 이러한 문제들을 &amp;lsquo;유틸리티 클래스의 조합&amp;rsquo;이라는 접근법으로 해결합니다. 위의 카드 컴포넌트는 Tailwind CSS를 사용하면 다음과 같이 됩니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;rounded-lg shadow-md bg-white overflow-hidden&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">img&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;w-full h-48 object-cover&amp;#34;&lt;/span> &lt;span class="na">src&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;image.jpg&amp;#34;&lt;/span> &lt;span class="na">alt&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;thumbnail&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;p-6&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">h2&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;text-2xl font-bold text-gray-800&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>제목&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">h2&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">p&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;mt-2 text-gray-600&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>설명문이 이곳에 들어갑니다.&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">p&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>클래스명 자체가 스타일의 구체적인 값(&lt;code>p-6&lt;/code>는 &lt;code>padding: 1.5rem;&lt;/code> 등)을 나타내고 있기 때문에, HTML을 보는 것만으로도 최종적인 렌더링 결과를 예측할 수 있습니다. 게다가 Tailwind의 JIT(Just-In-Time) 컴파일러에 의해 실제로 사용된 클래스만이 프로덕션용 CSS 파일로 추출되기 때문에 CSS의 파일 크기는 극한까지 작아집니다.&lt;/p>
&lt;hr>
&lt;h2 id="2-hugo-pipes와-postcss의-아키텍처">2. Hugo Pipes와 PostCSS의 아키텍처
&lt;/h2>&lt;p>Hugo에 Tailwind CSS를 통합하기 위해서는 &lt;strong>Hugo Pipes&lt;/strong>라고 불리는 에셋 처리 파이프라인을 이해할 필요가 있습니다. Hugo Pipes는 Sass/SCSS 컴파일, JavaScript 번들링 및 Minify, 그리고 이번에 사용하는 &lt;strong>PostCSS&lt;/strong> 실행 등 에셋과 관련된 모든 처리를 Hugo 내부에서 완결시키는 강력한 기능입니다.&lt;/p>
&lt;p>PostCSS는 JavaScript 플러그인을 사용하여 CSS를 변환하기 위한 도구입니다. Tailwind CSS 자체도 실은 PostCSS의 플러그인으로서 동작하고 있습니다.&lt;/p>
&lt;h3 id="postcss에-의한-ast추상-구문-트리-변환-메커니즘">PostCSS에 의한 AST(추상 구문 트리) 변환 메커니즘
&lt;/h3>&lt;p>PostCSS가 어떻게 CSS를 처리하고 있는지를 이해하는 것은 트러블슈팅을 할 때 크게 도움이 됩니다. 다음의 Mermaid 다이어그램은 PostCSS가 CSS 파일을 읽어 들이고, 플러그인을 통해 변환하여 최종적인 CSS를 출력하기까지의 파이프라인을 보여줍니다.&lt;/p>
&lt;pre class="mermaid">
flowchart TD
A[&amp;#34;원본 CSS (styles.css)&amp;#34;] --&amp;gt;|Parser| B[&amp;#34;AST (추상 구문 트리)&amp;#34;]
B --&amp;gt; C[&amp;#34;Plugin 1: Tailwind CSS&amp;#34;]
C --&amp;gt; D[&amp;#34;Plugin 2: Autoprefixer&amp;#34;]
D --&amp;gt; E[&amp;#34;Plugin N: cssnano&amp;#34;]
E --&amp;gt;|Stringifier| F[&amp;#34;컴파일 및 최적화된 CSS&amp;#34;]
&lt;/pre>
&lt;ol>
&lt;li>&lt;strong>Parser (파서)&lt;/strong>: 입력된 원본 CSS 문자열을 분석하여, 프로그램으로 조작 가능한 데이터 구조인 AST(추상 구문 트리)로 변환합니다.&lt;/li>
&lt;li>&lt;strong>Plugins (플러그인들)&lt;/strong>:
&lt;ul>
&lt;li>&lt;strong>Tailwind CSS&lt;/strong>: 템플릿 파일(HTML이나 Markdown)을 스캔하여, 사용되고 있는 유틸리티 클래스를 AST 상에 노드로 추가합니다. 또한 &lt;code>@tailwind&lt;/code> 지시어를 전개합니다.&lt;/li>
&lt;li>&lt;strong>Autoprefixer&lt;/strong>: &lt;code>Can I Use&lt;/code> 데이터베이스를 참조하여, 필요에 따라 벤더 프리픽스(&lt;code>-webkit-&lt;/code>, &lt;code>-moz-&lt;/code> 등)를 AST의 속성에 추가합니다.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>Stringifier (스트링기파이어)&lt;/strong>: 변환이 완료된 AST를 다시 브라우저가 해석 가능한 CSS 문자열로 변환하여 출력합니다.&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="3-환경-구축과-전제-조건">3. 환경 구축과 전제 조건
&lt;/h2>&lt;p>그러면 실제 도입 절차로 들어가 보겠습니다. 먼저 필요한 소프트웨어가 설치되어 있는지 확인합니다.&lt;/p>
&lt;h3 id="필수-요건">필수 요건
&lt;/h3>&lt;ol>
&lt;li>
&lt;p>&lt;strong>Hugo Extended Version&lt;/strong>:
일반적인 Hugo가 아닌, Sass/SCSS 처리 기능이나 네이티브 PostCSS 연동 기능이 포함된 &lt;strong>Extended 버전&lt;/strong>이 필수입니다. 터미널에서 다음 명령어를 실행하여 버전 정보에 &lt;code>extended&lt;/code>라는 문자열이 포함되어 있는지 확인해 주세요.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">hugo version
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 예상되는 출력 예시:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># hugo v0.121.2-4146... windows/amd64 BuildDate=... VendorInfo=gohugoio +extended&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>Node.js와 npm&lt;/strong>:
Tailwind CSS나 PostCSS 등의 의존성 패키지는 Node.js 상에서 동작합니다. Node.js(LTS 버전 권장)가 설치되어 있는지 확인합니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">node -v
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">npm -v
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ol>
&lt;h3 id="npm-패키지-설치">npm 패키지 설치
&lt;/h3>&lt;p>프로젝트 루트 디렉토리(Hugo 설정 파일 &lt;code>hugo.toml&lt;/code>이 있는 위치)에서 npm을 초기화하고, 필요한 패키지를 설치합니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># package.json 생성&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">npm init -y
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 개발 의존성 패키지로 Tailwind CSS, PostCSS, Autoprefixer 설치&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">npm install -D tailwindcss postcss postcss-cli autoprefixer
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>[!IMPORTANT]
&lt;code>postcss-cli&lt;/code>가 설치되어 있지 않으면, Hugo 내부에서 PostCSS를 호출할 때 에러가 발생할 수 있습니다. Hugo Pipes는 내부적으로 &lt;code>postcss-cli&lt;/code>를 사용하므로 반드시 설치해 두어야 합니다.&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h2 id="4-설정-파일-구축-postcss--tailwind-css">4. 설정 파일 구축 (PostCSS &amp;amp; Tailwind CSS)
&lt;/h2>&lt;p>패키지 설치가 완료되면, 프로젝트의 동작을 제어할 2개의 중요한 설정 파일을 작성합니다. 프로젝트 루트 디렉토리에 배치해 주세요.&lt;/p>
&lt;h3 id="tailwindconfigjs-작성">tailwind.config.js 작성
&lt;/h3>&lt;p>터미널에서 다음 명령어를 실행하면 기본 설정 파일이 생성됩니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">npx tailwindcss init
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>생성된 &lt;code>tailwind.config.js&lt;/code>를 에디터로 열고, &lt;code>content&lt;/code> 속성을 설정합니다. 이 부분은 매우 중요합니다. Tailwind는 여기서 지정된 경로의 파일을 분석하여 사용되고 있는 클래스를 추출합니다. Hugo의 프로젝트 구조에 맞춰 레이아웃 파일이나 콘텐츠 파일을 정확하게 지정합니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="cl">&lt;span class="cm">/** @type {import(&amp;#39;tailwindcss&amp;#39;).Config} */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">module&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">exports&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Hugo의 디렉토리 구조에 맞춰 스캔 대상을 지정
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">content&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;./content/**/*.md&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;./content/**/*.html&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;./layouts/**/*.html&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;./assets/**/*.js&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 테마를 사용하는 경우에는 테마의 디렉토리도 포함해야 합니다
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// &amp;#34;./themes/my-theme/layouts/**/*.html&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">theme&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">extend&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 커스텀 색상이나 폰트 확장을 이곳에서 진행합니다
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">colors&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;brand-primary&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;#3490dc&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;brand-secondary&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;#ffed4a&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">fontFamily&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;sans&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;Helvetica Neue&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Arial&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Hiragino Kaku Gothic ProN&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Meiryo&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;sans-serif&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">plugins&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 필요에 따라 공식 플러그인 추가 (예: Typography 플러그인)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// require(&amp;#39;@tailwindcss/typography&amp;#39;),
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="postcssconfigjs-작성">postcss.config.js 작성
&lt;/h3>&lt;p>다음으로, PostCSS가 어떤 플러그인을 어떤 순서로 실행할지를 정의하는 &lt;code>postcss.config.js&lt;/code>를 프로젝트 루트에 작성합니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">module&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">exports&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">plugins&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">tailwindcss&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">autoprefixer&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>이 설정을 통해 Hugo가 PostCSS를 호출할 때, 먼저 Tailwind CSS 처리가 수행되고 그 다음에 Autoprefixer에 의한 벤더 프리픽스 부여가 진행됩니다.&lt;/p>
&lt;hr>
&lt;h2 id="5-hugo에서의-css-에셋-파이프라인-구축">5. Hugo에서의 CSS 에셋 파이프라인 구축
&lt;/h2>&lt;p>설정이 완료되면 드디어 Hugo 테마 측에 Tailwind CSS를 통합합니다.&lt;/p>
&lt;h3 id="5-1-엔트리-포인트가-되는-css-파일-작성">5-1. 엔트리 포인트가 되는 CSS 파일 작성
&lt;/h3>&lt;p>&lt;code>assets/css/&lt;/code> 디렉토리(존재하지 않는다면 만들어주세요)에 엔트리 포인트가 될 CSS 파일을 작성합니다. 여기서는 &lt;code>main.css&lt;/code>로 하겠습니다.&lt;/p>
&lt;p>&lt;strong>파일 경로: &lt;code>assets/css/main.css&lt;/code>&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="cl">&lt;span class="c">/* Tailwind의 기본 스타일(리셋 CSS 등) 불러오기 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">@&lt;/span>&lt;span class="k">tailwind&lt;/span> &lt;span class="nt">base&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">/* 컴포넌트 클래스 불러오기 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">@&lt;/span>&lt;span class="k">tailwind&lt;/span> &lt;span class="nt">components&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">/* 유틸리티 클래스 불러오기 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">@&lt;/span>&lt;span class="k">tailwind&lt;/span> &lt;span class="nt">utilities&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">/* 독자적인 커스텀 CSS가 필요한 경우 여기에 추가할 수 있지만,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"> 가능한 한 tailwind.config.js의 extend로 대응하는 것을 권장합니다 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">@&lt;/span>&lt;span class="k">layer&lt;/span> &lt;span class="nt">components&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="nc">btn-primary&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">@apply&lt;/span> &lt;span class="err">bg-blue-500&lt;/span> &lt;span class="n">hover&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">bg-blue-700&lt;/span> &lt;span class="kc">text&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="kc">white&lt;/span> &lt;span class="k">font&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="kc">bold&lt;/span> &lt;span class="n">py-2&lt;/span> &lt;span class="n">px-4&lt;/span> &lt;span class="n">rounded&lt;/span> &lt;span class="k">transition&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">colors&lt;/span> &lt;span class="n">duration-300&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="5-2-레이아웃-파일headhtml-편집">5-2. 레이아웃 파일(head.html) 편집
&lt;/h3>&lt;p>다음으로, Hugo의 템플릿에서 위에서 만든 CSS 파일을 불러와 PostCSS로 처리하는 파이프라인을 작성합니다. 일반적으로 &lt;code>&amp;lt;head&amp;gt;&lt;/code> 태그 내부를 정의하고 있는 부분 템플릿(예: &lt;code>layouts/partials/head.html&lt;/code>)을 편집합니다.&lt;/p>
&lt;p>&lt;strong>파일 경로: &lt;code>layouts/partials/head.html&lt;/code>&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go-html-template" data-lang="go-html-template">&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">head&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">meta&lt;/span> &lt;span class="na">charset&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;utf-8&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">meta&lt;/span> &lt;span class="na">name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;viewport&amp;#34;&lt;/span> &lt;span class="na">content&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;width=device-width, initial-scale=1&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">title&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Title&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span> | &lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Site.Title&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">title&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c">&amp;lt;!-- assets/css/main.css 가져오기 --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$css&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">:=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">resources&lt;/span>&lt;span class="na">.Get&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;css/main.css&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c">&amp;lt;!-- PostCSS의 옵션 정의 --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$options&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">:=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">dict&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;inlineImports&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">true&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$css&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$css&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">resources&lt;/span>&lt;span class="na">.PostCSS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$options&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c">&amp;lt;!-- 프로덕션 환경(Production)을 위한 에셋 최적화 파이프라인 --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">hugo&lt;/span>&lt;span class="na">.IsProduction&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c">&amp;lt;!-- 1. Minify (압축) --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$css&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$css&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">minify&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c">&amp;lt;!-- 2. Fingerprint (캐시 버스팅을 위한 해시 부여) --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$css&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$css&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">fingerprint&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;sha512&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c">&amp;lt;!-- 3. SRI(Subresource Integrity)를 포함하여 태그 출력 --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">link&lt;/span> &lt;span class="na">rel&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;stylesheet&amp;#34;&lt;/span> &lt;span class="na">href&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;&lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$css&lt;/span>&lt;span class="na">.RelPermalink&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="s">&amp;#34;&lt;/span> &lt;span class="na">integrity&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;&lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$css&lt;/span>&lt;span class="na">.Data.Integrity&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="s">&amp;#34;&lt;/span> &lt;span class="na">crossorigin&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;anonymous&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">else&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c">&amp;lt;!-- 개발 환경(Development)에서는 압축하지 않고 그대로 출력 (빌드 속도 우선) --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">link&lt;/span> &lt;span class="na">rel&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;stylesheet&amp;#34;&lt;/span> &lt;span class="na">href&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;&lt;/span>&lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nx">$css&lt;/span>&lt;span class="na">.RelPermalink&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>&lt;span class="s">&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">end&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">head&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h4 id="파이프라인-해설-및-mermaid-다이어그램">파이프라인 해설 및 Mermaid 다이어그램
&lt;/h4>&lt;p>위의 Go 템플릿 코드가 어떻게 CSS 파일을 처리해 나가는지, 일련의 파이프라인 처리를 다이어그램으로 설명합니다.&lt;/p>
&lt;pre class="mermaid">
flowchart TD
A[&amp;#34;assets/css/main.css&amp;#34;] --&amp;gt;|resources.Get| B[&amp;#34;Hugo 메모리 공간&amp;#34;]
B --&amp;gt;|resources.PostCSS| C[&amp;#34;PostCSS + Tailwind JIT 처리&amp;#34;]
C --&amp;gt; D{&amp;#34;hugo.IsProduction ?&amp;#34;}
D -- Yes (프로덕션 환경) --&amp;gt; E[&amp;#34;minify (공백·줄바꿈 삭제)&amp;#34;]
E --&amp;gt; F[&amp;#34;fingerprint (해시 계산)&amp;#34;]
F --&amp;gt; G[&amp;#34;public/css/main.[hash].css (SRI 포함)&amp;#34;]
D -- No (개발 환경) --&amp;gt; H[&amp;#34;public/css/main.css&amp;#34;]
&lt;/pre>
&lt;ol>
&lt;li>&lt;strong>&lt;code>resources.Get&lt;/code>&lt;/strong>: &lt;code>assets&lt;/code> 디렉토리 내의 지정된 파일을 찾아 메모리 상의 리소스 객체로 로드합니다.&lt;/li>
&lt;li>&lt;strong>&lt;code>resources.PostCSS&lt;/code>&lt;/strong>: 프로젝트 루트의 &lt;code>postcss.config.js&lt;/code>를 참조하여, CSS 소스 코드에 Tailwind CSS와 Autoprefixer 처리를 적용합니다. 개발 환경(&lt;code>hugo server&lt;/code>)에서는 JIT 모드가 작동하여, 파일 변경 시 필요한 클래스만을 빠르게 생성합니다.&lt;/li>
&lt;li>&lt;strong>&lt;code>minify&lt;/code>&lt;/strong>: 프로덕션 환경 빌드 시(&lt;code>hugo --environment production&lt;/code> 등)에 불필요한 공백이나 주석을 삭제하여 파일 크기를 최소화합니다.&lt;/li>
&lt;li>&lt;strong>&lt;code>fingerprint&lt;/code>&lt;/strong>: 파일의 내용에 기반하여 SHA 해시를 계산하고 파일명에 부여합니다(예: &lt;code>main.ab12cd...css&lt;/code>). 이를 통해 브라우저의 강력한 캐시를 활용하면서 CSS 업데이트 시에는 확실하게 새로운 파일을 불러오게 하는 &amp;lsquo;캐시 버스팅&amp;rsquo;을 실현합니다.&lt;/li>
&lt;li>&lt;strong>&lt;code>integrity&lt;/code>&lt;/strong>: Fingerprint로 계산된 해시값을 사용하여 CDN 등에서의 위변조를 방지하는 SRI 속성을 출력합니다.&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="6-css-최적화에서의-수학적-성능-분석">6. CSS 최적화에서의 수학적 성능 분석
&lt;/h2>&lt;p>Tailwind CSS를 도입하는 가장 큰 장점 중 하나는 전송되는 CSS 파일 크기의 극소화입니다. 이것이 웹 성능(특히 First Contentful Paint: FCP)에 어떤 영향을 미치는지 수학적인 모델을 사용하여 정량적으로 분석해 보겠습니다.&lt;/p>
&lt;h3 id="css-파일-크기-감소-모델">CSS 파일 크기 감소 모델
&lt;/h3>&lt;p>기존 CSS 프레임워크(Bootstrap 등)에서는 사용하지 않는 스타일까지 포함하여 전체가 로드되기 때문에 파일 크기 $S_{original}$이 커지기 쉽습니다(약 150KB〜200KB).
Tailwind CSS의 JIT 컴파일러에 의한 불필요한 클래스 제거(Purge) 적용 후의 크기를 $S_{purged}$라고 하면, 감소율 $R_{purge}$를 사용하여 다음과 같이 나타낼 수 있습니다.&lt;/p>
$$
S_{purged} = S_{original} \times (1 - R_{purge})
$$&lt;p>일반적인 프로젝트에서 $R_{purge}$는 $0.9$(90% 감소) 가까이에 도달하며, $S_{purged}$는 불과 10KB〜20KB 정도 내에 머뭅니다.&lt;/p>
&lt;p>게다가 전송 시에는 서버 측에서 Brotli나 Gzip에 의한 압축이 이루어집니다. 압축률을 $R_{compress}$(통상적으로 약 0.7〜0.8)라고 하면 네트워크를 흐르는 최종 페이로드 크기 $S_{final}$은 다음 공식으로 계산됩니다.&lt;/p>
$$
S_{final} = S_{purged} \times (1 - R_{compress})
$$&lt;h3 id="중요-렌더링-경로critical-rendering-path와-네트워크-지연">중요 렌더링 경로(Critical Rendering Path)와 네트워크 지연
&lt;/h3>&lt;p>브라우저가 화면에 첫 콘텐츠를 그릴 때까지의 시간(FCP)은 HTML 다운로드 시간, CSS 다운로드 시간, 그리고 렌더링 시간의 합계로 근사할 수 있습니다.&lt;/p>
$$
T_{FCP} \approx RTT + \frac{S_{HTML}}{BW} + RTT + \frac{S_{final}}{BW} + T_{render}
$$&lt;p>여기서,&lt;/p>
&lt;ul>
&lt;li>$RTT$: Round Trip Time (서버와의 왕복 통신 지연 시간)&lt;/li>
&lt;li>$BW$: 네트워크 대역폭 (Bandwidth)&lt;/li>
&lt;/ul>
&lt;p>모바일 환경 등 $BW$가 좁고 $RTT$가 큰(지연이 긴) 환경에서, $S_{final}$을 수 킬로바이트 단위까지 깎아낼 수 있는 Tailwind CSS의 접근 방식은 $\frac{S_{final}}{BW}$ 항을 극한까지 0에 가깝게 만들어, 경이로운 점수(Google PageSpeed Insights 등)를 내는 원동력이 됩니다.&lt;/p>
&lt;hr>
&lt;h2 id="7-개발-서버-실행과-핫-리로드-확인">7. 개발 서버 실행과 핫 리로드 확인
&lt;/h2>&lt;p>모든 설정이 완료되었다면 Hugo 개발 서버를 실행하고, Tailwind CSS가 올바르게 작동하는지 확인합니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">hugo server -D
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>브라우저에서 &lt;code>http://localhost:1313/&lt;/code>에 접속하여 사이트가 표시되는지 확인합니다.
Markdown 콘텐츠 파일이나 Hugo 템플릿(&lt;code>layouts/&lt;/code> 이하 파일)을 열어 클래스를 추가해 보세요.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="c">&amp;lt;!-- 테스트용 Tailwind 클래스 적용 예시 --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;bg-gradient-to-r from-blue-500 to-purple-600 text-white p-8 rounded-xl shadow-2xl text-center transform transition duration-500 hover:scale-105&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">h1&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;text-4xl font-extrabold tracking-tight&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>Tailwind CSS + Hugo is Awesome!&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">h1&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">p&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;mt-4 text-lg font-medium&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>핫 리로드가 즉각적으로 반영되는 것을 확인해 보세요.&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">p&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>파일을 저장하는 순간, Hugo의 강력한 파일 감시자와 Tailwind의 JIT 컴파일러가 연동하여 밀리초 단위로 CSS가 재구축되고, 브라우저가 자동으로 새로고침되는(핫 리로드) 쾌감을 맛볼 수 있을 것입니다.&lt;/p>
&lt;h3 id="트러블슈팅-스타일이-반영되지-않을-경우">트러블슈팅: 스타일이 반영되지 않을 경우
&lt;/h3>&lt;p>만약 변경 사항이 반영되지 않는다면 다음 사항들을 체크해 보세요.&lt;/p>
&lt;ol>
&lt;li>&lt;strong>&lt;code>tailwind.config.js&lt;/code>의 &lt;code>content&lt;/code> 경로 설정&lt;/strong>
스캔 대상 파일 경로가 잘못되어 있으면 Tailwind는 해당 파일 내에서 사용 중인 클래스를 감지하지 못하여 CSS에 출력하지 않습니다. 특히 테마를 사용하는 경우 테마 디렉토리의 경로가 누락되지 않았는지 확인하세요.&lt;/li>
&lt;li>&lt;strong>PostCSS 에러&lt;/strong>
터미널의 Hugo 서버 로그에 &lt;code>Error: failed to transform resource: PostCSS not found&lt;/code> 같은 에러가 출력된다면, &lt;code>npm install&lt;/code>이 제대로 실행되지 않았거나 &lt;code>postcss-cli&lt;/code>가 부족할 가능성이 있습니다.&lt;/li>
&lt;li>&lt;strong>Hugo 캐시 삭제&lt;/strong>
드물게 Hugo의 캐시 문제로 오래된 CSS가 남는 경우가 있습니다. 서버를 끄고 &lt;code>hugo server --ignoreCache&lt;/code>로 다시 실행하거나, OS의 임시 디렉토리(&lt;code>/tmp/hugo_cache/&lt;/code> 등)를 삭제해 보세요.&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="8-프로덕션-환경용-빌드와-더욱-고도화된-설정">8. 프로덕션 환경용 빌드와 더욱 고도화된 설정
&lt;/h2>&lt;p>사이트를 프로덕션 서버(Netlify, Vercel, GitHub Pages, Cloudflare Pages 등)에 배포할 때는, 환경 변수를 설정하여 프로덕션용 최적화 파이프라인을 실행해야 합니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 프로덕션 빌드 명령어 예시&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">NODE_ENV&lt;/span>&lt;span class="o">=&lt;/span>production hugo --minify --environment production
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;code>--environment production&lt;/code> 플래그를 붙임으로써 &lt;code>head.html&lt;/code> 안의 &lt;code>{{ if hugo.IsProduction }}&lt;/code> 블록이 실행되며, CSS의 Minify화와 Fingerprint 부여가 수행됩니다.&lt;/p>
&lt;h3 id="typography-플러그인을-활용한-markdown-스타일링">Typography 플러그인을 활용한 Markdown 스타일링
&lt;/h3>&lt;p>Hugo 같은 블로그나 문서 사이트에서는 Markdown에서 생성된 순수 HTML 요소(&lt;code>&amp;lt;h1&amp;gt;&lt;/code>, &lt;code>&amp;lt;p&amp;gt;&lt;/code>, &lt;code>&amp;lt;ul&amp;gt;&lt;/code> 등)에 직접 클래스를 추가할 수 없습니다. 이러한 경우에 매우 유용한 것이 Tailwind 공식 &lt;strong>Typography 플러그인&lt;/strong>입니다.&lt;/p>
&lt;ol>
&lt;li>
&lt;p>플러그인 설치&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">npm install -D @tailwindcss/typography
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;code>tailwind.config.js&lt;/code>에 추가&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">module&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">exports&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">plugins&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">require&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;@tailwindcss/typography&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>
&lt;p>템플릿에 적용
글 본문을 출력하는 컨테이너 요소에 &lt;code>prose&lt;/code> 클래스(및 취향에 맞게 색상이나 크기의 변형)를 부여하는 것만으로 아름다운 기본 스타일이 적용됩니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go-html-template" data-lang="go-html-template">&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">article&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;prose prose-lg prose-blue mx-auto mt-10&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="cp">{{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="na">.Content&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="cp">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">article&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ol>
&lt;p>이것으로 손수 복잡한 CSS 선택자(&lt;code>.article-content h2 { ... }&lt;/code>)를 작성할 필요가 전혀 없어지며, 컴포넌트의 모듈성이 완벽하게 유지됩니다.&lt;/p>
&lt;hr>
&lt;h2 id="9-요약-유지-보수성이-뛰어난-프론트엔드-생태계-완성">9. 요약: 유지 보수성이 뛰어난 프론트엔드 생태계 완성
&lt;/h2>&lt;p>수고하셨습니다. 이것으로 Hugo의 초고속 정적 사이트 생성 엔진과 Tailwind CSS의 모던한 스타일링 기능, 그리고 PostCSS의 확장성을 갖춘 완벽한 웹 개발 에셋 파이프라인이 완성되었습니다.&lt;/p>
&lt;p>이 아키텍처의 뛰어난 점은 **&amp;ldquo;설정은 처음 한 번으로 끝난다&amp;rdquo;**는 것입니다. 한 번 파이프라인을 구축해 두면, 개발자는 CSS 파일을 열 필요 없이 직관적인 유틸리티 클래스를 HTML이나 Markdown 템플릿에 작성하는 것만으로 복잡한 UI를 놀라운 속도로 조립해 나갈 수 있습니다.&lt;/p>
&lt;p>또한 출력되는 CSS 크기가 항상 최소화되기 때문에 Core Web Vitals 점수 향상으로도 직결되며 SEO 관점에서도 매우 유리하게 작용합니다.&lt;/p>
&lt;p>Hugo와 Tailwind CSS의 조합은 개인 기술 블로그부터 대규모 기업 사이트까지 모든 프로젝트에서 &amp;lsquo;최고의 선택지&amp;rsquo; 중 하나로 계속 남을 것입니다. 부디 이 강력한 툴체인을 활용하여 쾌적한 웹 개발 라이프를 즐기시길 바랍니다!&lt;/p></description></item><item><title>블로그의 다크 모드 지원을 위한 CSS 설계의 포인트</title><link>http://kenji.blog/ko/p/dark-mode-css-design-patterns/</link><pubDate>Sat, 12 Sep 2026 12:00:00 +0900</pubDate><guid>http://kenji.blog/ko/p/dark-mode-css-design-patterns/</guid><description>&lt;img src="http://kenji.blog/p/dark-mode-css-design-patterns/img/eyecatch.jpg" alt="Featured image of post 블로그의 다크 모드 지원을 위한 CSS 설계의 포인트" />&lt;p>현대 웹 개발에서 다크 모드(Dark Mode) 지원은 단순한 &amp;lsquo;있으면 좋은 기능(Nice to have)&amp;lsquo;에서 사용자 경험(UX)을 향상시키기 위한 &amp;lsquo;필수 요건(Must have)&amp;lsquo;으로 변화하고 있습니다. 특히 블로그나 문서 사이트처럼 장시간 텍스트 읽기를 전제로 하는 미디어에서는 사용자의 눈의 피로를 덜어주고 기기의 배터리 소모를 줄이는 효과가 있기 때문에, 다크 모드 지원의 중요성은 매우 높다고 할 수 있습니다.&lt;/p>
&lt;p>본 기사에서는 블로그의 다크 모드 지원에 있어 피할 수 없는 기술적인 과제와 유지보수성이 높은 CSS 설계의 핵심을 프런트엔드 엔지니어의 관점에서 매우 깊이 파고들어 해설합니다. CSS Custom Properties(CSS 변수)의 활용, FOUC(Flash of Unstyled Content)를 방지하기 위한 고급 JavaScript 제어와 SSR 연동, 접근성(WCAG 2.1 AAA)을 보장하기 위한 색채 설계(RGB, HSL, 그리고 최신 OKLCH), 나아가 Tailwind CSS를 활용한 실전 코드 예시까지 다크 모드 구현의 모든 것을 망라합니다.&lt;/p>
&lt;hr>
&lt;h2 id="1-css-custom-propertiescss-변수를-이용한-테마-설계의-기초">1. CSS Custom Properties(CSS 변수)를 이용한 테마 설계의 기초
&lt;/h2>&lt;p>다크 모드를 구현하는 데 있어 현재 가장 표준적이고 강력한 방법이 &lt;strong>CSS Custom Properties(CSS 변수)&lt;/strong> 를 이용한 접근법입니다. Sass 같은 CSS 전처리기의 변수(&lt;code>$color&lt;/code>)가 컴파일 시점에 정적으로 해결되는 반면, CSS 변수는 브라우저의 런타임에서 동적으로 해결 및 덮어쓰기 됩니다. 이를 통해 JavaScript에서 클래스를 전환하는 것만으로 페이지 전체의 색조를 순식간에 변경하는 것이 가능해집니다.&lt;/p>
&lt;h3 id="11-기본적인-컬러-테마-정의">1.1 기본적인 컬러 테마 정의
&lt;/h3>&lt;p>먼저, &lt;code>:root&lt;/code> 가상 클래스를 사용하여 라이트 모드(기본값)의 컬러 팔레트를 정의합니다. 그리고 &lt;code>[data-theme='dark']&lt;/code>와 같은 속성(또는 &lt;code>.dark&lt;/code> 클래스)이 부여되었을 때, 해당 변수들을 덮어쓰는 설계 패턴이 정석입니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="cl">&lt;span class="c">/* 라이트 모드(기본값)의 변수 정의 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">:&lt;/span>&lt;span class="nd">root&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--color-bg-primary&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#ffffff&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--color-bg-secondary&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#f3f4f6&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--color-text-primary&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#111827&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--color-text-secondary&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#4b5563&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--color-accent&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#3b82f6&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--color-border&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#e5e7eb&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">/* 다크 모드 시의 변수 덮어쓰기 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[&lt;/span>&lt;span class="nt">data-theme&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;dark&amp;#39;&lt;/span>&lt;span class="o">]&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--color-bg-primary&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#111827&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--color-bg-secondary&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#1f2937&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--color-text-primary&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#f9fafb&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--color-text-secondary&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#9ca3af&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--color-accent&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#60a5fa&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--color-border&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#374151&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">/* 실제 적용 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nt">body&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">background-color&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nf">var&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">--&lt;/span>&lt;span class="kc">color&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">bg&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">primary&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">color&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nf">var&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">--&lt;/span>&lt;span class="kc">color&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="kc">text&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">primary&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">transition&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="k">background-color&lt;/span> &lt;span class="mf">0.3&lt;/span>&lt;span class="kt">s&lt;/span> &lt;span class="kc">ease&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">color&lt;/span> &lt;span class="mf">0.3&lt;/span>&lt;span class="kt">s&lt;/span> &lt;span class="kc">ease&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nt">a&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">color&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nf">var&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">--&lt;/span>&lt;span class="kc">color&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">accent&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>이렇게 레이아웃이나 타이포그래피 지정과 색상(테마) 지정을 완전히 분리함으로써 CSS의 유지보수성은 비약적으로 향상됩니다.&lt;/p>
&lt;h3 id="12-media-prefers-color-scheme-dark-의-활용">1.2 @media (prefers-color-scheme: dark) 의 활용
&lt;/h3>&lt;p>OS 레벨에서 다크 모드가 설정된 경우, 웹사이트 첫 방문 시부터 자동으로 다크 테마를 적용하는 것이 UX 관점에서 바람직합니다. 이를 실현하는 것이 &lt;code>@media (prefers-color-scheme: dark)&lt;/code> 라는 미디어 쿼리입니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="cl">&lt;span class="c">/* OS의 환경 설정이 다크 모드인 경우의 폴백 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">@&lt;/span>&lt;span class="k">media&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="nt">prefers-color-scheme&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nt">dark&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">:&lt;/span>&lt;span class="nd">root&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="nd">not&lt;/span>&lt;span class="o">([&lt;/span>&lt;span class="nt">data-theme&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;light&amp;#39;&lt;/span>&lt;span class="o">])&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--color-bg-primary&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#111827&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--color-bg-secondary&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#1f2937&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--color-text-primary&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#f9fafb&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--color-text-secondary&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#9ca3af&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--color-accent&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#60a5fa&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--color-border&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mh">#374151&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>이 표기법에서는 사용자가 명시적으로 라이트 모드를 선택(&lt;code>data-theme='light'&lt;/code>)하지 않는 한, OS의 다크 모드 설정을 존중하여 변수를 덮어씁니다.&lt;/p>
&lt;hr>
&lt;h2 id="2-색-공간의-이해와-접근성wcag-21-aaa">2. 색 공간의 이해와 접근성(WCAG 2.1 AAA)
&lt;/h2>&lt;p>다크 모드의 색채 설계에서 단순히 &amp;lsquo;배경을 검게, 글자를 하얗게&amp;rsquo; 하는 것만으로는 불충분합니다. 대비가 너무 강하면 헐레이션이 일어나 오히려 읽기 힘들어지고, 대비가 너무 낮으면 시인성이 떨어집니다. Web Content Accessibility Guidelines (WCAG)에서는 시인성을 확보하기 위한 대비율(명암비)이 엄격하게 정의되어 있습니다.&lt;/p>
&lt;h3 id="21-wcag-대비율-계산식">2.1 WCAG 대비율 계산식
&lt;/h3>&lt;p>WCAG에서의 대비율(Contrast Ratio) $CR$ 은 배경색과 전경색의 상대 휘도(Relative Luminance)를 이용하여 다음과 같이 정의됩니다.&lt;/p>
$$CR = \frac{L_{lighter} + 0.05}{L_{darker} + 0.05}$$&lt;p>여기서 $L_{lighter}$는 밝은 쪽 색의 상대 휘도, $L_{darker}$는 어두운 쪽 색의 상대 휘도입니다(값의 범위는 0.0부터 1.0까지). WCAG 2.1의 레벨 AAA를 달성하기 위해서는 일반 텍스트에서 &lt;strong>7:1 이상&lt;/strong>, 큰 텍스트에서 &lt;strong>4.5:1 이상&lt;/strong>의 대비율이 요구됩니다.&lt;/p>
&lt;p>상대 휘도 $L$ 은 sRGB 색 공간의 RGB 값으로부터 아래의 복잡한 수식으로 계산됩니다.&lt;/p>
$$L = 0.2126 \times R + 0.7152 \times G + 0.0722 \times B$$&lt;p>각 성분($R, G, B$)은 원래의 8비트 값($R_{sRGB}$)을 255로 나눈 정규화 값을 사용하여 감마 보정을 풀기 위한 다음 변환을 수행합니다.&lt;/p>
$$
R, G, B =
\begin{cases}
\frac{C_{sRGB}}{12.92} &amp; \text{if } C_{sRGB} \le 0.03928 \\
\left( \frac{C_{sRGB} + 0.055}{1.055} \right)^{2.4} &amp; \text{otherwise}
\end{cases}
$$&lt;p>이 계산을 수동으로 하는 것은 어렵지만, 색채 설계 도구를 활용하면 대비율이 7:1 ($CR \ge 7.0$)을 만족하는 색을 기계적으로 선정할 수 있습니다.&lt;/p>
&lt;h3 id="22-hsl-vs-rgb-vs-oklch">2.2 HSL vs RGB vs OKLCH
&lt;/h3>&lt;p>컬러 팔레트를 만들 때, 예전에는 RGB나 HSL이 주류였습니다. 하지만 이것들에는 &amp;lsquo;지각적 균일성&amp;rsquo;이라는 관점에서 큰 결함이 있습니다.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>RGB&lt;/strong>: 기계적인 빛의 삼원색이며, 사람이 직관적으로 &amp;lsquo;밝게 한다&amp;rsquo;, &amp;lsquo;어둡게 한다&amp;rsquo;와 같은 조정을 하기가 어렵습니다.&lt;/li>
&lt;li>&lt;strong>HSL&lt;/strong>: 색상(Hue), 채도(Saturation), 명도(Lightness)를 사용하지만, HSL의 &amp;lsquo;명도(L)&amp;lsquo;는 사람 눈의 지각적인 밝기와 일치하지 않습니다. 예를 들어 HSL에서 명도 50%인 순수한 노란색과 순수한 파란색은 수치상으로는 같은 밝기지만, 사람의 눈에는 노란색이 압도적으로 밝게 보입니다.&lt;/li>
&lt;li>&lt;strong>OKLCH&lt;/strong>: 최근 CSS Color Module Level 4에서 도입된 최신 색 공간입니다. Lightness(지각적 명도), Chroma(채도), Hue(색상)로 구성되어 있으며, **인간의 시각 특성과 완전히 일치(지각적 균일)**합니다.&lt;/li>
&lt;/ul>
&lt;p>OKLCH를 사용하면 색상(Hue)을 변경해도 동일한 지각적 명도(Lightness)를 유지할 수 있기 때문에 다크 모드용 컬러 팔레트 생성이 극도로 예측 가능하고 안전해집니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="cl">&lt;span class="c">/* OKLCH를 사용한 CSS 변수의 정의 예 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">:&lt;/span>&lt;span class="nd">root&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c">/* 라이트 모드의 기본 명도를 높게, 채도를 억제하여 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--bg-base&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nf">oklch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mf">0.98&lt;/span> &lt;span class="mf">0.01&lt;/span> &lt;span class="mi">250&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--text-base&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nf">oklch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mf">0.25&lt;/span> &lt;span class="mf">0.02&lt;/span> &lt;span class="mi">250&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--primary-brand&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nf">oklch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mf">0.65&lt;/span> &lt;span class="mf">0.15&lt;/span> &lt;span class="mi">250&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[&lt;/span>&lt;span class="nt">data-theme&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;dark&amp;#34;&lt;/span>&lt;span class="o">]&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c">/* 다크 모드에서는 명도를 반전시키는 것만으로, 지각적 대비를 유지하기 쉬움 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--bg-base&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nf">oklch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mf">0.20&lt;/span> &lt;span class="mf">0.02&lt;/span> &lt;span class="mi">250&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--text-base&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nf">oklch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mf">0.95&lt;/span> &lt;span class="mf">0.01&lt;/span> &lt;span class="mi">250&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">--primary-brand&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nf">oklch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mf">0.75&lt;/span> &lt;span class="mf">0.15&lt;/span> &lt;span class="mi">250&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c">/* 다크 모드용으로 조금 밝게 하여 시인성을 확보 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>이와 같이 OKLCH를 도입함으로써 여러 테마 간에 일관된 대비율(WCAG AAA 수준)을 보장하는 로직을 심플하게 구축할 수 있습니다.&lt;/p>
&lt;hr>
&lt;h2 id="3-foucflash-of-unstyled-content-방지와-ssr-하이드레이션">3. FOUC(Flash of Unstyled Content) 방지와 SSR 하이드레이션
&lt;/h2>&lt;p>다크 모드 지원에서 개발자를 가장 괴롭히는 것이 &lt;strong>FOUC(Flash of Unstyled Content)&lt;/strong> 라고 불리는 화면 깜빡임 문제입니다.&lt;/p>
&lt;h3 id="31-클라이언트-사이드-js를-통한-테마-전환의-함정">3.1 클라이언트 사이드 JS를 통한 테마 전환의 함정
&lt;/h3>&lt;p>React나 Vue 같은 SPA(혹은 SSG를 통한 정적 사이트)에서 사용자의 설정을 &lt;code>localStorage&lt;/code> 에 저장하고, JavaScript로 불러와 테마를 전환하는 방식이 일반적입니다. 하지만 이 처리를 React의 &lt;code>useEffect&lt;/code> 등에서 수행하면 다음과 같은 문제가 발생합니다.&lt;/p>
&lt;ol>
&lt;li>브라우저가 라이트 모드의 HTML/CSS를 렌더링한다.&lt;/li>
&lt;li>JS 번들이 로드되고 실행된다.&lt;/li>
&lt;li>&lt;code>localStorage&lt;/code> 에서 &lt;code>dark&lt;/code> 설정을 읽어온다.&lt;/li>
&lt;li>HTML에 &lt;code>dark&lt;/code> 클래스가 부여되고 화면이 갑자기 어두워진다 (깜빡임).&lt;/li>
&lt;/ol>
&lt;h3 id="32-완벽한-fouc-방지책-cookie와-ssr의-활용">3.2 완벽한 FOUC 방지책: Cookie와 SSR의 활용
&lt;/h3>&lt;p>FOUC를 완전히 방지하고 하이드레이션 오류를 막기 위한 모범 사례는 &lt;strong>사용자의 테마 설정을 &lt;code>document.cookie&lt;/code> 에 저장하고, 서버 사이드 렌더링(SSR) 단계에서 적절한 클래스를 부여한 HTML을 반환&lt;/strong> 하는 것입니다.&lt;/p>
&lt;p>아래 시퀀스 다이어그램은 Cookie를 이용한 테마 초기화의 이상적인 흐름을 보여줍니다.&lt;/p>
&lt;pre class="mermaid">
sequenceDiagram
participant User as &amp;#34;사용자&amp;#34;
participant Browser as &amp;#34;브라우저 OS&amp;#34;
participant Server as &amp;#34;서버 SSR&amp;#34;
participant DOM as &amp;#34;클라이언트 DOM&amp;#34;
User-&amp;gt;&amp;gt;Browser: &amp;#34;페이지 요청&amp;#34;
Browser-&amp;gt;&amp;gt;Server: &amp;#34;Cookie를 포함한 GET 요청 (theme=dark)&amp;#34;
Server--&amp;gt;&amp;gt;Browser: &amp;#34;&amp;lt;html data-theme=&amp;#39;dark&amp;#39;&amp;gt;가 포함된 HTML 반환&amp;#34;
Browser-&amp;gt;&amp;gt;DOM: &amp;#34;초기 HTML 렌더링 (FOUC 없음)&amp;#34;
DOM-&amp;gt;&amp;gt;DOM: &amp;#34;하이드레이션 스크립트 실행&amp;#34;
alt &amp;#34;Cookie 없음 (첫 방문)&amp;#34;
DOM-&amp;gt;&amp;gt;Browser: &amp;#34;OS prefers-color-scheme 확인&amp;#34;
Browser--&amp;gt;&amp;gt;DOM: &amp;#34;matchMedia 결과 (예: dark)&amp;#34;
DOM-&amp;gt;&amp;gt;DOM: &amp;#34;인라인 차단 스크립트를 통한 data-theme=&amp;#39;dark&amp;#39; 설정&amp;#34;
end
&lt;/pre>
&lt;h3 id="33-인라인-스크립트를-통한-방어선cookie를-사용할-수-없는-정적-사이트의-경우">3.3 인라인 스크립트를 통한 방어선(Cookie를 사용할 수 없는 정적 사이트의 경우)
&lt;/h3>&lt;p>SSG(정적 사이트 생성)만 지원되어 SSR이 불가능한 블로그(Hugo나 Gatsby, Astro의 정적 익스포트 등)의 경우, &lt;code>&amp;lt;head&amp;gt;&lt;/code> 태그 내부에 렌더링을 차단하며 실행되는 인라인 JavaScript를 배치하고 DOM이 렌더링 되기 직전에 클래스를 부여하는 방식이 필수적입니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="c">&amp;lt;!-- &amp;lt;head&amp;gt; 안의 마지막에 배치한다 --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="kd">function&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">try&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">localTheme&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">localStorage&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getItem&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;theme&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">osTheme&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">matchMedia&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;(prefers-color-scheme: dark)&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">matches&lt;/span> &lt;span class="o">?&lt;/span> &lt;span class="s1">&amp;#39;dark&amp;#39;&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;light&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">theme&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">localTheme&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nx">osTheme&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">documentElement&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data-theme&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">theme&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span> &lt;span class="k">catch&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">e&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>이 작은 스크립트는 브라우저의 렌더링을 차단하고 즉시 실행되기 때문에 화면이 렌더링 되는 시점에는 이미 &lt;code>data-theme&lt;/code> 속성이 설정되어 있어 화면 깜빡임(FOUC)을 완벽하게 방지할 수 있습니다.&lt;/p>
&lt;hr>
&lt;h2 id="4-tailwind-css-와-순수-scsscss에서의-구현-접근법">4. Tailwind CSS 와 순수 SCSS/CSS에서의 구현 접근법
&lt;/h2>&lt;p>다크 모드를 실제 프로젝트에 통합할 때, 각 도구별 접근 방식을 이해해 둘 필요가 있습니다.&lt;/p>
&lt;h3 id="41-tailwind-css-에서의-다크-모드">4.1 Tailwind CSS 에서의 다크 모드
&lt;/h3>&lt;p>Tailwind CSS 는 기본적으로 &lt;code>dark:&lt;/code> 변형(variant)을 제공하고 있어 매우 쉽게 다크 모드를 구현할 수 있습니다. 설정 파일(&lt;code>tailwind.config.js&lt;/code>)에서 &lt;code>darkMode&lt;/code> 속성을 설정합니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// tailwind.config.js
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">module&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">exports&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// &amp;#39;media&amp;#39; (OS 설정 의존) 또는 &amp;#39;class&amp;#39; (수동 전환 가능)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">darkMode&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;class&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">theme&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">extend&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">colors&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="cm">/* CSS 변수를 이용하여 Tailwind의 컬러 팔레트를 확장 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">primary&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;rgb(var(--color-primary) / &amp;lt;alpha-value&amp;gt;)&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">background&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;rgb(var(--color-background) / &amp;lt;alpha-value&amp;gt;)&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>HTML 측에서는 아래와 같이 클래스를 부여하기만 하면 됩니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">h1&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;text-2xl font-bold&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>Hello World&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">h1&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">p&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;mt-2&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>Tailwind makes dark mode incredibly easy.&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">p&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>하지만 모든 요소에 &lt;code>dark:bg-xxx&lt;/code> 라고 작성하는 것은 컴포넌트가 비대해지는 원인이 되기도 합니다. 대규모 블로그나 앱에서는 &lt;strong>CSS 변수를 기반으로 하고 Tailwind에서는 그 CSS 변수를 참조하는&lt;/strong> 하이브리드 설계(시맨틱 컬러 설계)가 권장됩니다.&lt;/p>
&lt;p>아래는 CSS 변수의 상속과 적용 계층을 보여주는 클래스 다이어그램입니다.&lt;/p>
&lt;pre class="mermaid">
classDiagram
class GlobalCSSVariables {
&amp;#34;--color-brand-500&amp;#34;
&amp;#34;--color-gray-900&amp;#34;
}
class SemanticVariables {
&amp;#34;--bg-primary&amp;#34;
&amp;#34;--text-base&amp;#34;
&amp;#34;--accent&amp;#34;
}
class TailwindConfig {
&amp;#34;theme.colors.background&amp;#34;
&amp;#34;theme.colors.primary&amp;#34;
}
class UIComponents {
&amp;#34;class=&amp;#39;bg-background text-primary&amp;#39;&amp;#34;
}
GlobalCSSVariables &amp;lt;|-- SemanticVariables : &amp;#34;:root &amp;amp; .dark&amp;#34;
SemanticVariables &amp;lt;|-- TailwindConfig : &amp;#34;tailwind.config.js&amp;#34;
TailwindConfig &amp;lt;.. UIComponents : &amp;#34;유틸리티 클래스 적용&amp;#34;
&lt;/pre>
&lt;h3 id="42-raw-scsscss-에서의-구현mixin-활용">4.2 Raw SCSS/CSS 에서의 구현(Mixin 활용)
&lt;/h3>&lt;p>Tailwind를 사용하지 않고 자체적으로 SCSS를 작성하는 프로젝트에서는 &lt;code>@mixin&lt;/code> 을 활용하여 다크 모드의 스타일을 캡슐화합니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-scss" data-lang="scss">&lt;span class="line">&lt;span class="cl">&lt;span class="cm">/* SCSS Mixin 정의 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">@mixin&lt;/span>&lt;span class="nf"> dark-mode&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="cm">/* [data-theme=&amp;#39;dark&amp;#39;] 속성, 또는 OS 설정 양쪽을 모두 지원 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">[&lt;/span>&lt;span class="nt">data-theme&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;dark&amp;#39;&lt;/span>&lt;span class="o">]&lt;/span> &lt;span class="k">&amp;amp;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">@content&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">@media&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">prefers-color-scheme&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="ni">dark&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nd">:root:not&lt;/span>&lt;span class="o">([&lt;/span>&lt;span class="nt">data-theme&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;light&amp;#39;&lt;/span>&lt;span class="o">])&lt;/span> &lt;span class="k">&amp;amp;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">@content&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm">/* 사용 예 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nc">.card&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="na">background-color&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mh">#ffffff&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="na">color&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mh">#333333&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="na">border&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="kt">px&lt;/span> &lt;span class="ni">solid&lt;/span> &lt;span class="mh">#eeeeee&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">@include&lt;/span>&lt;span class="nd"> dark-mode&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="na">background-color&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mh">#1a202c&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="na">color&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mh">#e2e8f0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="na">border-color&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mh">#2d3748&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>이 방법은 직관적이지만 컴파일 후의 CSS 파일 크기가 비대해지기 쉽기(미디어 쿼리가 각 선택자마다 복제됨) 때문에, 역시 CSS 변수(Custom Properties)를 중심으로 한 설계로의 전환이 현재의 트렌드입니다.&lt;/p>
&lt;hr>
&lt;h2 id="5-이미지image와-svg의-다크-모드-최적화">5. 이미지(Image)와 SVG의 다크 모드 최적화
&lt;/h2>&lt;p>텍스트와 배경의 색채 설계가 완료되었더라도, 콘텐츠로 배치된 이미지나 아이콘(SVG)이 라이트 모드 그대로라면 다크 모드 시에 너무 눈부시게 떠 보이게 됩니다. 이들에 대한 최적화도 필수적입니다.&lt;/p>
&lt;h3 id="51-이미지의-밝기를-낮추는-css-필터">5.1 이미지의 밝기를 낮추는 CSS 필터
&lt;/h3>&lt;p>사진 등의 비트맵 이미지는 다크 모드 시에 그대로 표시하면 너무 눈부실 수 있습니다. CSS의 &lt;code>filter&lt;/code> 속성을 사용하여 이미지의 명도(brightness)와 대비(contrast)를 약간 낮춤으로써 다크 테마의 UI에 자연스럽게 녹아들게 할 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="cl">&lt;span class="o">[&lt;/span>&lt;span class="nt">data-theme&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;dark&amp;#39;&lt;/span>&lt;span class="o">]&lt;/span> &lt;span class="nt">img&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="nd">not&lt;/span>&lt;span class="o">([&lt;/span>&lt;span class="nt">src&lt;/span>&lt;span class="o">*=&lt;/span>&lt;span class="s2">&amp;#34;.svg&amp;#34;&lt;/span>&lt;span class="o">])&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c">/* 밝기를 낮추고, 약간 대비를 올림 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">filter&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">brightness&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mf">0.8&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nb">contrast&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mf">1.1&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">transition&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="k">filter&lt;/span> &lt;span class="mf">0.3&lt;/span>&lt;span class="kt">s&lt;/span> &lt;span class="kc">ease&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[&lt;/span>&lt;span class="nt">data-theme&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;dark&amp;#39;&lt;/span>&lt;span class="o">]&lt;/span> &lt;span class="nt">img&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="nd">hover&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c">/* 호버 시에는 원래 밝기로 되돌림 (사용자가 자세히 보고 싶은 경우) */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">filter&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">brightness&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nb">contrast&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="52-picture-태그를-통한-이미지-분기-처리">5.2 &lt;code>&amp;lt;picture&amp;gt;&lt;/code> 태그를 통한 이미지 분기 처리
&lt;/h3>&lt;p>로고 이미지나 설명용 도해(배경이 흰색으로 고정된 JPEG 등)는 필터 처리만으로는 대응할 수 없습니다. 이 경우 HTML의 &lt;code>&amp;lt;picture&amp;gt;&lt;/code> 요소와 미디어 쿼리를 사용하여 다크 모드용의 다른 이미지 파일을 분기 처리하여 보여주는 것이 정답입니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">picture&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c">&amp;lt;!-- 다크 모드 OS 설정의 사용자에게는 이곳을 표시 --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">source&lt;/span> &lt;span class="na">srcset&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;/img/logo-dark.png&amp;#34;&lt;/span> &lt;span class="na">media&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;(prefers-color-scheme: dark)&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c">&amp;lt;!-- 기본(라이트 모드) --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">img&lt;/span> &lt;span class="na">src&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;/img/logo-light.png&amp;#34;&lt;/span> &lt;span class="na">alt&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;사이트 로고&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">picture&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>※단, 이 방법은 &lt;code>localStorage&lt;/code> 등을 통한 수동 토글과는 연동되지 않기(OS 설정에만 의존) 때문에, 수동 전환을 구현한 경우에는 JS로 이미지의 &lt;code>src&lt;/code>를 동적으로 변경하거나 CSS 클래스로 &lt;code>display: none&lt;/code>을 전환할 필요가 있습니다.&lt;/p>
&lt;h3 id="53-svg-아이콘의-currentcolor-대응">5.3 SVG 아이콘의 &lt;code>currentColor&lt;/code> 대응
&lt;/h3>&lt;p>아이콘 등에 사용하는 인라인 SVG는 칠하기 색상을 부모 요소의 텍스트 컬러와 연동시키는 가장 스마트한 방법입니다. SVG의 &lt;code>fill&lt;/code> 이나 &lt;code>stroke&lt;/code> 속성에 &lt;code>currentColor&lt;/code> 를 지정합니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="c">&amp;lt;!-- CSS의 color 속성 값(var(--text-primary) 등)이 자동으로 적용된다 --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">svg&lt;/span> &lt;span class="na">viewBox&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;0 0 24 24&amp;#34;&lt;/span> &lt;span class="na">fill&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;currentColor&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">path&lt;/span> &lt;span class="na">d&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;M12 2L2 22h20L12 2z&amp;#34;&lt;/span> &lt;span class="p">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">svg&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>이를 통해 다크 모드로 전환되어 부모 요소의 글자색이 흰색 계통이 되면, SVG 아이콘도 자동으로 흰색 계통으로 변합니다.&lt;/p>
&lt;hr>
&lt;h2 id="6-요약-지속-가능한-다크-모드-설계를-향해">6. 요약: 지속 가능한 다크 모드 설계를 향해
&lt;/h2>&lt;p>블로그나 웹 애플리케이션에서 고품질의 다크 모드를 구현하기 위해서는 아래의 포인트를 망라한 CSS 설계가 필수적입니다.&lt;/p>
&lt;ol>
&lt;li>&lt;strong>CSS Custom Properties 활용하기&lt;/strong>: 색상 지정의 하드코딩을 피하고, 시맨틱한 변수명(예: &lt;code>--bg-primary&lt;/code>)으로 추상화한다.&lt;/li>
&lt;li>&lt;strong>OKLCH 색 공간 채택하기&lt;/strong>: WCAG 2.1 AAA를 만족하는 접근성 높은 대비율(7:1 이상)을 지각적으로 균일한 색 공간에서 논리적으로 설계한다.&lt;/li>
&lt;li>&lt;strong>FOUC 대책 철저히 하기&lt;/strong>: SSR과 Cookie의 연동, 혹은 &lt;code>&amp;lt;head&amp;gt;&lt;/code> 내의 렌더링 차단 인라인 스크립트를 통해 초기 로드 시의 화면 깜빡임을 완전히 배제한다.&lt;/li>
&lt;li>&lt;strong>미디어와 자산의 최적화&lt;/strong>: &lt;code>filter: brightness()&lt;/code> 나 &lt;code>currentColor&lt;/code>, &lt;code>&amp;lt;picture&amp;gt;&lt;/code> 태그를 구사하여 텍스트 이외의 요소도 다크 테마와 조화시킨다.&lt;/li>
&lt;/ol>
&lt;p>단순한 &amp;lsquo;색상 반전&amp;rsquo;을 넘어서는 이러한 세심한 배려야말로 사용자에게 오랫동안 사랑받고 눈이 피로하지 않은 훌륭한 독서 경험(리딩 익스피리언스)을 제공하는 모던 블로그의 조건이라 할 수 있겠습니다. 앞으로 다크 모드를 도입할 개발자 분들은 꼭 이 글의 설계 패턴을 참고해 보시기 바랍니다.&lt;/p></description></item><item><title>소프트웨어 아키텍처의 기본: 클린 아키텍처란?</title><link>http://kenji.blog/ko/p/clean-architecture-basics-guide/</link><pubDate>Sat, 12 Sep 2026 12:00:00 +0900</pubDate><guid>http://kenji.blog/ko/p/clean-architecture-basics-guide/</guid><description>&lt;img src="http://kenji.blog/p/clean-architecture-basics-guide/img/eyecatch.jpg" alt="Featured image of post 소프트웨어 아키텍처의 기본: 클린 아키텍처란?" />&lt;p>현대 소프트웨어 개발에서 &amp;lsquo;변화에 강한 시스템&amp;rsquo;을 구축하는 것은 영원한 과제입니다. 비즈니스 요구사항의 변경, 새로운 프레임워크의 대두, UI의 쇄신, 데이터베이스의 마이그레이션. 이 모든 변화에 대해 시스템 전체를 재구축하지 않고도 유연하게 적응할 수 있는 아키텍처가 요구되고 있습니다. 그 해답 중 하나로 Robert C. Martin(일명 엉클 밥, Uncle Bob)이 제창한 것이 **클린 아키텍처(Clean Architecture)**입니다.&lt;/p>
&lt;p>본 문서에서는 클린 아키텍처의 역사, 목적, 4개의 계층에 대한 상세, 의존성 규칙, 그리고 구체적인 구현 예시를 통해 클린 아키텍처의 진수에 다가갑니다. 매우 깊고 상세한 기술적 해설을 제공합니다.&lt;/p>
&lt;h2 id="1-기존-아키텍처의-문제점과-클린-아키텍처의-역사">1. 기존 아키텍처의 문제점과 클린 아키텍처의 역사
&lt;/h2>&lt;p>역사적으로 볼 때 소프트웨어 아키텍처는 다양한 패러다임 전환을 겪어왔습니다. 초기 시스템에서는 비즈니스 로직, UI, 데이터 접근 코드가 강하게 결합되어 있었습니다(이른바 스파게티 코드). 그 후 관심사의 분리(Separation of Concerns)를 목적으로 3계층 아키텍처(프레젠테이션 계층, 비즈니스 로직 계층, 데이터 접근 계층)가 보급되었습니다.&lt;/p>
&lt;p>하지만 기존의 3계층 아키텍처에는 큰 문제가 있었습니다. 바로 &amp;lsquo;&lt;strong>도메인(비즈니스 로직)이 데이터베이스나 프레임워크에 의존하게 된다&lt;/strong>&amp;lsquo;는 점입니다.&lt;/p>
&lt;p>예를 들어, 비즈니스 로직 계층이 데이터 접근 계층(ORM 등)을 직접 호출하면, 데이터베이스의 스키마 변경이나 ORM의 변경이 비즈니스 로직으로 파급되어 버립니다. 즉, 가장 중요하고 변경되어서는 안 될 &amp;lsquo;비즈니스 규칙&amp;rsquo;이, 가장 기술적인 변경이 일어나기 쉬운 &amp;lsquo;인프라&amp;rsquo;에 의존하게 된다는 모순이 발생하고 있었습니다.&lt;/p>
&lt;p>이에 대한 해결책으로 다음과 같은 아키텍처들이 고안되어 왔습니다.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>헥사고날 아키텍처 (Ports and Adapters)&lt;/strong> - Alistair Cockburn&lt;/li>
&lt;li>&lt;strong>어니언 아키텍처&lt;/strong> - Jeffrey Palermo&lt;/li>
&lt;li>&lt;strong>DCI (Data, Context and Interaction)&lt;/strong> - James Coplien, Trygve Reenskaug&lt;/li>
&lt;li>&lt;strong>BCE (Boundary-Control-Entity)&lt;/strong> - Ivar Jacobson&lt;/li>
&lt;/ul>
&lt;p>이러한 아키텍처들은 모두 같은 목적을 가지고 있습니다. 바로 &amp;lsquo;&lt;strong>관심사의 분리&lt;/strong>&amp;lsquo;입니다. 소프트웨어를 계층으로 분할하고, 각각이 독립적으로 테스트 가능하며, 외부 에이전트(UI, DB, 프레임워크)로부터 독립적인 상태를 만드는 것입니다.&lt;/p>
&lt;p>Robert C. Martin은 이러한 훌륭한 아키텍처의 개념들을 통합하고, 하나의 실용적인 규칙으로 정리한 것을 &amp;lsquo;&lt;strong>클린 아키텍처&lt;/strong>&amp;lsquo;라고 명명했습니다.&lt;/p>
&lt;h2 id="2-클린-아키텍처의-목적과-특성">2. 클린 아키텍처의 목적과 특성
&lt;/h2>&lt;p>클린 아키텍처를 도입한 시스템은 다음과 같은 특성을 가집니다.&lt;/p>
&lt;ol>
&lt;li>&lt;strong>프레임워크 독립성 (Independent of Frameworks)&lt;/strong>: 아키텍처는 기능이 풍부한 소프트웨어 라이브러리의 존재에 의존하지 않습니다. 이를 통해 프레임워크를 &amp;lsquo;도구&amp;rsquo;로 사용할 수 있으며, 시스템을 프레임워크의 제약에 끼워 맞출 필요가 없어집니다.&lt;/li>
&lt;li>&lt;strong>테스트 가능성 (Testable)&lt;/strong>: 비즈니스 규칙은 UI, 데이터베이스, 웹 서버 및 기타 외부 요소 없이도 테스트할 수 있습니다.&lt;/li>
&lt;li>&lt;strong>UI 독립성 (Independent of UI)&lt;/strong>: UI는 시스템의 나머지 부분을 변경하지 않고도 쉽게 변경할 수 있습니다. 예를 들어, 웹 UI는 비즈니스 규칙을 변경하지 않고도 콘솔 UI로 대체될 수 있습니다.&lt;/li>
&lt;li>&lt;strong>데이터베이스 독립성 (Independent of Database)&lt;/strong>: Oracle이나 SQL Server를 Mongo, BigTable, CouchDB 등으로 변경할 수 있습니다. 비즈니스 규칙은 데이터베이스에 얽매이지 않습니다.&lt;/li>
&lt;li>&lt;strong>외부 에이전트 독립성 (Independent of any external agency)&lt;/strong>: 실제로 비즈니스 규칙은 외부 세계에 대해 아무것도 알지 못합니다.&lt;/li>
&lt;/ol>
&lt;h2 id="3-클린-아키텍처의-4가지-계층-layers">3. 클린 아키텍처의 4가지 계층 (Layers)
&lt;/h2>&lt;p>클린 아키텍처는 일반적으로 동심원 형태의 그림으로 표현됩니다. 중심에 가까워질수록 소프트웨어는 상위 수준의 정책(추상도가 높은 비즈니스 규칙)이 됩니다. 바깥쪽으로 갈수록 메커니즘(구체적인 세부 사항)이 됩니다.&lt;/p>
&lt;pre class="mermaid">
graph TD
subgraph &amp;#34;외부 인터페이스 (프레임워크 및 드라이버)&amp;#34;
A[&amp;#34;웹&amp;#34;]
B[&amp;#34;UI&amp;#34;]
C[&amp;#34;데이터베이스&amp;#34;]
D[&amp;#34;외부 API&amp;#34;]
end
subgraph &amp;#34;인터페이스 어댑터&amp;#34;
E[&amp;#34;컨트롤러&amp;#34;]
F[&amp;#34;게이트웨이&amp;#34;]
G[&amp;#34;프레젠터&amp;#34;]
end
subgraph &amp;#34;애플리케이션 비즈니스 규칙 (유즈케이스)&amp;#34;
H[&amp;#34;유즈케이스 인터랙터&amp;#34;]
end
subgraph &amp;#34;엔터프라이즈 비즈니스 규칙 (엔티티)&amp;#34;
I[&amp;#34;엔티티&amp;#34;]
end
A --&amp;gt; E
B --&amp;gt; E
C --&amp;gt; F
D --&amp;gt; F
E --&amp;gt; H
F --&amp;gt; H
G --&amp;gt; H
H --&amp;gt; I
&lt;/pre>
&lt;h3 id="31-엔티티-entities">3.1. 엔티티 (Entities)
&lt;/h3>&lt;p>엔티티는 전사적인 비즈니스 규칙(Enterprise Business Rules)을 캡슐화합니다. 엔티티는 메서드를 가진 객체일 수도 있고, 데이터 구조와 함수의 집합일 수도 있습니다. 사내의 여러 다른 애플리케이션에서 재사용할 수 있는 가장 일반적이고 고차원적인 규칙입니다.
단일 애플리케이션만 구축하는 경우에도 엔티티는 해당 애플리케이션의 비즈니스 객체가 됩니다. 외부의 변경(페이지 내비게이션의 변경이나 보안 변경 등)이 있더라도 엔티티는 결코 영향을 받지 않습니다.&lt;/p>
&lt;h3 id="32-유즈케이스-use-cases">3.2. 유즈케이스 (Use Cases)
&lt;/h3>&lt;p>유즈케이스 계층은 애플리케이션 고유의 비즈니스 규칙(Application Business Rules)을 포함합니다. 여기서는 시스템의 모든 유즈케이스를 캡슐화하고 구현합니다. 유즈케이스는 엔티티와 그 사이의 데이터 흐름을 조정하며, 엔티티에게 시스템의 목표를 달성하도록 지시합니다.
이 계층의 변경이 엔티티에 영향을 주어서는 안 됩니다. 또한 데이터베이스, UI, 프레임워크 등 외부의 변경이 이 계층에 영향을 주지도 않습니다. 유즈케이스는 이러한 관심사들로부터 완전히 분리되어 있습니다.&lt;/p>
&lt;h3 id="33-인터페이스-어댑터-interface-adapters">3.3. 인터페이스 어댑터 (Interface Adapters)
&lt;/h3>&lt;p>인터페이스 어댑터 계층은 유즈케이스나 엔티티에게 편리한 데이터 형식에서 데이터베이스나 웹 등 외부 에이전트에게 편리한 데이터 형식으로 변환하는 어댑터의 집합입니다.
예를 들어 웹 환경에서 GUI의 MVC(Model-View-Controller) 아키텍처 요소들이 이곳에 속합니다. 컨트롤러는 사용자의 입력을 받아 유즈케이스로 전달하고, 유즈케이스의 출력을 프레젠터가 받아 뷰(UI)에 맞게 포맷팅합니다.
또한 데이터를 데이터베이스(SQL 등)가 이해할 수 있는 형식으로 변환하는 것도 이 계층의 역할입니다. 이 계층보다 안쪽의 코드는 데이터베이스에 대해 아무것도 알아서는 안 됩니다.&lt;/p>
&lt;h3 id="34-프레임워크와-드라이버-frameworks--drivers">3.4. 프레임워크와 드라이버 (Frameworks &amp;amp; Drivers)
&lt;/h3>&lt;p>가장 바깥쪽 계층은 데이터베이스, 웹 프레임워크 등과 같은 도구들로 구성됩니다. 여기서는 보통 안쪽의 원들과 통신하기 위한 &amp;lsquo;연결 코드(글루 코드)&amp;rsquo; 이외의 코드는 거의 작성하지 않습니다.
이 계층에는 모든 세부 사항이 보관됩니다. 웹은 세부 사항입니다. 데이터베이스는 세부 사항입니다. 피해를 최소화하기 위해 이러한 세부 사항을 바깥쪽에 배치합니다.&lt;/p>
&lt;h2 id="4-의존성-규칙-the-dependency-rule">4. 의존성 규칙 (The Dependency Rule)
&lt;/h2>&lt;p>클린 아키텍처를 성립시키기 위한 가장 중요하고 절대로 어겨서는 안 되는 규칙이 있습니다. 그것이 바로 &amp;lsquo;&lt;strong>의존성 규칙 (The Dependency Rule)&lt;/strong>&amp;lsquo;입니다.&lt;/p>
&lt;blockquote>
&lt;p>소스 코드의 의존성은 반드시 안쪽(상위 수준의 정책)을 향해야만 한다.&lt;/p>
&lt;/blockquote>
&lt;p>안쪽 원에 속하는 코드는 바깥쪽 원에 속하는 코드에 대해 아무것도 알아서는 안 됩니다. 바깥쪽 원에서 선언된 이름(함수, 클래스, 변수 등)을 안쪽 원에서 언급해서는 안 됩니다.
마찬가지로, 바깥쪽 원에서 사용되는 데이터 포맷을 안쪽 원에서 사용해서는 안 됩니다. 특히 그 포맷이 바깥쪽 원의 프레임워크에 의해 생성된 것이라면 더더욱 그렇습니다.&lt;/p>
&lt;pre class="mermaid">
graph LR
A[&amp;#34;프레임워크 및 드라이버&amp;#34;] --&amp;gt;| 의존 | B[&amp;#34;인터페이스 어댑터&amp;#34;]
B --&amp;gt;| 의존 | C[&amp;#34;유즈케이스&amp;#34;]
C --&amp;gt;| 의존 | D[&amp;#34;엔티티&amp;#34;]
style A fill:#f9f9f9,stroke:#333,stroke-width:2px
style B fill:#e6f7ff,stroke:#333,stroke-width:2px
style C fill:#fff0f6,stroke:#333,stroke-width:2px
style D fill:#f6ffed,stroke:#333,stroke-width:2px
&lt;/pre>
&lt;p>수학적으로 표현하면, 계층의 인덱스를 $L_i$ 라고 하고, $i=0$ 을 엔티티(가장 안쪽 계층), $i=3$ 을 프레임워크(가장 바깥쪽 계층)로 정의했을 때, 특정 계층 $L_m$ 에서 $L_n$ 으로의 의존성이 존재한다면 반드시 다음의 부등식이 성립해야 합니다.&lt;/p>
$$ m > n $$&lt;p>즉, 의존성 벡터 $\vec{D}$ 는 항상 중심을 향합니다.&lt;/p>
&lt;h2 id="5-경계를-넘어서-의존성-역전-원칙-dip">5. 경계를 넘어서: 의존성 역전 원칙 (DIP)
&lt;/h2>&lt;p>의존성 규칙을 준수하려고 하면 곧바로 하나의 큰 문제에 직면하게 됩니다. &amp;lsquo;&lt;strong>유즈케이스가 데이터베이스에서 데이터를 가져와야 할 경우 어떻게 해야 하는가?&lt;/strong>&amp;rsquo;&lt;/p>
&lt;p>유즈케이스 계층(안쪽)이 인터페이스 어댑터 계층(바깥쪽의 Repository 구현체)을 직접 호출하면 의존성이 바깥쪽을 향하게 되어, 의존성 규칙 위반이 됩니다.&lt;/p>
&lt;p>이 문제를 해결하는 것이 바로 SOLID 원칙 중 &amp;lsquo;D(Dependency Inversion Principle: 의존성 역전 원칙)&amp;lsquo;입니다.&lt;/p>
&lt;h3 id="의존성-역전-원칙-dip-의-정의">의존성 역전 원칙 (DIP) 의 정의
&lt;/h3>&lt;ol>
&lt;li>상위 모듈은 하위 모듈에 의존해서는 안 된다. 두 모듈 모두 추상화에 의존해야 한다.&lt;/li>
&lt;li>추상화는 세부 사항에 의존해서는 안 된다. 세부 사항이 추상화에 의존해야 참조해야 한다.&lt;/li>
&lt;/ol>
&lt;p>이를 구현하기 위해 유즈케이스 계층에 **인터페이스(추상화)**를 정의하고, 바깥쪽 계층(인터페이스 어댑터)에서 그 인터페이스를 &lt;strong>구현&lt;/strong>합니다. 유즈케이스 계층은 자신이 정의한 인터페이스에만 의존하며, 바깥쪽의 구체적인 구현에는 의존하지 않습니다.&lt;/p>
&lt;pre class="mermaid">
classDiagram
class UseCaseInteractor {
- UserRepository repository
+ execute()
}
class UserRepository {
&amp;lt;&amp;lt;Interface&amp;gt;&amp;gt;
+ findById(id)
+ save(user)
}
class UserRepositoryImpl {
+ findById(id)
+ save(user)
}
UseCaseInteractor --&amp;gt; UserRepository : &amp;#34;Depends on (의존)&amp;#34;
UserRepositoryImpl ..|&amp;gt; UserRepository : &amp;#34;Implements (구현)&amp;#34;
note for UseCaseInteractor &amp;#34;내부 계층 (유즈케이스)&amp;#34;
note for UserRepository &amp;#34;내부 계층 (유즈케이스)&amp;#34;
note for UserRepositoryImpl &amp;#34;외부 계층 (인터페이스 어댑터)&amp;#34;
&lt;/pre>
&lt;p>위의 그림에서 실행 시의 제어 흐름(Control Flow)은 &lt;code>UseCaseInteractor&lt;/code> $\rightarrow$ &lt;code>UserRepositoryImpl&lt;/code> 입니다. 하지만 소스 코드의 의존 관계(Source Code Dependency)는 &lt;code>UserRepositoryImpl&lt;/code> $\rightarrow$ &lt;code>UserRepository&lt;/code>(안쪽)가 됩니다. 다형성을 이용함으로써 제어 흐름과는 반대 방향으로 소스 코드의 의존 관계를 향하게 할 수 있었습니다. 이것이 &amp;lsquo;의존성의 &lt;strong>역전&lt;/strong>&amp;lsquo;이라고 불리는 이유입니다.&lt;/p>
&lt;h2 id="6-typescript를-활용한-구체적인-구현-예시">6. TypeScript를 활용한 구체적인 구현 예시
&lt;/h2>&lt;p>여기서는 TypeScript를 사용하여 클린 아키텍처의 간단한 구현 예시(사용자 등록 기능)를 보여줍니다.&lt;/p>
&lt;h3 id="61-엔티티-entities">6.1. 엔티티 (Entities)
&lt;/h3>&lt;p>가장 중심에 있는 비즈니스 규칙입니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// src/domain/entities/User.ts
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">export&lt;/span> &lt;span class="kr">class&lt;/span> &lt;span class="nx">User&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">constructor&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">public&lt;/span> &lt;span class="kr">readonly&lt;/span> &lt;span class="nx">id&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">public&lt;/span> &lt;span class="kr">readonly&lt;/span> &lt;span class="nx">name&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">public&lt;/span> &lt;span class="kr">readonly&lt;/span> &lt;span class="nx">email&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">public&lt;/span> &lt;span class="kr">readonly&lt;/span> &lt;span class="nx">createdAt&lt;/span>: &lt;span class="kt">Date&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span> &lt;span class="p">{}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 엔티티 고유의 비즈니스 규칙(예: 이름 길이 체크 등)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kr">public&lt;/span> &lt;span class="nx">isValid&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kr">boolean&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span> &lt;span class="o">&amp;gt;=&lt;/span> &lt;span class="mi">3&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">email&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">includes&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;@&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="62-유즈케이스-use-cases">6.2. 유즈케이스 (Use Cases)
&lt;/h3>&lt;p>유즈케이스 계층에서는 입력과 출력의 데이터 구조(DTO)와 의존성을 역전시키기 위한 리포지토리 인터페이스를 정의합니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;span class="lnt">56
&lt;/span>&lt;span class="lnt">57
&lt;/span>&lt;span class="lnt">58
&lt;/span>&lt;span class="lnt">59
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// src/application/repositories/UserRepository.ts
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">User&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="kr">from&lt;/span> &lt;span class="s1">&amp;#39;../../domain/entities/User&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 유즈케이스 계층이 정의하는 인터페이스
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">export&lt;/span> &lt;span class="kr">interface&lt;/span> &lt;span class="nx">UserRepository&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">findByEmail&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">email&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">Promise&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">User&lt;/span> &lt;span class="err">|&lt;/span> &lt;span class="na">null&lt;/span>&lt;span class="p">&amp;gt;;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">save&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">user&lt;/span>: &lt;span class="kt">User&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">Promise&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">void&lt;/span>&lt;span class="p">&amp;gt;;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// src/application/usecases/RegisterUser/RegisterUserDTO.ts
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">export&lt;/span> &lt;span class="kr">interface&lt;/span> &lt;span class="nx">RegisterUserInputDTO&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">name&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">email&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">export&lt;/span> &lt;span class="kr">interface&lt;/span> &lt;span class="nx">RegisterUserOutputDTO&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">id&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">name&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">email&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">createdAt&lt;/span>: &lt;span class="kt">Date&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// src/application/usecases/RegisterUser/RegisterUserUseCase.ts
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">User&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="kr">from&lt;/span> &lt;span class="s1">&amp;#39;../../domain/entities/User&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">UserRepository&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="kr">from&lt;/span> &lt;span class="s1">&amp;#39;../../repositories/UserRepository&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">RegisterUserInputDTO&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">RegisterUserOutputDTO&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="kr">from&lt;/span> &lt;span class="s1">&amp;#39;./RegisterUserDTO&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">export&lt;/span> &lt;span class="kr">class&lt;/span> &lt;span class="nx">RegisterUserUseCase&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 추상(인터페이스)에 의존한다. 구상에는 의존하지 않는다.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kr">constructor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kr">private&lt;/span> &lt;span class="kr">readonly&lt;/span> &lt;span class="nx">userRepository&lt;/span>: &lt;span class="kt">UserRepository&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">public&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="nx">execute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">input&lt;/span>: &lt;span class="kt">RegisterUserInputDTO&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">Promise&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">RegisterUserOutputDTO&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">existingUser&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">userRepository&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">findByEmail&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">input&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">email&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">existingUser&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">throw&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nb">Error&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;User already exists&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">newUser&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">User&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">crypto&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">randomUUID&lt;/span>&lt;span class="p">(),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">input&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">input&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">email&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">new&lt;/span> &lt;span class="nb">Date&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">newUser&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">isValid&lt;/span>&lt;span class="p">())&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">throw&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nb">Error&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Invalid user data&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 바깥쪽의 DB 저장 처리를 호출하지만, 의존은 안쪽(인터페이스)을 향하고 있다
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">userRepository&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">save&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">newUser&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">id&lt;/span>: &lt;span class="kt">newUser.id&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">name&lt;/span>: &lt;span class="kt">newUser.name&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">email&lt;/span>: &lt;span class="kt">newUser.email&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">createdAt&lt;/span>: &lt;span class="kt">newUser.createdAt&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="63-인터페이스-어댑터-interface-adapters">6.3. 인터페이스 어댑터 (Interface Adapters)
&lt;/h3>&lt;p>데이터베이스로의 구체적인 접근 처리(Repository 구현)와 HTTP 요청을 처리하는 Controller를 작성합니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// src/adapters/repositories/PostgresUserRepository.ts
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">UserRepository&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="kr">from&lt;/span> &lt;span class="s1">&amp;#39;../../application/repositories/UserRepository&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">User&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="kr">from&lt;/span> &lt;span class="s1">&amp;#39;../../domain/entities/User&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 바깥쪽 계층(Driver)인 DB 클라이언트를 가정
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">DatabaseClient&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="kr">from&lt;/span> &lt;span class="s1">&amp;#39;../../infrastructure/database/DatabaseClient&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">export&lt;/span> &lt;span class="kr">class&lt;/span> &lt;span class="nx">PostgresUserRepository&lt;/span> &lt;span class="kr">implements&lt;/span> &lt;span class="nx">UserRepository&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">constructor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kr">private&lt;/span> &lt;span class="kr">readonly&lt;/span> &lt;span class="nx">dbClient&lt;/span>: &lt;span class="kt">DatabaseClient&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">public&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="nx">findByEmail&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">email&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">Promise&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">User&lt;/span> &lt;span class="err">|&lt;/span> &lt;span class="na">null&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">record&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">dbClient&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">query&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;SELECT * FROM users WHERE email = $1&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="nx">email&lt;/span>&lt;span class="p">]);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">record&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">User&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">record&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">id&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">record&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">record&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">email&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">record&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">created_at&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">public&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="nx">save&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">user&lt;/span>: &lt;span class="kt">User&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">Promise&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">void&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">dbClient&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">query&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;INSERT INTO users (id, name, email, created_at) VALUES ($1, $2, $3, $4)&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">[&lt;/span>&lt;span class="nx">user&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">id&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">user&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">user&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">email&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">user&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">createdAt&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// src/adapters/controllers/UserController.ts
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">RegisterUserUseCase&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="kr">from&lt;/span> &lt;span class="s1">&amp;#39;../../application/usecases/RegisterUser/RegisterUserUseCase&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">export&lt;/span> &lt;span class="kr">class&lt;/span> &lt;span class="nx">UserController&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">constructor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kr">private&lt;/span> &lt;span class="kr">readonly&lt;/span> &lt;span class="nx">registerUserUseCase&lt;/span>: &lt;span class="kt">RegisterUserUseCase&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">public&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="nx">register&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">req&lt;/span>: &lt;span class="kt">any&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">res&lt;/span>: &lt;span class="kt">any&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">Promise&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">void&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">try&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">input&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">name&lt;/span>: &lt;span class="kt">req.body.name&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">email&lt;/span>: &lt;span class="kt">req.body.email&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">output&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">registerUserUseCase&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">execute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">input&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">status&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">201&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">json&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">output&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span> &lt;span class="k">catch&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">error&lt;/span>: &lt;span class="kt">any&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">res&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">status&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">400&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">json&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">message&lt;/span>: &lt;span class="kt">error.message&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="64-메인-컴포넌트-의존성-주입-di">6.4. 메인 컴포넌트 (의존성 주입: DI)
&lt;/h3>&lt;p>애플리케이션 시작 시 모든 의존 관계를 구성(와이어링)합니다. 이를 Composition Root라고 부릅니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// src/infrastructure/web/server.ts
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">import&lt;/span> &lt;span class="nx">express&lt;/span> &lt;span class="kr">from&lt;/span> &lt;span class="s1">&amp;#39;express&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">DatabaseClient&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="kr">from&lt;/span> &lt;span class="s1">&amp;#39;../database/DatabaseClient&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">PostgresUserRepository&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="kr">from&lt;/span> &lt;span class="s1">&amp;#39;../../adapters/repositories/PostgresUserRepository&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">RegisterUserUseCase&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="kr">from&lt;/span> &lt;span class="s1">&amp;#39;../../application/usecases/RegisterUser/RegisterUserUseCase&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">UserController&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="kr">from&lt;/span> &lt;span class="s1">&amp;#39;../../adapters/controllers/UserController&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">app&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">express&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">app&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">use&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">express&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">json&lt;/span>&lt;span class="p">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 1. 드라이버 초기화
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">dbClient&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">DatabaseClient&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="cm">/* 접속 정보 */&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 2. 어댑터 초기화 (구상 클래스를 인스턴스화)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">userRepository&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">PostgresUserRepository&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">dbClient&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 3. 유즈케이스 초기화 (인터페이스에 구상을 주입 = DI)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">registerUserUseCase&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">RegisterUserUseCase&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">userRepository&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 4. 컨트롤러 초기화
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">userController&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">UserController&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">registerUserUseCase&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 라우팅
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">app&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">post&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/users&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">req&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="nx">userController&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">register&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">req&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">res&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">app&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">listen&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">3000&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Server is running on port 3000&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>이처럼 가장 바깥쪽의 &amp;lsquo;시작 스크립트&amp;rsquo;가 더러운 세부 사항(구상 클래스의 인스턴스화)을 맡고, 안쪽 계층에는 깔끔한 인터페이스만 전달하는 구조로 만들면 비즈니스 로직은 외부 세계로부터 완전히 격리됩니다.&lt;/p>
&lt;h2 id="7-결합도와-응집도의-수학적-고찰">7. 결합도와 응집도의 수학적 고찰
&lt;/h2>&lt;p>소프트웨어 공학에서 아키텍처의 품질을 평가하는 지표로 **결합도(Coupling)**와 **응집도(Cohesion)**가 있습니다.&lt;/p>
&lt;p>결합도 $C$ 란 모듈 간 의존성의 강도를 나타냅니다. 모듈 $A$ 가 모듈 $B$ 에 의존하는 경우, 시스템의 총 의존 관계 수를 $N_{dep}$, 모듈 수를 $N_{mod}$ 라고 할 때, 복잡도를 나타내는 지표 중 하나는 다음과 같이 표현할 수 있습니다.&lt;/p>
$$ Complexity \propto \frac{N_{dep}}{N_{mod}} $$&lt;p>클린 아키텍처에서 DIP를 적용함으로써 물리적인 의존성의 화살표를 추상화로 향하게 합니다. 추상화(인터페이스)의 변경 빈도(Instability: $I$)는 매우 낮게 설계됩니다.&lt;/p>
&lt;p>Instability $I$ 는 다음 공식으로 계산됩니다(Robert C. Martin의 정의).&lt;/p>
&lt;ul>
&lt;li>$C_e$ (Efferent Coupling): 바깥을 향한 결합 (자신이 의존하고 있는 수)&lt;/li>
&lt;li>$C_a$ (Afferent Coupling): 안을 향한 결합 (자신에게 의존하고 있는 수)&lt;/li>
&lt;/ul>
$$ I = \frac{C_e}{C_e + C_a} $$&lt;ul>
&lt;li>$I = 0$ 인 경우, 해당 컴포넌트는 완전히 안정적입니다(아무에게도 의존하지 않으며, 다른 곳에서 의존받고 있음).&lt;/li>
&lt;li>$I = 1$ 인 경우, 해당 컴포넌트는 완전히 불안정합니다(다른 곳에서 의존받지 않으며, 다른 곳에 의존하고 있음).&lt;/li>
&lt;/ul>
&lt;p>클린 아키텍처의 &amp;lsquo;엔티티 계층&amp;rsquo;은 $C_e = 0$(바깥쪽에 의존하지 않음)이므로 $I = 0$ 이 됩니다. 즉, 가장 안정적인 계층입니다.
반대로 &amp;lsquo;UI 계층&amp;rsquo;이나 &amp;lsquo;DB 계층&amp;rsquo;은 $C_a \approx 0$ 이고 $C_e > 0$ 이기 때문에 $I \approx 1$ 이 되어, 쉽게 변경 가능한 계층(불안정한 계층)이 됩니다.&lt;/p>
&lt;p>아키텍처의 중요한 원칙인 SDP(Stable Dependencies Principle: 안정된 의존성 원칙)는 &amp;lsquo;&lt;strong>의존성은 더 안정된 컴포넌트($I$가 작은 컴포넌트)를 향해야 한다&lt;/strong>&amp;lsquo;고 규정하고 있습니다. 클린 아키텍처의 동심원은 바로 이 SDP를 시각화한 것이며, 바깥쪽($I=1$)에서 안쪽($I=0$)으로 의존성이 향하도록 설계되어 있습니다.&lt;/p>
&lt;h2 id="8-테스트-전략과-클린-아키텍처">8. 테스트 전략과 클린 아키텍처
&lt;/h2>&lt;p>클린 아키텍처의 가장 큰 장점 중 하나는 &lt;strong>테스트의 용이성&lt;/strong>입니다. 계층이 분리되어 있기 때문에 각 계층에 맞는 테스트를 독립적으로 작성할 수 있습니다.&lt;/p>
&lt;h3 id="81-엔티티-테스트-unit-test">8.1. 엔티티 테스트 (Unit Test)
&lt;/h3>&lt;p>외부에 대한 의존성이 전혀 없는 순수한 로직이므로 DB도 모킹도 필요하지 않습니다. 가장 빠르게 실행할 수 있는 확실한 테스트가 됩니다.&lt;/p>
&lt;h3 id="82-유즈케이스-테스트-unit-test-with-mocks">8.2. 유즈케이스 테스트 (Unit Test with Mocks)
&lt;/h3>&lt;p>리포지토리 등의 외부 의존성은 모두 인터페이스로 정의되어 있기 때문에, 테스트 시에는 **테스트용 모의 객체(Mock)나 인메모리 구현체(Fake)**를 주입(DI)하기만 하면 됩니다. 실제 데이터베이스를 띄울 필요는 없습니다. 이를 통해 비즈니스 로직의 복잡한 분기나 예외 처리를 고속으로 테스트할 수 있습니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 유즈케이스의 테스트 예시 (Jest를 가정)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;기존 이메일 주소로 가입하려고 하면 에러가 발생한다&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Fake 리포지토리 생성
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">mockRepo&lt;/span>: &lt;span class="kt">UserRepository&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">findByEmail&lt;/span>: &lt;span class="kt">async&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">email&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">User&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;1&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Test&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">email&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nb">Date&lt;/span>&lt;span class="p">()),&lt;/span> &lt;span class="c1">// 기존 사용자를 반환
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">save&lt;/span>: &lt;span class="kt">async&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">user&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">useCase&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">RegisterUserUseCase&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">mockRepo&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 유즈케이스 실행 및 에러 단언(Assertion)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="nx">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">useCase&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">execute&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;Bob&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">email&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;test@example.com&amp;#39;&lt;/span> &lt;span class="p">}))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="nx">rejects&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="nx">toThrow&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;User already exists&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="83-어댑터-테스트-integration-test">8.3. 어댑터 테스트 (Integration Test)
&lt;/h3>&lt;p>리포지토리 구현 클래스는 실제로 데이터베이스에 연결하여 SQL이 올바른지 테스트합니다. 컨트롤러 테스트는 HTTP 요청을 받아 JSON을 반환하는 부분을 테스트합니다. 여기서는 비즈니스 로직의 상세한 검증은 수행하지 않으며, 어디까지나 &amp;lsquo;변환&amp;rsquo;과 &amp;lsquo;통신&amp;rsquo;이 올바른지 확인합니다.&lt;/p>
&lt;h2 id="9-클린-아키텍처의-단점과-채택해야-할-시기">9. 클린 아키텍처의 단점과 채택해야 할 시기
&lt;/h2>&lt;p>만능으로 보이는 클린 아키텍처지만 강력한 은탄환은 아닙니다. 다음과 같은 단점(트레이드오프)이 존재합니다.&lt;/p>
&lt;ol>
&lt;li>&lt;strong>초기 학습 비용과 개발 비용의 증가&lt;/strong>: 파일 수나 인터페이스(추상화)의 수가 대폭 증가합니다. DTO의 데이터 복사와 같은 &amp;lsquo;보일러플레이트 코드(상용구 코드)&amp;lsquo;가 많아집니다.&lt;/li>
&lt;li>&lt;strong>소규모 프로젝트에는 오버킬&lt;/strong>: 며칠 만에 만드는 프로토타입이나 변경이 거의 발생하지 않는 단발성 도구에서 이 아키텍처를 채택하는 것은 불필요한 비용 낭비가 되는 경우가 많습니다. CRUD 작업만 있는 단순한 API에도 적합하지 않습니다.&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>채택해야 할 시기&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>장기간(수년 이상) 유지보수 및 운영될 것으로 예상되는 제품.&lt;/li>
&lt;li>비즈니스 규칙이 복잡하고 사양 변경이 빈번하게 발생하는 시스템.&lt;/li>
&lt;li>대규모 개발 팀에서 분업(프론트엔드, 백엔드, 인프라 등)을 진행하고 싶은 경우.&lt;/li>
&lt;li>도메인 주도 설계(DDD: Domain-Driven Design)와 결합하여 복잡한 비즈니스 영역을 모델링하고 싶은 경우.&lt;/li>
&lt;/ul>
&lt;h2 id="10-요약">10. 요약
&lt;/h2>&lt;p>클린 아키텍처는 &amp;lsquo;비즈니스 규칙&amp;rsquo;이라는 시스템의 핵심을 UI나 데이터베이스, 프레임워크와 같은 &amp;lsquo;세부 사항&amp;rsquo;으로부터 보호하기 위한 설계 사상입니다.&lt;/p>
&lt;p>그 핵심을 이루는 것은 &lt;strong>의존성 규칙&lt;/strong>과 **의존성 역전 원칙(DIP)**입니다. 이를 올바르게 적용함으로써 소프트웨어는 변화에 유연해지고, 테스트가 쉬워지며, 오랜 기간에 걸쳐 그 가치를 계속 유지할 수 있습니다.&lt;/p>
&lt;p>중요한 것은 클린 아키텍처의 디렉토리 구조를 맹목적으로 모방하는 것이 아니라, &amp;lsquo;&lt;strong>왜 그렇게 분할하는가&lt;/strong>&amp;rsquo;, &amp;lsquo;&lt;strong>의존성의 화살표는 어디를 향하고 있는가&lt;/strong>&amp;lsquo;라는 본질을 이해하고, 자신의 프로젝트의 규모와 복잡성에 맞게 적절히 적용해 나가는 것입니다.&lt;/p>
&lt;hr>
&lt;p>&lt;em>Reference: &amp;ldquo;Clean Architecture: A Craftsman&amp;rsquo;s Guide to Software Structure and Design&amp;rdquo; by Robert C. Martin&lt;/em>&lt;/p></description></item></channel></rss>