내가 이 정도야

hELLO. 티스토리 스킨 개발 리뷰 - 1부

기왕 스킨을 개발했고, 꽤나 괜찮게 나가고 있으니 포트폴리오 겸 적어두면 어떨까 써보기로 했다. 본래 티스토리 스킨 과 같은 프로젝트는 안 쓰려고 했지만, 고맙게도 깃허브에서 별을 제법 받았다. 개발 과정과 기록을 남기는 일은 아무래도 개발자로써 해두면 좋을테니 말이다. 보통 프로젝트 날짜와 이름, 약간의 설명만 적어두었는데 2020년 들어서는 바꿔보기로 했다.

 

<100>


https://github.com/pronist/hELLO

 

pronist/hELLO

hELLO is a Tistory Skin. Contribute to pronist/hELLO development by creating an account on GitHub.

github.com

어떤 기술을 썼나요?

스킨 소개 페이지에도 나와있듯, 기초적인 웹기술, 일부 확장 언어와 프레임워크를 사용했다.

 

 

다만 일반적으로 티스토리 스킨에 Node.js, Webpack 은 쓰이지 않지만, 스택에 포함된 이유는 바로 티도리 프레임워크 때문이다. 티도리 프레임워크는 본인이 직접 개발한 티스토리 스킨 개발을 위한 프론트엔드 프레임워크이며, 이 글의 다음 글에서 관련 개발 리뷰를 써 볼 예정이다.

 

https://tidory.com/

 

티스토리 스킨 프레임워크, 티도리(TIDORY)

오직 티스토리 스킨만을 위한 프레임워크

tidory.com

라이브러리와 프레임워크

사용한 라이브러리와 프레임워크는 아래와 같으며, 폰트는 Noto Sans KR, 코드 하이라이팅용으로는 D2Coding 을 사용했다. 3.0 이전의 버전에는 Vue.js 프레임워크도 사용했었으나, 3.0 부터는 Vue.js 컴포넌트를 사용하지 않기 때문에 제외되었다.

티스토리 스킨의 구성 요소

티스토리 공식 문서를 보면 알겠지만, 스킨을 구성하기 위해서는 제법 많은 요소가 들어간다. 선택적이지만 hELLO 스킨에는 모든 기능을 담기위해 애썼다. 사실 간단하게 보면 요소는 큰 세 가지밖에 없는데, 바로 글 읽기와 글 목록, 그리고 홈 커버다. 다만 그 종류가 많고 표현 방법이 여러가지로 구성될 수 있다는 점 때문에 일이 많아진다. 참고로 1부에서는 위에 세 가지는 모두 배제하고 부가적인 것부터 알아본다.

 

https://tistory.github.io/document-tistory-skin/

 

소개 · GitBook

티스토리 스킨 가이드 티스토리에서는 스킨을 통해서 사이트를 자유롭게 디자인할 수 있습니다. 스킨은 치환자를 사용하여 제작되고 사용된 치환자에는 티스토리의 데이터가 대입되어 티스토�

tistory.github.io

참고로 티스토리 스킨을 제작할 때는 백엔드는 신경쓰지 않으므로 프론트엔드만 있다. 게다가 치환자의 존재로 HTML 보다는 CSS 가 대부분이지만, 개발을 돌아보는 리뷰의 특성상 CSS 는 설명 할 필요가 없기 때문에 제외한 점을 참고해주었으면 좋겠다.

app.pug

app.pug 는 티도리 프레임워크에서 스킨 영역 밖의 웹페이지 부분의 레이아웃을 담당한다. 여기서 html 태그와 head, body 가 존재하는데 중점적으로 보아야하는 것은 바로 테마를 설정하는 부분이다.

doctype html

html#__hELLO(lang='ko' data-theme='[##_var_theme_##]')
  head
    //- ...
    
    //- DARK MODE
    script(scoped).
      $('html').attr('data-theme', localStorage.TTDARK
        ? localStorage.TTDARK === 'Y' ? 'dark' : 'light'
        : window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : $('html').attr('data-theme')
      )

  body(id='[##_body_id_##]')
    //- TISTORY SKIN
    block TIDORY
    
    //- ...

티도리 프레임워크는 기본적으로 티스토리 스킨의 관례에 따라 script 태그는 script.js, style 태그는 style.css 로 분리되는데 scoped 속성을 부여하면 분리되지 않는다. (script 태그에 scoped 속성을 쓰면 VSC에서 코드 하이라이트가 되지 않는다는 슬픈 일이 ...)

 

여기서는 사용자 블로그의 테마를 설정하는 아주 중요한 일을 하는데, 최신 브라우저는 CSS 미디어 쿼리에서 다크 모드를 올바르게 사용할 수 있지만, 버전이 낮은 브라우저는 그럴 수 없다. 테마에 대한 설정은 기본적으로 로컬 스토리지TTDARK 라는 키에 저장된다. 따라서 TTDARK 라는 값이 있다면 그것을 가장 우선 순위로 하며, 그렇지 않다면 미디어 쿼리(OS 설정 등)를 따르고 그 외에는 블로거가 수동으로 설정한 테마로 설정한다.

다크 모드

다크 모드를 표현할 때는 선택자가 html[data-theme='dark'] 이므로 이를 그대로 옮겨주면 된다. 기본 테마는 라이트 모드이므로 라이트 모드의 코드는 일반적으로 존재하며, 다크 모드는 여기에 스타일을 덮어씌울 뿐이다. 다크 모드를 스타일시트에서 구현하는 방법은 여러가지일 것이다.

style
  :stylus
    html[data-theme='dark']
      //- When theme is dark mode ...

index.pug

이 파일은 티스토리 스킨의 관점에서 최상위 파일이며 스킨의 기본 구조를 담당한다. app.pug 를 상속받기 때문에 block TIDORY 내부에 스킨 코드를 작성해야 하는데, 현재 스킨은 다음과 같은 구조를 가진다.

extends app

block TIDORY
  if process.env.NODE_ENV === 'development'
    include views/TistoryToolbar
  s_if_var_loader
    include views/Loader
  include views/ScrollIndicator
  s_t3
    #__tidory
      include views/Sidebar
      s_if_var_navbar
        include views/Navigation
      include views/Main
      include views/Footer

크게 보면 views/Sidebar, views/Navigation, views/Main, views/Footer 이다. 그 외에 부가적으로 views/Loaderviews/ScrollIndicator 가 있는데, 이 둘은 스킨 자체와는 연관이 없기 때문에 일부러 티스토리 전용 태그인 s_t3 바깥으로 빼놓았다.

views/TistoryToolbar.pug

여기에 있는 views/TistoryToolbar 는 보다시피 개발 모드에만 포함시키게 해 놓았는데, 티도리 프레임워크에서 스킨 프리뷰를 하게 되었을 때 기본적으로 구독버튼 및 티스토리 메뉴를 보여주지 않기 때문이다. 따라서 개발모드에서 이를 일부러 추가하여 시뮬레이션 할 수 있게 하였다. 이는 커스텀 구독버튼이나 사이드바에 있는 메뉴를 구성할 때 사용했다.

.menu_toolbar(class='#menubar')
  h2.screen_out 티스토리툴바
  #menubar_wrapper.btn_tool.btn_tool_type1
    include TistoryToolbar/TistoryToolbar
  if process.env.IS_OWNER === 'false'
    include TistoryToolbar/Subscribe

views/Sidebar.pug

사이드바에는 각종 위젯들이 들어갈 수 있는데 구성은 다음과 같다. 다른 메뉴들은 그냥 보면 알겠지만, Sidebar/TistoryToolbar 의 경우는 눈여겨 보면 좋다. 이 경우는 위에서 임의로 추가한 views/TistoryToolbar 를 복사하여 사이드바에 넣은 코드가 들어있기 때문이다. 프로덕션에서는 실제 블로그를 사용하는 사용자 메뉴가 들어가겠지만, 개발에서는 그렇지 않다.

#sidebar__mask
aside#__sidebar(role='sidebar')
  #sidebar__shadow
    s_sidebar
      s_sidebar_element
        <!-- 프로필 -->
        include Sidebar/Profile
      s_sidebar_element
        <!-- 방문자 수 -->
        include Sidebar/Counter
      s_sidebar_element
        <!-- 검색 -->
        include Sidebar/Search
      s_sidebar_element
        <!-- 카테고리 -->
        include Sidebar/Category
      s_sidebar_element
        <!-- 블로그 메뉴 -->
        include Sidebar/BlogMenu
      s_sidebar_element
        <!-- 공지사항 -->
        include Sidebar/Notice
      s_sidebar_element
        <!-- 인기 글 -->
        include Sidebar/PopularPosts
      s_sidebar_element
        <!-- 태그 -->
        include Sidebar/Tags
      s_sidebar_element
        <!-- 최근 댓글 -->
        include Sidebar/RecentComments
      s_sidebar_element
        <!-- 최근 글 -->
        include Sidebar/RecentPosts
      s_sidebar_element
        <!-- 티스토리 -->
        include Sidebar/TistoryToolbar
        
s_if_var_sidebar
  style(scoped)
  //- When sidebar is on screen ...

스킨 옵션에 관한 특수 태그인 s_if_var_sidebar 부분에서는 사이드바가 나타나 있을 때를 말하고, 화면에 대한 스타일을 일부 덮어쓴다. 사이드바가 있을 때와 없을 때는 tape, screen 과 같이 full screen 에 기반하는 헤딩 스타일이 적용되는 방식, 정확히는 width, margin-left 와 같은 속성을 조절해줄 필요가 있기 때문이다.

Sidebar/TistoryToolbar.pug

아래의 코드를 보면, 스크립트에서 그저 티스토리 메뉴에 해당하는 엘리먼트를 복사해서 사이드바에 넣을 뿐이라는 점을 알 수 있다.

#sidebar__tistory
  h2 티스토리

script.
  /**
   * Copy TistoryToolbar to Sidebar
   */
  $(document).ready(() => {
    $('.menu_toolbar > #menubar_wrapper > .header_layer').appendTo('#__sidebar > #sidebar__shadow > #sidebar__tistory')
  })

views/Navigation.pug

네비게이션은 스킨의 상단에 위치하는 바(Bar) 같이 생긴 것을 말한다. 마크업 내용은 별거 없다.

nav#__nav(role='navigation'
  class='uk-navbar-container uk-navbar-transparent'
  uk-sticky='top: 280; animation: uk-animation-slide-top; cls-active: sticky; cls-inactive: uk-navbar-transparent;'
  uk-navbar)
  #nav__s1(class='uk-navbar-left')
    img.profile(src='[##_image_##]')
  #nav__metainfo(class='uk-navbar-center')
    h1.title [##_title_##]
  #nav__s2(class='uk-navbar-right')
    include Navigation/Subscribe

여기서 일부 생략한 script 태그들을 잠깐 보자. 아래의 코드는 주석에서 쓰여있듯 퍼머링크에서는 글의 제목을 포함한 것들을 복사하여 네비게이션에 넣어준다. 또한 모바일에서는 왼쪽에 프로필 사진이 나타나고, 그것을 누르면 사이드바가 나타날 텐데, 그에 대한 것은 두 번째 script 태그에서 처리한다.

script.
  /**
   * Copy metainfo to Navigation header
   */
  $(document).ready(() => {
    const $h = $('.content__permalink > header > .heading > *:not(.metainfo)')
    const $metainfo = $('#__nav > #nav__metainfo')
    // Only for permalink
    if ($h.length) {
      $metainfo.empty()
      $h.each((_, $h) => $metainfo.append($($h).clone()))
    }
  })

script.
  /**
   * Sidebar Toogle
   */
  $(document).ready(() => {
    const $sidebar = $('#__sidebar')
    const $sidebarMask = $('#sidebar__mask')

    $('#__nav > #nav__s1 > .profile').on('click', () => {
      $sidebar.css('left', 0)
      $sidebarMask.fadeIn(200)
    })
    $sidebarMask.on('click', () => {
      $sidebar.css('left', '-250px')
      $sidebarMask.fadeOut(200)
    })
  })

Navigation/Subscribe.pug

해당 파일은 구독버튼을 의미하는데, 여기에는 마크업이 없으며 script 태그가 하나 존재하는데, 그 역할은 구독버튼을 네비게이션에 추가하는 일이다. 참고로, 시뮬레이션을 위한 구독버튼은 views/TistoryToolbar 소속인 TistoryToolbar/Subscribe.pug 파일이다.

script.
  /**
   * Copy TistoryToolbar to Body
   */
  $(document).ready(() => {
    $('.menu_toolbar > div[class=btn_tool]').appendTo('#__nav > #nav__s2')
    $('#__nav > #nav__s2 > div[class=btn_tool] > .btn_menu_toolbar.btn_subscription').append('<i class="fa fa-star"></i>')
  })

views/Footer.pug

저작권 표시와 일부 테마 변경과 같은 위젯들이 존재하는 파일이다. 마크업은 단순하다. 스킨 옵션에 따라 적용해줄 수 있는 s_if_var_switch_themes_if_var_to-top 이 있다. s_if_var 또는 s_not_var 로 시작하는 태그들은 전부 스킨 옵션과 관련이 있다.

footer#__footer(role='footer' class='uk-navbar-container uk-navbar-transparent' uk-navbar)
  div(class='uk-navbar-left')
    a#__designed-by(href='https://pronist.tistory.com/5' target='_blank')
      i(class='fa fa-code')
  div(class='uk-navbar-right')
    s_if_var_switch-theme
      #__theme-btn
        i(aria-hidden='true' class='fa fa-moon-o')
    s_if_var_to-top
      a#__toTop(href='#' uk-scroll='target: #__hELLO')
        i(class='fa fa-chevron-up')

테마 변경하기

테마를 변경하기 위한 script 태그는 아래와 같은데, 현재 설정된 테마를 불러와서 로컬 스토리지에 담아주는 역할을 하며, 그 값에 따라 테마를 바꿔준다.

script.
  /**
   * Change a Theme with Button
   */
  $(document).ready(() => {
    const $themeBtn = $('#__theme-btn')
    $themeBtn.on('click', () => {
      const TTDARK = $('html').attr('data-theme') === 'dark' ? 'N' : 'Y'
      localStorage.TTDARK = TTDARK

      $('html').attr('data-theme', localStorage.TTDARK === 'N' ? 'light' : 'dark')
    })
  })

마치며

티스토리 스킨은 일반 프론트엔드보다 신경써야 할 부분이 많다. 스킨 옵션의 적용여부, 홈 커버, 글 읽기 모드, 썸네일이 있고 없고의 차이 등등 해당 스킨을 개발하면서 느낀 티스토리 스킨의 경우의 수가 제법 많아 개발에 일부 애를 먹긴 했다.

 

대부분 보고된 버그들은 CSS 의 코드 상의 문제였기 때문에 더욱 그렇다. 개발자 한 명이 스킨의 구성요소를 조합하여 나올 수 있는 모든 경우의 수를 고려하여 처리하기란 쉽지 않은 일이다. 티스토리 스킨은 스킨에서 제공되는 요소들을 제어 할 수 있다보니 사용자가 어떠한 형태로 스킨을 구성할 지 개발자는 알 길이 없다. 그저 보기에는 쉬워보이는 티스토리 스킨 개발에는 예상 외의 여러 요소가 많으니 주의하는 것이 좋다.

 

해당 시리즈는 1부와 2부로 나뉘었는데, 그 이유는 내용이 상당히 많기 때문이다. 스킨의 기능이 많기 때문에 어쩔 수 없다. 따라서 다음 포스트에서는 홈 커버와 글 목록, 글 읽기에 대해서 이야기해보도록 할 것이다.