1부에서는 views/Main 을 제외한 대부분의 파일들을 알아보았는데, 이번 포스트에서는 views/Main 만 대부분 알아볼 것이다. 분량이 다소 많다. 두개로 나눠도 글 읽기와 글 목록이지만 조금 더 들어가보면 포스트, 홈 커버, 리스트, 태그 클라우드, 방명록을 포함한다. 방명록, 태그 클라우드, 공지사항, 페이징에 대한 설명은 다른 부분과 겹치는 것도 있고 해서 일부 생략한다.
현재 기준으로 코드 하이라이팅이 이상한 것을 볼 수 있을텐데, highlight.js 에서 pug 를 지원하지 않는다는 슬픈 사실.
tidory.comfig.js
여기서 들어가기 전에 별칭에 대한 설정을 확인해보자면 다음과 같다. 따라서 저러한 형태로 쓰인 경로가 있다면 치환해서 사용하면 된다. 설정된 별칭은 @, ~views
이다.
/**
* Tidory Configuration
* https://tidory.com/docs/configuration/
*/
module.exports = {
/**
* Template aliases
*/
alias: {
'@': 'assets',
'~views': 'views'
}
}
views/Main.pug
include @/templates/Main/Comment
include @/templates/Main/Post
main#__main
#main__content
include Main/Cover
include Main/Guestbook
include Main/Post
include Main/Notice
include Main/TagCloud
include Main/List
s_if_var_paging
include Main/Paging
여기에 속한 파일들을 전부 알아보기 전에, 위에 있는 @/templates 아래에 있는 파일들을 알아볼 필요가 있다.
@/templates/Comment.pug
해당 유형의 파일에는 댓글과 관련된 것들이 들어가는데, 댓글 폼과 댓글 목록이다. 아래의 두 파일을 덧글 관련 파일이 아닌, views/Main 의 제일 위에다가 포함시킨 이유는 이 두개의 템플릿은 방명록에도 쓰일 것이기 때문이다. 바로 믹스인의 형태로.
include Comment/Form
include Comment/List
Comment/Form.pug
덧글/방명록 작성 폼을 말하며 두 개의 믹스인으로 구성된다. 티스토리 스킨을 구성하다 보면 치환자의 태그의 이름만 다르거나 하는 등 중복이 되는 경우를 여실히 볼 수 있다. form
믹스인을 보면, 중간에 개발 모드일 때랑 아닐때랑 구분해서 표현하고 있다. 특수한 경우에는 프리뷰에서 확인할 수가 없기 때문이다. 예를 들면 관리자 여부, 로그인 여부에 따라 출력이 다른 경우 말이다.
mixin formGuestControl(type)
.form__guest
.name
label(for='name') 이름
input#name(type='text' name=`` value='')
.password
label(for='password') 암호
input#password(type='password' maxlength='8' name=`` value='')
mixin form(type)
#{`s_${type}_input_form`}
div(class=`${type}-form content__form`)
.form__shadow
label(for='comment') 댓글
textarea#comment(name=`${type === 'guest' ? '' : ''}`)
div(class=`${type}-form-control form-control`)
if process.env.NODE_ENV === 'development'
.form__control__inner
+formGuestControl(type)
else
#{`s_${type}_member`}
.form__control__inner
#{type === 'guest' ? 's_guest_form' : 's_rp_guest'}
+formGuestControl(type)
.form__submit
input#secret(type='checkbox' name=``)
label#secret-label(for='secret')
a(href='#' onclick=``) 댓글쓰기
파라매터로 type
을 받고 있는데, 이는 방명록일 때와 덧글일 때를 구분하기 위함이다.
Comment/List.pug
해당 파일은 덧글 목록과 방명록 목록을 표현한다. 댓글과 대댓글은 코드의 중복이 있기 때문에 믹스인으로 별도로 빼놓았으며, 대댓글에는 댓글 쓰기가 없으므로 isReplyable
을 파라매터로 받고있다. 아래의 믹스인들이 실제로 어떻게 사용되는지는 이 포스트의 후반부에 나올 것이다.
mixin comment(type, isReplyable = true)
div(class=``)
.header
.user
.pic: img.lazyload(data-src=`` data-sizes='auto' width='48' height='48')
.metainfo
.name #{``}
time.date #{``}
.body #{``}
.control
a(href='#' onclick=``) 수정/삭제
if isReplyable
a(href='#' onclick=``) 댓글쓰기
a.more(href='#') 댓글보기
mixin list(type, rType)
#{`s_${type}_container`}
div(class=`${type}-list content__list`)
ol(uk-accordion='multiple: true; toggle: .control > .more; content: > ol; animation: false; targets: > li[id^=comment]')
#{`s_${type}_rep`}
li(id=``)
+comment(type)
#{`s_${rType}_container`}
ol
#{`s_${rType}_rep`}
li(id=``)
+comment(type, false)
@/templates/Post.pug
여기에는 글 목록과 글 읽기에 대한 것들이 들어가 있으며 사용처는 포스트와 공지사항, 페이지다.
include Post/Index
include Post/Permalink
Post/Index.pug
이 친구도 믹스인이지만, 짧다. 이 믹스인은 커버가 아닌 최신 글일 때의 글 목록을 표현하는 마크업을 가지고 있다.
mixin index(type, hasCategory=true)
#{`s_${type === 'list' ? 'list' : 'index_article'}_rep`}
include Index/Post
Index/Post.pug
div(class=`${type} content__index`)
.img
#{`s_${type}_rep_thumbnail`}
img.thumbnail(src=``)
.description
if hasCategory
.category: a(href=``) #{``}
h1.title: a(href=``) #{``}
p.summary #{``}
time.date
| →
span #{``}
@/templates/Post/Permalink.pug
퍼머링크는 공지사항과 포스트, 페이지 글 읽기에 해당하며, 내용이 가장 많은 편에 속한다. 아래의 믹스인을 통해 글 읽기를 해결한다.
mixin permalink(pageType, type, hasCategory = true)
#{`s_${pageType}_rep`}
div(id=`__${pageType}`)
include Permalink/Post
if hasCategory
include Permalink/Comment
s_if_var_notify
include Permalink/Notification
퍼머링크를 위한 스크립트에서는 코드를 하이라이팅 하고, 이미지를 정렬하고, h2, h3, h4 태그에 링크를 부여하고, 그리고 중요한 역할인 글 모드를 설정하기도 한다.
Permalink/Post.pug
퍼머링크 포스트의 마크업은 다음과 같다. 아래에 관리자 메뉴, 태그, 카테고리 더 보기, 글 작성자 등 또 다른 여러 파일이 포함되어 있지만, 다른 것보다 중요도가 낮아서 살펴볼 필요는 없다.
div(class=`${type} content__permalink` data-mode='')
header.header
#{`s_${type}_rep_thumbnail`}
.img
.mask
img.thumbnail.lazyload(data-src=`` data-sizes='auto' alt=``)
.heading
if hasCategory
a.category(href='/category/%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4') 포트폴리오
h1.title #{``}
.metainfo
time.date #{``}
if hasCategory
include Post/Admin
article.content
| #{``}
include Post/Subscription
s_if_var_scrollspy
include Post/Scrollspy
footer.footer
if hasCategory
s_if_var_related
include Post/Related
include Post/Tag
include Post/Btn
s_if_var_author
include Post/Author
Post/Scrollspy.pug
이 녀석은 TOC(Table Of Contents)를 표시해준다. 위젯이기 때문에 별도로 분리를 해놓았다. 사실 기존에는 Vue.js 컴포넌트로 구성했었으나 꼭 그럴 필요는 없는 것같아서 일반적인 템플릿으로 바꾸었다. 마크업 자체는 짧지만, 스크립트에서 본문에 속한 헤더를 추출하고, 메뉴를 구성하는 역할을 해준다.
#__spy
#spy__shadow(uk-sticky='offset: 65')
ul(class='uk-nav uk-nav-default' uk-scrollspy-nav='closest: li; offset: 65')
Permalink/Comment.pug
해당 파일을 언급하는 이유는, 여기서 우리가 작성한 믹스인을 처음 사용하기 때문이다. form, list
믹스인을 다음과 같이 사용할 수 있다. 본래 댓글 마크업은 길지만, 이미 믹스인에 작성해두었기 때문에 호출만 해주면 된다. 방명록도 비슷하게 처리하기 때문에 생략이다.
.permalink__comment
s_rp
+form('rp')
+list('rp', 'rp2')
Permalink/Notification.pug
이 친구는 글 읽기에서 이전 글/다음 글 팝업 창을 띄운다. 스킨의 특유 기능 중 하나이므로 살펴보기로 하자. 이전 글/다음 글이 있으므로 일단 두 코드가 어느정도 중복이므로 믹스인으로 구성하며, h.notify()
메서드에서는 UIkit.notification()
을 실행하는 것이 주요 코드이므로 생략한다.
mixin notify(type, label, pos)
#{`s_article_${type}`}
a.permalink__notify(href=`` class='uk-box-shadow-medium' id=type)
#{`s_article_${type}_thumbnail`}
.thumbnail
img(src=``)
//- img(src='https://t1.daumcdn.net/tistory_admin/static/mobile/m640/img_relation.png')
.metainfo
.description #{label}
.title #{``}
script.
/**
* Set timer for Notification
*/
$(document).ready(() => setTimeout(() => h.notify('#' + '#{type}', `#{pos}`, 15000), 3000))
+notify('next', '다음 글', 'bottom-right')
+notify('prev', '이전 글', 'bottom-left')
주석 처리해놓은 것은 테스트를 위한 것이다. 해당 치환자는 다른 치환자와는 다르게 섬네일이 없다고 해도 대체 이미지로 표시를 해주기 때문에 저렇게 테스트를 통해 처리하고 h.notify()
에서 해당 대체 이미지를 없애는 코드를 추가해주어야 한다. 어떻게 생각해보면 티스토리 치환자의 동작 방식이 일관적이지 않기 때문에 개발이 조금 힘들었던 점도 있다.
views/Main/Post.pug
드디어 글 읽기 부분을 우리가 작성한 믹스인으로 통해 써보기로 구현해보기로 하자. 이 친구는 덧글처럼 아주 심플한 모양을 가진다. 아래의 믹스인 호출은 각각 포스트 글 읽기와 페이지를 나타내며 페이지의 경우에는 카테고리가 없기 때문에 두 번째 파라매터에 false
를 준다. Post/Protected 는 보호글인데, 이 친구는 생략한다.
s_article_rep
+permalink('permalink_article', 'article')
+permalink('page', 'article', false)
include Post/Protected
views/Main/List.pug
글 목록을 총괄한다. 태그, 검색, 카테고리와 같은 것에 해당한다. 단, 공지사항은 예외다.
s_list
section.__list.main__list(data-mode='list' data-image-mode='')
header.list__header
s_list_image
.img
.mask
img.thumbnail(src='')
.heading
h1.title
ul
+index('list')
여기서 글 스타일은 data-mode
에서, 카테고리 이미지 스타일은 data-image-mode
에서 설정한다는 점을 살펴보자. 해당 스타일에 따라 CSS 를 다르게 처리할 뿐이고 마크업은 똑같다.
views/Main/Cover.pug
이번에 살펴볼 부분은 홈 커버다. 커버에는 믹스인이 많으며, 스타일이 여러 개가 존재하므로 좀 길다. 본래는 스타일마다 파일을 쪼개는 것도 생각해보았으나, 중복이 너무 많아서 그렇게 하지는 않고 글 목록 스타일처럼 처리해보기로 했다. 이것을 일반 티스토리 스킨 만들듯 skin.html 에 하드 코딩했다면 매우 피곤했을 것으로 생각된다. 도대체 중복되는 코드만 몇개이며 길이는 얼마나 길어질까.
mixin coverItem()
s_cover_item
li.content__index
block
mixin coverImage()
a.img(href='')
s_cover_item_thumbnail
img.thumbnail.lazyload(data-src='' data-sizes='auto' alt='')&attributes(attributes)
mixin coverDescriptionIfContent(isFeatured=false)
.description&attributes(attributes)
.category: a(href='')
h1.title: a(href='')
unless isFeatured
p.summary
time.date
| →
span
mixin coverDescriptionIfNotContent(isFeatured=false)
.description&attributes(attributes)
h1.title: a(href='')
unless isFeatured
p.summary
mixin coverDescription(isFeatured=false)
s_cover_item_article_info
+coverDescriptionIfContent(isFeatured)&attributes(attributes)
s_cover_item_not_article_info
+coverDescriptionIfNotContent(isFeatured)&attributes(attributes)
위의 것들은 커버에 사용될 구성 요소들이다. 아이템, 이미지, 설명같은 것을 넣을 수 있으며 포스트가 지정(IfContent)된 것이 아닌 사용자가 별도로 생성한 컨텐츠(IfNotContent)라면 다른 처리가 필요하다. 이제 만든 믹스인을 사용하여 타입에 알맞는 커버를 정의할 수 있다.
mixin slider(mode, isFullScreen=false)
section.__slider.main__cover(data-mode=mode)
h1.cover__title
div(class='uk-position-relative uk-visible-toggle uk-light'
tabindex='-1'
uk-slider='finite: true; autoplay: true'
style=isFullScreen ? 'min-height: 100vh' : 'max-height: px; height: px')
ul&attributes(attributes)
+coverItem()
+coverImage()(style=isFullScreen ? 'min-height: 100vh' : 'max-height: px; height: px')
div(class='uk-overlay-primary uk-position-cover')
+coverDescription(true)(class='uk-position-center uk-panel')
a(class='uk-position-center-left uk-position-small uk-hidden-hover' href='#' uk-slidenav-previous uk-slider-item='previous')
a(class='uk-position-center-right uk-position-small uk-hidden-hover' href='#' uk-slidenav-next uk-slider-item='next')
mixin cover(mode, isFeatured=false)
case mode
when 'default'
when 'list'
when 'grid'
when 'gallery'
when 'zigzag'
section.__list.main__cover(data-mode=mode)
h1.cover__title
ul&attributes(attributes)
+coverItem()
+coverImage()
+coverDescription(isFeatured)
when 'slideshow'
when 'slideshow-screen'
section.__slide.main__cover(data-mode=mode class='uk-position-relative uk-visible-toggle uk-light' tabindex='-1')&attributes(attributes)
h1.cover__title
ul(class='uk-slideshow-items')
+coverItem()
+coverImage()(uk-cover)
div(class='uk-overlay-primary uk-position-cover')
+coverDescription(true)(class='uk-position-center uk-position-small')
a(class='uk-position-center-left uk-position-small uk-hidden-hover' href='#' uk-slidenav-previous uk-slideshow-item='previous')
a(class='uk-position-center-right uk-position-small uk-hidden-hover' href='#' uk-slidenav-next uk-slideshow-item='next')
when 'slider'
+slider(mode)&attributes(attributes)
when 'slider-screen'
+slider(mode, true)&attributes(attributes)
마치며
hELLO. 스킨 개발 리뷰 2부가 끝났다. 해당 스킨을 만드는데에는 꽤나 많은 시간을 들였고, 코드의 중복을 줄이거나 버그를 줄이기 위해 여러모로 애썼다. 이 기세로 봐서는 다음 티스토리 스킨은 안 만들것 같다. 신경 쓸 것이 많은데다가 이 보다 더 좋은 품질의 스킨을 만들 수 있을지도 의문이다. 디자인적으로 획기적인 것이 있다면 고려는 해볼 것이지만, 사실 지금은 새로운 언어를 익히는데 더 시간을 쓰고 있어서 신규 스킨을 만드는 것은 계획에 없다.