postcss 를 사용해 hugo 테마에 tailwindcss 적용하기

Apply tailwindcss in an easy way by utilizing the functions provided by hugo.

·updated
// INDEX

hugo 로 블로그를 만들고 테마와 컨텐츠를 관리하고 있다.

hugo 의 pipe 기능과 postcss 를 이용해서 hugo 테마에 tailwindcss 를 적용하고, 개발 환경과 운영 환경에 맞게 css 파일을 빌드할 수 있도록 적용해보자.


필요 라이브러리 구성

테마에 관련된 디렉토리는 site/themes 하위에 존재한다.

사용하는 테마의 디렉토리로 이동하자.

cd themes/soyhenge

tailwindcss 설정에 필요한 라이브러리와 의존성은 npm 으로 관리하자.
npm 사용을 위해선 npm 이 설치되어 있어야 하며 node 를 설치하면 된다.

npm init -y

명령어를 실행하면 package.json 파일이 생성된다.
필요한 의존성을 다운받는다.

npm install --save-dev autoprefixer postcss postcss-cli tailwindcss @tailwindcss/aspect-ratio @tailwindcss/forms @tailwindcss/typography

각각의 라이브러리의 사용 목적은 다음과 같다.

  • autoprefixer: 브라우저 호환성을 위해 CS S에 -webkit-, -moz- 등의 접두사를 자동으로 추가하는 플러그인.
  • postcss: 플러그인을 사용하여 CSS 를 변환하는 도구.
  • postcss-cli: 커맨드 라인에서 PostCSS 를 사용하기 위한 도구.
  • tailwindcss: 유틸리티 기반의 프론트엔드 CSS 프레임워크.
  • @tailwindcss/aspect-ratio: TailwindCSS 로 이미지 비율을 설정하기 위한 플러그인.
  • @tailwindcss/forms: TailwindCSS 로 폼 스타일을 설정하기 위한 플러그인.
  • @tailwindcss/typography: TailwindCSS 로 폰트 및 마크다운 스타일링을 위한 플러그인.

package.json 을 확인하면 다음과 같이 개발 의존성에 라이브러리와 플러그인이 추가되어 있을것이다.
버전 정보는 다를 수 있다.

# package.json 
...
"devDependencies": {
  "@tailwindcss/aspect-ratio": "^0.4.x",
  "@tailwindcss/forms": "^0.5.x",
  "@tailwindcss/typography": "^0.5.x",
  "autoprefixer": "^10.x.x",
  "postcss": "^8.x.x",
  "postcss-cli": "^11.0.0",
  "tailwindcss": "^3.4.x"
}

autoprefixer 의 사용 가능한 브라우저 밴더에 대한 접두사 설정을 위해 browserslist 에 대한 내용도 추가하자.

# package.json 
...
"devDependencies": {
  ...
},
"browserslist": [
  "last 1 version",
  "> 1%",
  "maintained node versions",
  "not dead"
],

tailwindcss & postcss 사용을 위한 구성 설정

assets 디렉토리는 hugo 에서 전역적으로 사용하는 resource 를 저장하는 디렉토리이다. 주로 이미지, css, js 등의 정적 파일을 저장하는 디렉토리이다. assets 하위에 디렉토리를 만들자.

mkdir -p assets/css && touch tailwindcss.config.js postcss.config.js styles.css

각 파일에 들어갈 내용은 다음과 같다.

postcss configuration

// postcss.config.js
const themeDir = __dirname + '/../../';
module.exports = {
    plugins: [
    require('tailwindcss')(themeDir + 'assets/css/tailwind.config.js'),
    require('autoprefixer')({
        path: [themeDir]
        })
    ]
}

themeDir 이라는 변수는 현재 파일의 디렉토리 상위 2단계 위 디렉토리를 나타낸다. my-theme/assets/css/postcss.config.js 기준 2단계 상위 디렉토리는 테마 디렉토리의 루트 디렉토리가 themeDir 이 된다.

postcss.config.js 의 설정은 postcss 에서 사용할 plugin 에 대한 설정이다.

  • tailwindcss 를 이용해 tailwindcss 형식의 css 를 브라우저가 이해할 수 있는 css 파일로 변환한다. tailwindcss 구성 파일 경로를 포함한다.
  • autoprefixer 를 이용해 브라우저 호환성을 위한 접두사를 추가한다. 앞서 설정한 browserslist 를 찾기 위한 path 설정을 포함한다.

tailwindcss configuration

// tailwindcss.config.js
module.exports = {
    variants: {},
    content: [
      "./themes/**/layouts/*.html",
      "./themes/**/layouts/partials/**/*.html",
      "./themes/**/layouts/**/*.html",
      "./layouts/*.html",
      "./layouts/**/*.html",
    ],
    theme: {
        extend: {},
    },
    plugins: [
        require('@tailwindcss/aspect-ratio'),
        require('@tailwindcss/forms'),
        require('@tailwindcss/typography'),
    ]
}

tailwindcss.config.js 의 설정은 tailwindcss 를 사용하기 위한 설정이다. tailwindcss 를 사용할 content 경로와, 앞서 설치한 plugin 을 정의하고 다른 항목은 기본값으로 충분하다.

styles css

/* styles.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind variants;

styles.css 에선 사용할 tailwindcss 의 지시어를 선언한다. 해당 파일을 읽어 postcss 가 브라우저가 이해 가능한 css 파일로 변환해 줄 것이다.


hugo pipe 와 html template

hugo 는 여러 html 파일을 레고처럼 조립해서 하나의 html 파일을 만들 수 있다. 이는 go 의 text/template 과 html/template 패키지를 사용하기 때문이다. go 의 template 문법을 사용하면 조립 가능한 레고 블록인 template 을 쉽게 만들 수 있다.

hugo 에서 layouts 디렉토리는 템플릿을 포함하는 디렉토리이다.
layouts/_default 와 layouts/partials 에 조립에 필요한 template 파일을 저장하는 기본적인 경로로 사용한다.

  • _default
    • 웹 사이트의 페이지마다 가장 기본이 되는 템플릿을 작성해두는 폴더.
    • baseof.html, list.html, single.html, taxonomy.html 등이 있다.
  • partials
    • 레이아웃의 어디서나 사용 가능한 작은 부품이 되는 템플릿을 선언하는 폴더.
    • 예를들어 head 태그 내의 meta 태그를 위한 meta.html, 네비게이션을 위한 nav.html 등을 선언하고 다른 템플릿에서 사용할 수 있다.

_default 디렉토리의 baseof.html 은 hugo 가 정적 페이지를 생성할 때 사용하는 가장 밑바탕이 되는 템플릿이다. 일반적으로 html 태그, head 와 body 태그를 선언한다.



layouts/_default/baseof.html

<!DOCTYPE html>
<html lang="{{ .Site.LanguageCode }}">
    {{- partial "head.html" . -}}

    <body>
        <main>
            {{- block "main" . -}}{{- end -}}
        </main>
    </body>
</html>

Go 에서 템플릿 변환 시 치환하거나 처리가 필요한 부분이라고 명시적으로 작성하는 방법은 중괄호 두개 사이에 템플릿 문법을 작성하는 것이다. Go 의 템플릿 문법과 그것을 확장한 Hugo 의 템플릿 문법은 다양한 기능을 지원한다. 여기서는 템플릿 문법이라고 표현할것이다.

더 자세한 사용법을 알아보기 위해선 template api 문서 와 hugo 공식 페이지에서 확인해보자.

사용한 템플릿 문법에대한 간단한 설명은 다음과 같다.

  • {{ .Site.LanguageCode }} : hugo 전역에서 접근 가능한 site object 의 language code 값을 html 태그의 lang attribute 로 설정한다.
  • partial : partials.Include 함수의 alias 인 partial 은 hugo 의 확장 문법으로 부분적으로 선언된 템플릿의 결과를 가지고온다. 기본적으로 rendering output 을 반환한다.
    • hugo 의 모든 partials 는 layouts/partials 디렉토리에 저장되어야 하며, partials 하위에 디렉토리 구조로 관리할 수 있다.
  • . (dot) : 현재 컨텍스트를 의미한다. 컨텍스트란 각각의 템플릿에 전달되는 데이터이다.
  • {{- , -}} : 이전 내용의 공백 제거, 이후에 오는 내용의 공백을 제거를 한다. 불필요한 whitespace 를 줄일 수 있다.
  • block : end 와 함께 사용하며, block 에 선언된 내용을 기본으로 사용하며, 동일한 이름 (위에선 main) 으로 선언된 define 구문이 있다면 해당 부분으로 내용이 대체된다.
    • main 하위에 올 수 있는 내용은 다양하기 때문에 partial, template 과 같은 고정된 템플릿 형태가 아닌 대체될 수 있는 block 으로 설정했다.



layouts/partials/head.html

<head>
    <meta charset="utf-8" />
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <title>{{- .Site.Title -}}</title>

    {{- if .Description }}
        <meta name="description" content="{{ .Description }}" />
    {{ end -}}
    
    {{ partialCached "css.html" . }}
</head>

head 태그 부분만 똑 떼서 작성한 것으로 해당 부분이 그대로 baseof.html 의 {{- partial “head” . -}} 부분으로 치환된다. 각 페이지의 head 부분에 대해서 baseof.html 과 따로 관리를 위해 partial 로 선언한다.

이렇게 여러 부품으로 분할된 html 템플릿을 하나로 조립하여 정적 사이트로 생성해주는 것이 hugo 의 기능이다.


여기서 사용된 템플릿 문법은 다음과 같다.

  • .Site.Title : 전역에서 접근할 수 있는 site object 에서 title 변수를 가지고 온다.
    • hugo.toml 혹은 config.toml 에 선언한 title 을 기본으로 사용한다. 참고
  • if : end 와 같이 사용되며 조건 처리를 하는 경우에 사용된다. 위의 경우 content 의 front matter 에 description 이 있는 경우 description 을 meta 태그로 사용해 seo 최적화에 도움이 될 수 있다.
  • partialCached :partials.IncludeCached 의 alias 인 partialCached 는 페이지가 요청될 때 마다 다시 값을 요청할 필요가 없는 partial 대해 캐싱을 허용하는 hugo 문법이다.
    • 위의 경우 partials 폴더에 선언된 css.html 을 부분 캐싱 한다는 의미이다. 참고



layouts/partials/css.html

{{- $styles := resources.Get "css/styles.css" | postCSS (dict "config" "./assets/css/postcss.config.js") -}}

{{ printf $styles }}
{{- if hugo.IsServer }}
    {{ $styles = $styles | resources.ExecuteAsTemplate (printf "css/styles.%v.css" now.UnixMilli) . }}
    <link rel="stylesheet" href="{{ $styles.RelPermalink }}">
{{ else }}
    {{ $styles = $styles | minify | fingerprint | resources.PostProcess }}
    <link rel="stylesheet" href="{{ $styles.RelPermalink }}" integrity="{{ $styles.Data.Integrity}}">
{{ end -}}

css.html 은 postcss 기능을 사용하여 tailwindcss 지시어를 css 로 변환하기 위해 가장 핵심적인 부분이다. \ css 파일을 동적으로 처리해서 개발 환경과 배포 환경에서 각각 다른 방식으로 css 파일을 처리한다.

전체적인 흐름은 다음과 같다.



1. css 파일 가지고 오기

{{- $styles := resources.Get "css/styles.css" | postCSS (dict "config" "./assets/css/postcss.config.js") -}}
  • $styles : 변수를 선언하는 템플릿 문법이다. 여기선 $styles 라는 변수명으로 선언했다.
  • resources.Get "css/styles.css": resources.Get 함수는 지정된 경로에서 전역으로 사용 가능한 리소스 파일을 가지고 온다.
    • 리소스는 기본적으로 assets 디렉토리에서 가지고 온다. 앞서 만든 tailwindcss 의 지시어가 작성된 css/styles.css 파일을 상대 경로로 지정한다.
  • | : hugo 의 |(pipe) 는 assets 의 resource 를 처리할 수 있는 기능을 하는 모음이다.
    • pipe 함수를 파이프 이후에 작성하여 assets 에 대해 연속적인 처리가 가능하다.
    • 다양한 파이프 함수에 대해서 확인해 볼 수 있다. 참고
  • postCSS (dict "config" "./assets/css/postcss.config.js"): postCSS 함수는 hugo 에서 지원하는 pipe 함수로 PostCSS 를 사용하여 CSS 파일을 처리를 도와준다.
    • 여기서는 dict 함수를 사용해 config 라는 key 값에 PostCSS 설정 파일(postcss.config.js) 의 경로로 지정한 map 을 만들어 postCSS 함수의 옵션으로 전달한다. 참고



2. 개발 모드에서 css 파일 처리

{{- if hugo.IsServer }}
    {{ $styles = $styles | resources.ExecuteAsTemplate (printf "css/styles.%v.css" now.UnixMilli) . }}
    <link rel="stylesheet" href="{{ $styles.RelPermalink }}">
{{ else }}

앞서 선언한 $styles 변수에는 postCSS 가 실행된 css 파일이 resources 의 형태로 할당되어 있을것이다. assets 의 resources 를 처리하기 위해 파이프를 사용해서 처리한다.

  • if hugo.IsServer: hugo.IsServer는 현재 Hugo 가 개발 서버 모드에서 실행되고 있는지를 확인하는 조건문이다. 참고
  • {{ $styles = $styles | resources.ExecuteAsTemplate (printf "css/styles.%v.css" now.UnixMilli) . }}
    • resources.ExecuteAsTemplate 함수는 리소스를 반환하며, 주어진 컨텍스트로 구문 분석하고 실행한 후 대상 경로를 캐시 키로 사용하여 결과를 캐싱한다.
    • printf "css/styles.%v.css" now.UnixMilli 는 printf 로 target path 를 문자열로 제공하기 위한 함수를 사용하며, 현재 시간을 밀리초 단위로 포함하는 파일 이름을 생성하여 개발 서버의 캐시를 방지한다.
  • <link rel="stylesheet" href="{{ $styles.RelPermalink }}">: 처리된 스타일시트를 HTML에 링크한다. $styles 변수엔 처리된 css 가 resources 타입으로 있으며 RelPermalink 로 링크 경로를 참조할 수 있다.



3. 운영 모드에서 css 파일 처리

{{ $styles = $styles | minify | fingerprint | resources.PostProcess }}
	<link rel="stylesheet" href="{{ $styles.RelPermalink }}" integrity="{{ $styles.Data.Integrity}}">
{{ end -}}

운영 모드에선 개발 서버와 다르게 계속해서 파일을 업데이트 할 필요가 없다. 따라서, 최소화 된 파일로 변환한다. 여기서도 resources 의 처리를 위해 pipe 함수를 사용한다.

  • {{ $styles = $styles | minify | fingerprint | resources.PostProcess }}: assets 의 resources 를 처리하기 위한 pipe 함수를 사용해 운영 환경의 css 파일을 처리한다.
    • minify: 스타일시트를 압축하여 파일 크기를 줄인다.
    • fingerprint: 파일의 무결성을 확인하기 위해 해시를 생성하고 파일 이름에 해시를 추가한다.
    • resources.PostProcess: 빌드 후 리소스를 변환할 수 있도록 지연시켜준다.
  • <link rel="stylesheet" href="{{ $styles.RelPermalink }}" integrity="{{ $styles.Data.Integrity}}">: 처리된 스타일시트를 HTML에 링크한다. integrity 속성은 해시를 포함하여 파일의 무결성을 검증할 수 있게 도와준다.

운영모드와 개발모드의 스타일시트의 링크는 다음과 같은 차이를 볼 수 있다.

운영 모드 Prod mode stylesheet link

개발 모드 Dev mode stylesheet link


tailwindcss 를 theme 에 적용하기

tailwindcss 사용을 위한 준비는 끝났다. 이제 사용해보자.

layouts/_default/baseof.html

<!DOCTYPE html>
<html lang="{{ .Site.LanguageCode }}">
    {{- partial "head" . -}}

    <body class="flex flex-col min-h-screen bg-slate-500 text-slate-50Í ">
        <main class="flex-1 grow mx-auto w-screen max-w-screen-2xl px-6 pt-4">
            {{- block "main" . -}}{{- end -}}
        </main>
    </body>
</html>

layouts/index.html

{{ define "main" }}
    <article class="justify-self-center mx-auto text-center ">
        <h1 class="font-serif font-extralight text-5xl">Hello ✋</h1>
    </article>
{{ end }}

layouts 폴더의 index.html 파일은 홈페이지 변환시 사용된다. \ baseof.html 에 main 이란 이름을 가진 block 을 치환하기 위해 동일한 이름을 가진 define 구문을 정의한다.

  • define : block 혹은 template 으로 사용할 수 있는 부분을 정의한다.

이제 테마 폴더를 벗어나 프로젝트의 루트로 이동해 정적 사이트를 빌드해보자.

pwd  # 현재 경로는 ../my-site/themes/my-new-theme 이라고 가정한다.
cd ../..
hugo -D

hugo -D 는 draft 상태의 content 를 포함하여 정적 페이지를 빌드한다는 hugo 명령어다. 위 명령어를 실행하면 my-site 의 루트 폴더에 public 이라는 폴더가 에러없이 생성되어야 한다.

public folder 는 hugo 가 빌드된 결과물이 생성되는 폴더이다.

After hugo build

자세한 구성은 다를 수 있지만 css 폴더 와 index.html 은 있을것이다.

css 폴더에 하위의 css 파일은 tailwindcss 지시어로 선언된 파일을 postcss 처리를 통하여 브라우저가 이해할 수 있는 css 로 변환된 파일이다. \ 이는 hugo 에서 지원하는 asset management 기능을 통해 layouts/partials/css.html 에 선언한 과정을 수행하는 것이다.
hugo 덕분에 번거로운 과정 없이 hugo site 빌드를 통해서 tailwindcss 처리가 가능하도록 설정했다.

서버를 실행해보자.

hugo server -D

위 과정을 거치면 hugo 는 빌드 이후 서버를 1313 포트에서 실행한다.

Hugo server running terminal

개발 모드에서 실행되었으며, 접근 가능 url 은 localhost:1313 이다.
들어가서 확인해보면 설정한 tailwindcss 가 적절하게 반영되어 있다.

Homepage