🏷️
Jekyll 포스트에 태그 기능 추가하기

Jekyll에서 포스트 별로 태그를 추가하고 관리하는 방법에 대해 알아봅니다.

Posted by 재그지그 on September 22, 2019

Blog Jekyll


미리보기

여러 포스트가 있는 블로그에서 특정 주제와 관련된 포스트만 보고 싶을 때가 있습니다. 이럴 때 필요한 기능이 바로 태그인데요, 아쉽게도 Jekyll에서는 기본적으로 포스트 태그와 관련된 기능들을 지원하지는 않습니다. 테마에 따라 기본적으로 해당 기능이 구현되어 있을 수 있으나, 제 블로그의 경우에는 이를 지원하지 않았습니다.

정확히 이야기하자면 Jekyll 공식 문서에서도 태그와 관련된 설명이 나오긴 합니다. 그저 간단하게 각 포스트의 머릿말(front matter)에서 태그(tags) 배열을 선언하는 방식으로 사용할 수 있습니다. 하지만 이는 그저 스트링이 담긴 배열일 뿐, 태그를 활용한 여러 기능들은 직접 구현해야 합니다.

따라서 이번 포스트에서는 Jekyll에서 포스트 별 태그 기능과 태그 목록 페이지를 구현한 방법에 대해 소개합니다. 구현 예시는 위의 이미지를 통해 확인하실 수 있습니다.

이번 포스트는 튜토리얼 글이지만, 여러분의 사전 지식이 조금 필요합니다. HTML, CSS, JavaScript와 함께 Liquid라는 템플릿 언어에 대한 이해도를 필요로 합니다. 다만 JavaScript 코드를 읽고 쓰는 데 어려움이 없다면 Liquid 문법은 따로 공부하지 않아도 충분히 이해하실 수 있을 것입니다.

만들어 볼 기능

  • 포스트 별 태그 기능
  • 태그 목록을 별도의 파일로 관리
  • 태그를 보여줄 때에는 별도의 포매팅 작업을 거쳐 보여주기
  • 포스트 목록에서 태그 별 필터링 기능
  • 외부에서 태그 클릭 시 해당 태그로 필터링 된 포스트 목록으로 이동

포스트에 태그 추가하기

이 부분은 Jekyll 공식 문서에서도 설명되어 있습니다.

포스트에 태그를 추가하는 방법은 정말 간단합니다. Jekyll의 각 포스트에는 YAML 타입으로 선언된 머릿말이 있는데, 거기에다가 tags 변수를 그저 추가하기만 하면 됩니다.

---
layout: post
tags: [blog, jekyll]
---

이렇게 하면 해당 포스트에는 "blog""jekyll"이라는 문자열이 담긴 배열이 선언됩니다. 하지만 이를 실제로 렌더링된 페이지에서 보기 위해서는 별도로 처리가 필요합니다.

Jekyll의 각 포스트는 렌더링되는 HTML 파일의 이름을 layout이라는 변수 안에 선언하고 있습니다. 위의 예시의 경우 이 포스트는 layout의 값이 post이므로 post.html 안에 들어갈 것입니다. 따라서 _layout 디렉토리 안에 들어가 있는 post.html 파일을 수정해야 합니다.

해당 HTML 파일을 열어보면 {{ content }}라고 적힌 부분을 확인할 수 있는데, 이 부분에 우리가 작성한 포스트가 삽입될 것입니다. 따라서 이 위치를 참고해서 태그를 보여주고 싶은 부분에 아래와 같은 코드를 넣어줍니다.

{% if page.tags %}
  {% for tag in page.tags %}
    <span class="tag">{{ tag }}</span>
  {% endfor %}
{% endif %}

Jekyll에는 기본적으로 몇 개의 전역 변수들이 선언되어 있는데, 그 중 하나가 page입니다. 이 page에는 아까 각 포스트의 머릿말에 포함된 변수들이 포함되어 있기 때문에, page.tags와 같은 문법으로 태그 목록을 불러올 수 있습니다.

따라서 위의 문법은 만약 현재 보고 있는 포스트 안에 tags라는 변수가 머릿말에 선언되어 있다면, 이를 반복해서 <span> 태그 안에 넣어 렌더링해달라는 의미인 것이죠. 아래는 위의 코드를 넣고 실행시켰을 때 나온 결과물입니다.

미리보기

태그 목록을 별도의 파일로 관리하기

위의 방식으로만 태그를 구현해도 되지만, 관리의 용이성을 생각한다면 별도로 태그를 저장하는 파일을 따로 만드는 것이 좋습니다. 만약 내가 여태껏 작성한 포스트에서 사용된 모든 태그 목록을 보고 싶다면? 태그를 별도의 파일로 구분해서 관리하지 않았다면 힘든 작업이 될 것입니다.

이를 위해 Jekyll에서는 마치 데이터베이스처럼 전역에서 사용할 수 있는 데이터 파일을 지원하고 있습니다. _data라는 디렉토리 안에 JSON, YAML, CSV 파일을 넣는 것으로 사용할 수 있죠.

저는 _data 디렉토리 아래에 tags.yml 이라는 파일을 아래와 같은 내용으로 만들었습니다.

- javascript
- frontend
- blog
- jekyll
- ruby
...

이제 각 포스트 머릿말에 담길 태그들은 tags.yml에 저장된 태그만을 사용할 것이기 때문에, post.html 내부의 파일을 조금 수정해줍니다.

{% if page.tags %}
  {% for tag in page.tags %}
    {% if site.data.tags contains tag %}
      <span class="tag">{{ tag }}</span>
    {% endif %}
  {% endfor %}
{% endif %}

{% if site.data.tags contains tag %}라는 부분을 추가함으로써, 만약 사전에 _data 디렉토리 내부의 tags.yml의 배열에 포함되지 않은 태그들은 출력되지 않을 것입니다.

---
layout: post
tags: [blog, jekyll, mistake]
---

미리보기

위의 예시처럼 mistake라는 미리 정의되지 않은 태그를 넣으면 포스트에 출력되지 않습니다.

태그를 출력할 때 포매팅 작업하기

여태껏 만든 태그들이 모두 영소문자 문자열로 적힌 것을 눈치채셨나요? 만약 javascript라는 태그를 썼는데 실제로 출력될 때는 JavaScript, 또는 자바스크립트라고 출력되기를 원한다면 어떻게 해야 할까요? javascript라는 태그 외에 또 다른 새로운 태그를 추가해야 하는 걸까요?

이 때 필요한 것이 바로 실제 데이터 값들을 출력하기 전에 미리 라벨(label)을 입히는 포매팅 작업입니다. 저장되는 데이터들을 일관성있게 유지하면서도 사용자에게는 시각적으로 다른 값을 보여줄 수 있죠.

이를 위해서 _data 디렉토리 아래에 format.yml 파일을 만들고 아래와 같이 키(key)와 밸류(value)를 선언해줍니다.

javascript: JavaScript
frontend: FrontEnd
blog: Blog
jekyll: Jekyll
ruby: Ruby
...

post.html 역시 아래와 같은 형태로 바꾸어줍니다.

{% if page.tags %}
  {% for tag in page.tags %}
    {% if site.data.tags contains tag %}
      <span class="tag">{{ site.data.format[tag] }}</span>
    {% endif %}
  {% endfor %}
{% endif %}

위 방법은 format.yml에 저장된 키 값들을 확인해서 일치하는 키 값의 밸류를 대신 출력해주는 방식입니다. 따라서 아래처럼 소문자로 태그를 작성했다 하더라도, 포매팅이 된 모습을 확인할 수 있습니다.

---
layout: post
tags: [blog, jekyll]
---

미리보기

포스트 목록에서 태그 별 필터링 기능

이제 모든 포스트 목록에서 태그 별 필터링을 할 수 있는 기능을 만들어 보도록 하겠습니다. 우선 개인의 테마에서 작성한 포스트 목록을 확인할 수 있는 HTML 파일로 이동합니다. 저 같은 경우에는 posts/index.html 파일에 들어있군요.

우선은 Jekyll에서 사용하고 있는 모든 태그를 보여주도록 합시다.

{% for tag in site.data.tags %}
  <span class="tag" data-tag="{{tag}}">
    {{ site.data.format[tag] }}
  </span>
{% endfor %}

이번에는 아까와 다르게 태그의 목록에 data-tag 속성이 추가로 붙은 것을 확인할 수 있습니다. 이를 이용해 현재 태그가 포매팅 되기 전의 밸류를 HTML 코드 내에 저장할 수 있습니다. 즉 개발자 도구를 이용해 검사해보면 아래와 같이 렌더링 되게 됩니다.

인스펙트

이제 이 태그를 클릭했을 때의 data-tag에 저장된 값을 얻어와서 해당 태그가 없는 포스트의 경우에는 숨겨주면 되겠군요. 그러기 위해서는 우선 각 포스트에도 태그 값들을 저장해야겠죠?

{% for post in site.posts %}
  <div class="post-wrapper"
    {% if post.tags %}
      {% for tag in post.tags %}
        data-{{ tag }}
      {% endfor %}
    {% endif %}>
    <article class="post-preview">
      ...
    </article>
  </div>

코드가 조금 장황한데, 쉽게 얘기해서 각 포스트에 들어있는 모든 태그들을 data-[태그값]들로 속성을 주고 있는 예시입니다. (이렇게 보니까 뭔가 PHP 코드 같네요…) 그 결과물은 아래와 같습니다.

인스펙트

각 포스트 별 태그가 data-[태그값]으로 잘 들어간 모습입니다. 그럼 이제 태그를 클릭하게 되면 필터링이 되는 로직을 작성해야겠죠?

  1. 클릭한 태그의 data-tag 속성에 저장된 값을 읽어온다
  2. 만약 이미 필터링 된 포스트들이 있다면, 기존의 필터링을 해제한다
  3. data-[태그값]들을 포함하고 있는 엘리먼트(.post-wrapper)를 순환하면서 태그값이 없는 경우 .hidden 클래스로 숨겨준다
  4. 태그가 선택되었다는 것을 시각적으로 표현하기 위해 선택된 태그에 .selected 클래스를 입혀준다
$("[data-tag]").click((e) => {
  currentTag = e.target.dataset.tag;
  filterByTagName(currentTag);
})

function filterByTagName(tagName) {
  $('.hidden').removeClass('hidden');
  $('.post-wrapper').each((index, elem) => {
    if (!elem.hasAttribute(`data-${tagName}`)) {
      $(elem).addClass('hidden');
    }
  });
  $(`.tag`).removeClass('selected');
  $(`.tag[data-tag=${tagName}]`).addClass('selected');
}

인스펙트

다행히 잘 동작하네요!

외부에서 태그 클릭 시 해당 태그로 필터링 된 포스트 목록으로 이동

이제 거의 다 왔습니다! 아까 저희가 포스트에서 만들어놓은 태그들, 기억 나시나요? 이제 그 태그를 클릭했을 때의 결과는 전체 포스트 목록 페이지로 이동하면서, 선택한 태그로 필터링 된 상태가 되어 있는 것이 아무래도 자연스러울 것입니다. 아무래도 서로 다른 페이지 간에 데이터를 전달해야 하기 때문에, 쿼리 파라미터를 이용했습니다.

우선 포스트 목록 페이지에서 쿼리 파라미터로 ?tag=태그값이 있는 경우, 이를 받아서 필터링이 되는 로직으로 개선을 해 봅시다.

$(document).ready(function() {
  let currentTag = "";
  const queryTag = getQuery().tag;
  if (queryTag) {
    currentTag = queryTag;
    filterByTagName(currentTag)
  }
});

function getQuery() { ... }
// getQuery 함수는 쿼리 파라미터를 JSON 형태로 리턴
// 출처: https://gent.tistory.com/62

페이지가 로드 되고 난 후에 쿼리 파라미터로 tag의 값을 읽어와 필터링을 해 주는 역할을 합니다. 그리고 포스트 목록 페이지 외부에서 있는 태그 코드들을 모두 쿼리 파라미터가 포함된 하이퍼링크로 바꾸어줍니다.

{% if page.tags %}
  {% for tag in page.tags %}
    {% if site.data.tags contains tag %}
      <a href="/posts/?tag={{ tag }}">
        <span class="tag">{{ site.data.format[tag] }}</span>
      </a>
    {% endif %}
  {% endfor %}
{% endif %}

이렇게 하면 외부에서 쿼리 파라미터로 태그 값을 잘 전달할 수 있습니다.

하지만 이 경우에는 제일 처음 페이지가 로드 될 때에만 쿼리 파라미터의 태그가 유효하게 되고, 다른 태그를 클릭하면 쿼리 파라미터는 그대로 변하고 있질 않아서 뭔가 찝찝할 수 있죠. 이를 위해 다른 태그 클릭 시 쿼리 파라미터를 동적으로 바꿔주는 함수도 작성해봅시다.

function updateQueryString(tagName) {
  const path = `${location.protocol}//${location.host}${location.pathname}?tag=${tagName}`;
  window.history.replaceState({ path }, '', path);
}

저는 쿼리 파라미터의 업데이트 내역을 history로 남기는 게 좋은 것 같아서 위와 같은 처리를 했습니다. 이러면 쿼리 파라미터가 업데이트 될 때, 뒤로가기를 누르게 되면 이전 쿼리 파라미터의 상태로 페이지가 이동이 됩니다.

결과물은 아래와 같습니다.

인스펙트

이렇게하면 태그 페이지 구현이 모두 끝났습니다!

마무리

이렇게 오늘은 Jekyll에서 태그 기능과 태그 목록 페이지를 구현하는 방법에 대해 알아보았습니다. 아무래도 초심자를 위한 글이다보니, 최대한 글을 쉬운 난이도로 풀어쓰려 노력했습니다. 이 때문에 글이 조금 장황해진 감이 없지 않아 있는데, 아무쪼록 글을 읽어주신 여러분들의 블로그 세팅에 조금이나마 도움이 되기를 바랍니다. 혹시라도 궁금한 점이 있거나 더 나은 방법이 있다면 덧글로 남겨주시면 감사하겠습니다.