# 태그 시스템 개선하기 1/2

# 서두

지난 글(VuePress에 태그 시스템 구축하기)에서 VuePress에 태그 시스템을 구축하는 방법을 다루었고, 매우 간단하지만 훌륭한 결과를 얻을 수 있었다. 본 글에서는 부족한 기능을 조금 더 추가해보는 시간을 갖도록 하겠다.

# 현재의 문제점과 요구사항 1

태그 시스템은 현재도 매우 훌륭하게 동작한다. 다만 문제점은 동일한 태그이나 한글/영문이 혼용되면 혼란이 발생한다는 점이다. 예를 들어,

키워드: #정적 사이트 생성기, #Static Site Generator
1

위와 같은 키워드가 있다면 한국인은 한글과 영문을 구분하지 않고 받아들일 것이다. 최종적으로 의미하는 바가 같기 때문이다. 그렇다고 저렇게 태그를 달면 태그가 2개나 생성되기 때문에 키워드 리스트 페이지에서 중복된 결과가 2벌 등장하는 셈이다.

즉, 화면에 표시되는 텍스트는 다르더라도 실제 태그할 키워드를 속성으로 가지고 있도록 만들면 될 것 같다. 예를 들어,

---
...
tags: ["정적 사이트 생성기(Static Site Generator)", "Static Site Generator"]
---
1
2
3
4

와 같은 형태로 표시하는 텍스트와 실제 링크할 태그를 별도 지정하게 만들면 되지 않을까? 먼저, 태그는 태그(Tag) 또는 Tag와 같은 형태로 지정하도록 결정했다. 두 태그 값이 모두 Tag라는 동일한 내부 값을 가지면서도 화면에 보이는 텍스트는 별도로 지정하는 기능인 것이다.

# 구현

역시 나만의 문제점 인식과 그 해결을 해보는 과정을 거쳐야 제대로 공부가 되는 것 같다. 어떻게 구현했는지 살펴보자.

태그 값에서 화면에 표시될 값과 내부 속성 값을 추출하는 메소드를 만들었는데, 태그 시스템에 쓰이는 컴포넌트가 <TagLinks /><TagList /> 두개였으므로 공통 메소드로 만들고 싶었다.

먼저 아래처럼 공통 모듈을 만들고




 



 





// docs/.vuepress/components/tag-methods.js

export default {
  getText: function (tag) {
    const index = tag.search(/\(/)
    return index > 0 ? tag.slice(0, index) : tag
  },
  getValue: function (tag) {
    const index = tag.search(/\(/)
    return index > 0 ? tag.slice(index+1, -1) : tag
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

실제 컴포넌트에서 임포트하여 사용하도록 만들 수 있었다.





 


 


 






<!-- docs/.vuepress/components/TagLinks.vue -->

<template>...</template>
<script>
import { getText, getValue } from './tag-methods'
export default {
  methods: {
    getText: function (tag) {
      return getText(tag)
    },
    getValue: function (tag) {
      return getValue(tag)
    }
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

이건 구현을 분리해 내기는 했지만, 컴포넌트 내부에서 사용하기 위해서는 methods 속성에 감싸여진 형태로 추가 구현이 필요했다. 그렇지 않으면 this 컨텍스트에 바인딩되지 않는다. 생각만큼 깔끔하거나 제대로 된 방법이 아닌 것처럼 느껴졌다. 검색 결과 믹스인이 가장 적합한 기능으로 판단됐다.

ReactAngular에도 동일하게 믹스인 기능이 있다. 이들 커뮤니티에서는 믹스인을 그리 권장하지 않는다. 객체의 기능을 주입하능 방식이기 때문에 객체지향 방법론에 위배된다고 보는 이들이 많은것 같다.

Vue에서는 공식 가이드(믹스인 | Vue.js 공식 한글 문서 (opens new window))에도 등장하며 딱히 권장이나 비권장한다는 언급은 없다. 기본 철학대로 상황에 따라 효율적인 경우도 있기 때문에 개발자의 선택에 맡긴다고 생각된다.

믹스인 형태로 구현은 아래처럼 할 수 있었다.




 











// docs/.vuepress/components/mixins/tag-methods.js

export default {
  methods: {
    getText: function (tag) {
      const index = tag.search(/\(/)
      return index > 0 ? tag.slice(0, index) : tag
    },
    getValue: function (tag) {
      const index = tag.search(/\(/)
      return index > 0 ? tag.slice(index+1, -1) : tag
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

4번째 줄처럼 methods 속성 내부에 메소드를 구현하고, 실제 컴포넌트에 주입하는 방식이다.




 

 
 




 

 

 










<!-- docs/.vuepress/components/TagLinks.vue -->

<template lang="html">
  <div v-if="isNotEmpty" class="wrap">
    키워드:
    <router-link v-for="tag in $page.frontmatter.tags" :key="tag" :to="{path: `/tags.html#${getValue(tag)}`}">
      #{{getText(tag)}}
    </router-link>
  </div>
</template>
<script>
import tagMethods from './mixins/tag-methods'
export default {
  mixins: [tagMethods],
  computed: {
    isNotEmpty() {
      const { tags } = this.$page.frontmatter
      return Array.isArray(tags) && tags.length > 0
    }
  }
}
</script>
<style scoped>
...
</style>
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

14번째 줄처럼 믹스인 시킨다음 6, 7번째 줄처럼 컴포넌트 메소드 형태로 사용이 가능하다. 중복된 코드가 거의 사라지고 상당히 깔끔해졌다.

TIP

태그가 없는 경우, 태그 컴포넌트를 감추기 위해 추가로 computed 속성인 isNotEmpty를 추가하였다.

동일하게 <TagList />에도 사용이 가능했다. 29, 30번째 줄을 참고하자.








 










 









 
 
 






 





<!-- docs/.vuepress/components/TagList.vue -->

<template lang="html">
  <div>
    <span v-for="tag in tags">
      <h2 :id="tag.value">
        <router-link :to="{path: `/tags.html#${tag.value}`}" class="header-anchor" aria-hidden="true">#</router-link>
        {{tag.value}}<template v-if="tag.text"> ({{tag.text}})</template>
      </h2>
      <ul>
        <li v-for="page in tag.pages">
          <router-link :to="{path: page.path}">{{page.title}}</router-link>
        </li>
      </ul>
    </span>
  </div>
</template>
<script>
import _ from 'lodash'
import tagMethods from './mixins/tag-methods'
export default {
  mixins: [tagMethods],
  computed: {
    tags() {
      let tags = {}
      for (let page of this.$site.pages) {
        for (let index in page.frontmatter.tags) {
          const tag = page.frontmatter.tags[index]
          const text = this.getText(tag)
          const value = this.getValue(tag)
          if (value in tags === false) tags[value] = {value, text: null, pages: []}
          // text가 한글인 경우, 따로 저장해둔다.
          if (!tags[value].text && text !== value) tags[value].text = text
          // 이미 추가된 페이지는 스킵
          if (_.findIndex(tags[value].pages, p => p.key === page.key) < 0) tags[value].pages.push(page)
        }
      }
      return _.sortBy(tags, ['value'])
    }
  }
}
</script>
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

31번째 줄처럼 tag객체는 기존의 page 배열을 담고 있었던 구조에서 valuetext 정보를 추가로 가지는 형태로 변경하였다. 추가로 <TagList />에서는 Lodash (opens new window) 패키지를 사용해서 중복 페이지를 쉽게 찾아내고, 키워드 정렬 역시 간단하게 수행하였다.

# 결론

기존의 코드를 약간 수정하는 수준에서 매우 만족할만한 태그 시스템 개선을 이루었다. 이제는 태그를 지정할 때마다 한글과 영문 두개를 다 달아야 하나?하는 고민을 덜게 되었다. 덤으로 믹스인 기능을 실습해 볼 수 있었다.

아직 개선이 더 필요한 부분이 있어서 VuePress 태그 시스템 개선하기 2에서 이어서 다루도록 하겠다.

최종 수정: 2020-6-29 12:47:28