Liquid를 활용해 블로그의 모든 포스트를 한눈에 보여주는 목차 페이지를 만든다.
Liquid를 사용함으로써 포스트를 추가 할 때마다 수동으로 목차 페이지를 수정할 필요가 없고 Jekyll Build 과정에서 목차 페이지가 자동으로 업데이트 되도록 한다.
또한 포스트들의 카테고리 상속 관계를 보고 목차가 트리 구조를 가지도록 할 것이다.

요새 이사 준비로 바쁜데다, 목차 자동 생성 알고리즘을 업그레이드(알고리즘 시간 복잡도를 \(n^2\)에서 n으로 줄였다 ^^) 하냐고 포스트 시간이 많이 늦춰졌다…
느리더라도 조금씩 계속 써나가도록 하겠다.

장단점

Liquid를 사용해서 목차 페이지를 이루는 HTML을 자동 생성하므로 포스트가 아무리 추가 되어도 수동으로 목차 페이지를 수정할 필요가 없으며 목차 페이지의 Liquid 코드는 변하지 않는다.

또한 Git Page의 Repository 관점에서 보면 목차 페이지의 Liquid 코드가 변하지 않으므로 포스트가 새로 추가될 때 마다 목차 페이지를 갱신 push 할 필요가 없다.

그리고 목차 페이지의 생성은 Git Page 자체에서 Jekyll Build 과정에서 일어나므로 코드가 차지하는 용량 부담이 적다.

그러나 수동으로 URL을 일일히 입력해 주는 것에 비해 구현 난이도가 훨씬 높다.

목차 트리 예시

아래는 최종 완성된 HTML 샘플이다. 여기에 CSS와 Javascript를 붙이면 내 블로그의 목차와 같이 된다.

Requirement

  • Jekyll, Liquid
  • Liquid 함수의 재귀적 사용
  • HTML

목차 트리 설명

최종적으로 우리가 만들 목차 트리는 아래와 같이 특정 카테고리에 포함되는 포스트와 하위 카테고리가 상위 카테고리에 상속되는 구조가 될것이다.

TOC Tree

포스트들이 위와 같은 상속 관계를 갖도록 하는 트리 구조는 블로그 만들기 포스트 시리즈 중 첫번째 포스트 게시하기 포스트에서 카테고리를 이용해서 포스트를 분류하는 방법에 대해 얘기한 포스트 분류 섹션의 작업 내용에서 이어진다.

다시 한번 여기서 얘기 하자면, 위 목차 트리 구조를 만들기 위해서 먼저 필요한 작업은 4개의 포스트를
/category1/_posts/2021-01-17-post1_1.md, /category1/_posts/2021-01-17-post1_2.md, /category1/category2/_posts/2021-01-17-post2_1.md, /category1/category2/_posts/2021-01-17-post2_2.md 경로의 파일로 생성하는 것이다.

이처럼 _posts 디렉토리보다 더 상위 있는 디렉토리들은 _posts 하위에 생성한 포스트의 카테고리가 된다.

HTML 표현

위 구조를 HTML로 표현하면 아래와 같다.

TOC Tree to HTML Concept

실제론 CSS 적용 때문에 완전히 일치 하진 않지만 HTML 구조의 개념을 표현한 것이다.

제일 먼저 Main 카테고리를 의미하는 category1<ul> 태그가 있다. 그리고 Main 카테고리 안에 속하는 포스트들과 하위 카테고리 등이 <li> 태그로 <ul> 태그의 아이템이 된다.
<li> 태그 아이템이 포스트라면 바로 포스트 URL로 연결하면 되지만 하위 카테고리라면 다시 한번 하위 카테고리 category2<ul> 태그가 된다.
그리고 마찬가지로 그 안에 속하는 포스트들과 하위 카테고리 등이 <li> 태그가 된다.
만약 그 하위 카테고리에 하위 카테고리가 있다면 <li> 태그 안에 <ul> 태그가 또다시 생긴다.
이런 과정이 무한히 반복 될 수 있다. 마치 프랙탈..!

포스트 Group

Jekyll에서 지원하는 Liquid Filter 중 group_by가 있다. 이 기능을 이용해서 모든 포스트를 각자의 카테고리들로 그룹화 할 것이다. 아래와 같은 코드로 그룹핑을 간단히 구현할 수 있다.

{% assign grouped_posts = site.posts | group_by: 'categories' %}

그 결과는 아래 같은 형태인데, Ruby 언어는 모르지만 Ruby 언어의 객체인 것 같으며 Json Object를 원소로 하는 Json Array와 유사하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[
  {
    "name" => "["category1", "category2"]",
    "items" => [
      #<Jekyll::Document front-end/jekyll/_posts/2021-02-07-category_inheritance_using_dir_structure.md collection=posts>,
      #<Jekyll::Document front-end/jekyll/_posts/2021-02-03-jekyll_comm_opt_host.md collection=posts>,
      ... ],
    "size" => 4
  },
  {
    "name" => ...,
    "items" => [ ... ],
    "size" => ...
  },
  ...
]

Filter group_by 주의점

포스트 Group 섹션에서 group_by filter를 사용할 때 group_by의 key값으로 categories를 사용했다.

이때 categories는 길이가 1인 문자, 문자열, 숫자 등이 아니라 순회 가능한 Array 객체이다. 따라서 group_by 과정에서 Array 객체가 문자열로 강제 형변환 되어 key 값이 된다.

즉, group_by의 키 값인 categoriescategory1, category2 2개 원소를 포함한 Array 였다면, 이것은 group_by의 결과로 문자열 키 값 ["category1", "category2"]이 된다.

Group By 결과물을 group 객체라 할 때 그 내부 원소에 접근하려면 group['["category1", "category2"]'] 처럼 공백 하나 틀림 없이 문자열을 키로 넘겨주어야 한다.

카테고리 Navigation 파일

내 블로그의 모든 카테고리의 정보를 저장해두는 파일을 하나 만든다. 이 파일은 카테고리의 title, url, 자식 카테고리의 정보를 정의한다.

이 파일도 Liquid 함수를 이용해 포스트들에서 카테고리 구조를 유추하는 방식으로 자동화하면 작성하지 않아도 되겠지만, 카테고리를 한눈에 보기위해 마련해둔다.

내 블로그는 Minimal-Mistakes를 상속해서 만들었으므로 이미 비슷한 파일인 _data/navigation.yml 파일이 존재한다. 없다면 _data 디렉토리 하위에 파일을 하나 만들자.

그 파일 안에 yml array 객체를 하나 만들어서 카테고리의 정보를 정의한다. 아래는 내 _data/navigation.yml에 정의되어 있는 카테고리 정보이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
main-sidebar:
  - title: "Computing"
    url: "/computing/"
    children:
    - title: "Computer"
      url: "/computing/computer/"
      children:
      - title: "CPU"
        url: "/computing/computer/cpu/"
    - title: "Linux"
      url: "/computing/linux/"
      children:
      - title: "Command"
        url: "/computing/linux/commands/"
    - title: "Programming"
      url: "/computing/programming/"
      children:
      - title: "Python"
        url: "/computing/programming/python/"
  - title: "Deep Learning"
    url: "/deep-learning/"
    children:
    - title: "CNN"
      url: "/deep-learning/cnn/"
      children:
      - title: "CAM"
        url: "/deep-learning/cnn/cam/"
    - title: "VGG"
      url: "/deep-learning/vgg/"
  - title: "Mathematics"
    url: "/mathematics/"
    children:
    - title: "Linear Algebra"
      url: "/mathematics/linear-algebra/"
    - title: "Statistics"
      url: "/mathematics/statistics/"
  - title: "Data Engineering"
    url: "/data-engineering/"
    children:
    - title: "Hadoop"
      url: "/data-engineering/hadoop/"
    - title: "Spark"
      url: "/data-engineering/spark/"
  - title: "Front-End"
    url: "/front-end/"
    children:
    - title: "HTML"
      url: "/front-end/html/"
    - title: "CSS"
      url: "/front-end/css/"
    - title: "JavaScript"
      url: "/front-end/javascript/"
    - title: "Jekyll"
      url: "/front-end/jekyll/"
    - title: "Liquid"
      url: "/front-end/liquid/"
  - title: "Back-End"
    url: "/back-end/"
  - title: "Tools"
    url: "/tools/"
    children:
    - title: "Flask"
      url: "/tools/flask/"
    - title: "FAISS"
      url: "/tools/faiss/"
    - title: "AIF360"
      url: "/tools/aif360/"
    - title: "Git"
      url: "/tools/git/"
  - title: "Blog"
    url: "/blog/"
    children:
    - title: "Tips"
      url: "/blog/tips/"
  - title: "Writing"
    url: "/writing/"
    children:
    - title: "Poem"
      url: "/writing/poem/"

하나의 카테고리는 title, url을 가지며 하위 카테고리가 있는 경우 children 객체를 갖는다.

children 객체 또한 상위의 yml array와 같은 array 타입이고 또 그 하위의 children이 있을 경우 같은 array 구조가 반복된다.

Liquid 재귀 함수

포스트의 카테고리로, 포스트와 카테고리 또는 카테고리와 카테고리가 서로 부모 자식 관계를 갖도록 트리 구조를 정의하고 이것을 HTML로 구현 하려면 몇가지 방법이 있겠지만 나는 Jekyll 기능을 활용하기 위해 Liquid 재귀 함수를 사용했다.

Liquid로 함수 만드는 법은 이 포스트 Liquid와 Jekyll로 함수 만들기 & Tutorial를 참조하면 좋다.

Github에 업로드 되어있는 내 블로그의 소스 코드 중 /_includes/functions/ 위치에 Liquid 함수 구현체들이 모여 있다.

그 중 Liquid 재귀 함수로 목차를 자동 생성하는 함수는 /_includes/functions/make_toc_tree_v3.html 이고 소스 코드는

위 두가지 경로의 내용을 보면 된다.

함수 순서도

재귀 함수의 순서도를 그리면 아래와 같다. 그림 아래에 순서도에 대한 자세한 설명을 덧붙였다.

Liquid Function Flow Chart of make_toc_tree_v3.html

  • Input: “grouped_posts”
    포스트 Group 섹션에서 설명한 Liquid Filter로 포스트들을 그룹화한 Ruby 객체를 함수의 인자로 넘겨 받는다.

  • Input: “arr”
    카테고리 Navigation 파일 섹션에서 설명한 Yaml로 작성된 카테고리 정보 Array를 함수의 인자로 넘겨 받는다.

  • 카테고리로 구성된 <ul> 태그 생성
    하나의 <ul> 안에 <li> 태그로 카테고리를 담을 것이다.

  • Input으로 받은 카테고리 Array Iteration
    넘겨 받은 arr 인자를 Liquid 반복문으로 Iteration 한다. c 변수는 하나의 카테고리 정보를 의미한다.

  • 카테고리 링크 생성
    하나의 카테고리는 <li>, <a> 태그로 만들어지고 c객체에 담겨있는 title, url 값을 각각 카테고리명, href 값으로 넣는다.

  • 현재 카테고리의 카테고리 검색용 문자열 생성
    c.url 값으로 카테고리 c카테고리 검색용 문자열을 만든다.
    Filter group_by 주의점 섹션에서 언급한대로 categories Array 객체가 group_by 결과물의 key 값으로 그대로 사용될 수 없고 문자열로 강제 형변환 됐기 때문이다.
    따라서 우리는 강제 형변환 된 문자열의 형식과 동일한 형식의 문자열을 만들어야 한다.
    그 형식은 c 카테고리에 도달하기 까지 있었던 부모 카테고리들과 자신을 Array 형태로 나열한 형식이다.
    예를들어, c.url"/front-end/jekyll/" 이면 만들어진 문자열은 ["front-end", "jekyll"]이 된다.

  • 카테고리 하위의 포스트들을 추출
    위 단계에서 만든 카테고리 검색용 문자열은 여기에서 사용된다.
    where filter를 사용하면 카테고리 c카테고리 검색용 문자열과 같은 카테고리 그룹에 속한 post들의 Array를 추출할 수 있다.

  • 필요한 경우 포스트를 커스텀 정렬
    내가 작성한 포스트의 머리말에는 임의의 정렬을 위해 post-order라는 변수명으로 숫자 값을 지정해주는 경우가 있다.
    포스트는 기본적으로 같은 카테고리 범위에서 ‘작성 날짜’를 기준으로 정렬된다.
    이때 임의의 정렬을 하려면 이처럼 변수를 하나 만들고 sort filter를 이용해 post-order 값으로 오름차순 정렬한다.

  • 포스트 링크 생성
    위 단계에서 추출한 포스트 Array를 Iteration 하면서 <ul>, <li> 태그로 HTML 구조화 한다.

  • 하위 카테고리가 있으면 위 과정 반복 (재귀)
    카테고리 cchildren 객체가 정의돼 있는 경우 지금의 함수를 다시 한번 호출하는 Liquid 재귀 호출을 수행한다.
    단, 인자를 바꿔서 arr인자에 전체 카테고리 정보를 담고있는 navigation Array가 아닌 c.children Array를 넣어준다.
    그러면 지금까지와 같은 과정이 c.children 부분 집합 내에서만 이뤄지게 될 것이다.

위 과정들의 Iteration이 모두 완료되면 HTML 표현 섹션에서 보인 그림과 같은 구조의 HTML이 자동 생성된다.

또 Post가 추가 작성되면 위 순서도에서 보였듯 Liquid 함수 make_toc_tree_v3.html 실행 과정 중에 자동으로 목차에 추가된다.

여기까지 HTML 생성 작업이 완료되었으면 남은건 CSS를 입히고, Javascript를 적용해서 페이지를 더욱 풍성하게 하는 일들이 남았다.

Meta Info

Categories:

Published At:

Modified At:

Leave a comment