티스토리 뷰

Table Of Contents

왜 나만 없어 TOC

블로그를 티스토리로 이전하고 나서, 마크다운 편집기 빼고 다 좋은데 다른 블로그들이 부러웠던 점은 바로 글 옆에 생기는 소제목 목차였습니다.
많은 분들과 마찬가지로, 작성의 편리함과 비교적 빠른 생산성 때문에 마크다운으로 글을 자주 작성하는 편인데요.
소제목들을 쭉 모아서 만든 글 목차는 휑한 블로그 사이드 바도 채워주고, 지금 사용자가 글의 어디쯤을 읽고 있는지, 얼마나 이 재미없는 글이 많이 남았는지 알려주는 좋은 기능이라 생각했습니다.
클릭하면 해당 위치로 글 이동도 손쉽게 할 수 있구요.

다른 블로그 플랫폼들을 보면, github.io에도 이런게 종종 보이고, velog에도 보이고, 세상 모두가 다 가지고 있는데 티스토리만 없는 겁니다.
찾다보니 이 목차를 Table Of Contents라고 부르는 것도 알아냈습니다. 줄여서 TOC.

나만 없어 TOC

플러그인 하나쯤 있을 법도 한데... 뭐 아쉬운 사람이 만들어야겠죠.
그래도 우리의 티스토리는 다행히 html, css, js를 편집 가능하게 열어줍니다.

정리한 요구 사항

자바스크립트랑 조금 더 친해질 겸 토이 프로젝트로 조금씩 만들어보자, 싶어서 노트북을 빼들었습니다.
그래서 다음과 같이 제가 원하는 기능들, 보아왔던 기능들을 정리해서 요구 사항을 만들고 Readme.md에 정리해 보았습니다.
(Github 저장소는 글 아래에 첨부하겠습니다.)

  • 티스토리 게시글에서 h1, h2, h3 태그를 추출해 TOC를 동적으로 만들어 준다.
  • h1, h2, h3 태그는 level에 따라 Tab과 글자 크기로 구분한다.
  • h1 태그가 없으면 h2 태그가 최상위 항목, h2 태그도 없으면 h3 태그로만 이루어진 TOC를 만들어 준다.
  • TOC 항목을 클릭하면, 해당 태그가 있는 곳으로 이동하는 스크롤 이벤트가 발생한다.
  • 사용자가 스크롤을 내리면, 현재 사용자의 위치를 TOC 항목에 CSS를 걸어 동적으로 표시해 준다.
  • 만약 태그가 너무 많아서 지정한 max-height를 넘어 가면, TOC에 스크롤이 생긴다.
  • TOC 스크롤이 생긴 후에는 보이지 않는 아래 쪽 항목으로 게시글 뷰가 이동하면 TOC도 같이 따라 스크롤이 내려간다.
  • TOC가 이동할 수 있는 범위를 게시글 범위로 제한한다. 예를 들어 맨 아래쪽 영역까지 침범하지 않도록 한다.
  • 화면의 가로 길이에 따라 동적으로 사라져야 한다. 태블릿 이하의 크기에서는 보이지 않도록.
  • 티스토리 특성 상 스킨의 제약을 많이 받는다. 최대한 범용적으로 만들긴 하겠지만, 일단은 현재 스킨을 기준으로 작성한다.
  • 기존 티스토리의 소스를 최대한 건드리지 않는 선에서 추가한다. 업로드 편의를 위해서 css, js 파일도 하나씩만 작성한다.

원래 H1 태그는 너무 커서 부담스럽기에 H2와 H3으로만 글을 작성해왔지만, 이 글 만큼은 H1, H2, H3 모두 사용하여 작성해 보았습니다.
옆에 나오는 TOC Card를 감상(?)하시면서 읽어주시면 감사하겠습니다. (모바일/태블릿에서는 보이지 않아요!)


만들어보자

모듈 패턴

기존의 소스를 건드리지 않고, 최대한 업로드 부담도 줄이려면 css, js 파일은 하나씩만 만들어야겠다 생각했습니다.
css야 양이 많지 않겠지만, 자바스크립트는 함수를 기능 별로 쪼개다보면 하나의 파일에서 관리가 어려워지기 때문에, 자바스크립트의 모듈 패턴을 사용하여 코드를 관리하기로 했습니다.

모듈 패턴을 적용하여 다음과 같이 상수 영역과 TOC_CARD 영역으로 나누고, TOC_CARD 안은 Controller, Service로 영역을 구분하였습니다.

const CONSTANTS = (function () {
  // 여러가지 상수들
  const KEY_OF_H1 = 1;

  return {
    keyOfH1: KEY_OF_H1,
  }
})();

const TOC_CARD = (function () {
  const TocCardController = function () {
    const tocCardService = new TocCardService();

    const init = function () {
      tocCardService.exampleMethod();
    }

    return {
      init,
    }
  }

  const TocCardService = function () {
    const exampleMethod = function () {

    }

    return {
      exampleMethod: exampleMethod,
    }
  }

  const tocCardController = new TocCardController();

  const init = function () {
    tocCardController.init();
  }

  return {
    init,
  }
})();

TOC_CARD.init();

가장 아래 쪽에 있는 TOC_CARD.init()이 실행되면, 즉시 실행 함수로 TOC_CARD 함수가 실행되고, key-value 형태의 return 값을 통해 내부에 있는 함수에 접근할 수가 있습니다.
init 메소드를 타고 Controller의 init 메소드를 실행시키고, 차례로 해당 Service의 함수들도 실행시킬 수 있습니다.

모듈 패턴을 사용하면 추후에 함수가 많이 늘어나도, 각 함수의 영역이 확실하기 때문에 관리가 좀 더 용이하다는 장점이 있습니다.
큰 흐름은 Controller에서 관리하고, 실제 로직은 Service에서 관리하면 되겠네요.

TOC Card 만들기

이제 틀을 만들었으니 초기화 작업을 진행해봅시다.
말이 초기화지 사실 H 태그 탐색과 Toc Card 생성 등 해야할 일이 많습니다.

초기화 메소드에서 구현해야 할 기능들은 다음과 같습니다.

  • H1, H2, H3 태그들이 현재 게시글 내부에 있는지 탐색한다. 없다면 아무 작업도 하지 않는다.
  • H 태그들에 고유 id를 부여한다.
  • H 태그들의 내용을 바탕으로 Toc Card에 Toc 항목들을 만들어서 추가한다.

H 태그 탐색하기

const mainContents = document.querySelector('.area_view');
const hTags = mainContents.querySelectorAll('h1, h2, h3');

/* h1, h2, h3 태그가 있는지 확인한다 */
const checkExistenceOfHTags = function () {
  if (mainContents === undefined) {
    return false;
  }

  return hTags.length != 0;
}

먼저 TOC의 재료가 되는 H 태그들을 탐색합니다.
티스토리에서 게시글 영역을 의미하는 클래스는 area_view네요.

먼저 area_view 클래스를 가진 태그가 있는지 확인합니다. 현재 페이지가 블로그 홈 같이 게시글 페이지가 아닐 수도 있으니까요.
게시글 페이지가 맞다면 H1, H2, H3 태그들을 가지고 옵니다.
해당 태그들이 하나도 없다면 TOC는 만들지 않습니다.

탐색한 H 태그들은 다른 서비스 메소드에서 자주 사용하기 때문에 서비스에서 변수로 들고 있도록 합니다.
이제 위 메소드를 기반으로 초기화 작업을 진행할지 말지를 정합니다.

/* Controller */
const init = function () {
  const existsHTags = tocCardService.checkExistenceOfHTags();
  if (existsHTags) {
    initTocElementsCard(); // 카드 초기화
    giveIdToHTags(); // H 태그에 id 부여
    registerHTagsOnTocCard(); // TOC에 태그 등록
  }
};

H 태그에 ID 부여하기

const giveIdToHTags = function () {
  hTags.forEach((hTag, indexOfHTag) => {
    hTag.id = generateIdOfHTag(indexOfHTag);
  });
}

const generateIdOfHTag = function (indexOfHTag) {
  return 'h-tag-' + indexOfHTag;
}

티스토리의 H 태그들은 기본적으로 아무 id도 없는 상태입니다.
나중에 스크롤 이벤트 같이 특정 태그를 타겟팅해야 하는 상황에 id 값이 필요하기 때문에 H 태그들에게 id를 부여하는 작업을 합니다.

처음에는 소제목 내용에서 공백을 -로 변환하여 id값을 주었는데요. 현재 단락 같은 경우 H-태그에-ID-부여하기 와 같은 식으로요.
그런데 ? 같은 문자가 들어가면 id를 부여할 때 에러가 발생했습니다. 왜지
그래서 고민하다가 그냥 각 태그 index에 prefix를 주어서 id를 만들기로 결정했습니다.

최상위 태그를 판별해서 레벨 부여하기

/* Controller */
const registerHTagsOnTocCard = function () {
  const levelMap = tocCardService.getLevelsByHighestTag();

  tocCardService.registerTagsOnToc(levelMap);
}

이제 TOC 카드에 태그를 등록해 봅시다.

제가 두었던 주안점은 레벨 개념인데요.
H1, H2, H3 순으로 레벨이 낮아지는데, 각 태그에 따라 고유한 CSS를 가지는 것이 아니라 부여된 레벨에 따라서 CSS를 다르게 주어야겠다고 생각했습니다.
예를 들어 H1, H2, H3 태그가 모두 있을 경우에는 H1을 가장 높은 레벨로 설정하여 각 레벨 별로 CSS를 차등하게 주면 되지만, H2, H3만 있는 경우 H2를 가장 높은 레벨, H3만 있는 경우는 H3를 가장 높은 레벨로 하여 CSS를 적용해야 한다고 생각했습니다.

여러 가지 방법이 있겠지만, 저는 각 상황에 따른 레벨 맵을 만들어놓고 가져와서 사용하기로 했습니다.
방금 말한 예시에 맞게, 최상위 태그가 H1인 경우 H1, H2, H3에 차례로 1, 2, 3 레벨을 주고, 최상위 태그가 H2인 경우 차례로 1, 2 레벨, 최상위 태그가 H3인 경우 1 레벨을 부여하였습니다.

const CONSTANTS = (function () {
  // ...

  /* 최상위 태그에 따른 레벨 Map */
  const levelsByH1 = function () {
    return new Map([[KEY_OF_H1, LEVEL_1], [KEY_OF_H2, LEVEL_2], [KEY_OF_H3, LEVEL_3]])
  }

  const levelsByH2 = function () {
    return new Map([[KEY_OF_H2, LEVEL_1], [KEY_OF_H3, LEVEL_2]])
  }

  const levelsByH3 = function () {
    return new Map([[KEY_OF_H3, LEVEL_1]])
  }

  // ...
})();

/** 최상위 태그에 따른 레벨 Map 받아오기
* 
* h1 ~ h3 태그 중 가장 높은 태그를 찾아서 그에 맞게 Level을 설정한다.
* 예를 들어, h1 태그가 없고 h2, h3 태그만 있는 경우
* h2가 가장 높은 태그이며, 해당 태그 h2에 LEVEL_1을 부여하고 그 다음 태그인 h3에는 LEVEL_2를 부여한다.
* 
* 부여된 Level에 따라 적용되는 CSS가 달라진다.
* */
const getLevelsByHighestTag = function () {
  const levelMapByHighestTag = {
    'H1': CONSTANTS.levelsByH1,
    'H2': CONSTANTS.levelsByH2,
  };

  return levelMapByHighestTag[findHighestHTag().tagName] || CONSTANTS.levelsByH3;
}

/* 최상위 태그 판별 작업 */
const findHighestHTagName = function () {
  return [...hTags].reduce((pre, cur) => {
    const tagNumOfPre = parseInt(pre.tagName[1]);
    const tagNumOfCur = parseInt(cur.tagName[1]);

    return (tagNumOfPre < tagNumOfCur) ? pre : cur;
  });
}

최상위 태그를 찾기 위해서 reduce 함수를 활용했는데요, 적절히 사용하면 꽤나 여러가지 일들을 할 수 있는 함수입니다.
아까 찾아 놓았던 H 태그들은 node의 list이기 때문에, ES6문법인 전개구문으로 [...hTags]와 같이 array로 변환하는 작업을 거친 뒤 reduce 함수를 적용합니다. (ES6 문법은 우테코의 '큰곰'님이 도움을 주셨습니다.)
reduce 함수에서는 이전 결과물인 H 태그와 현재 H 태그의 숫자를 비교해서 마지막에 가장 큰 수를 가진 태그를 남기는 작업을 합니다.

TOC Card에 등록하기

그리고 해당 레벨 맵을 이용하여 TOC 태그에 항목들을 차례로 등록합니다.

/* TOC에 태그 삽입 */
const registerTagsOnToc = function (levelMap) {
  hTags.forEach((hTag, indexOfHTag) => {
    let hTagItem;

    levelMap.forEach((level, key) => {
      if (hTag.matches(`h${key}`)) {
        hTagItem = createTagItemByLevel(level, hTag, indexOfHTag);
      }
    })

    tocElementsCard.appendChild(hTagItem);
  });
}

각 H 태그에 대해 어느 레벨인지 찾아서 createTagItemByLevel() 함수를 통해 그에 맞는 TOC 항목을 생성합니다.
생성한 TOC 항목은 TOC Card에 추가합니다.

smooth한 스크롤 이벤트

TOC 항목을 생성할 때 염두에 둘 점 두 가지는, H 태그에서처럼 TOC 항목의 고유 id를 생성하는 것과 스크롤 이벤트를 주는 것입니다.
id는 H 태그 때와 마찬가지로 TOC index에 prefix를 두어 부여하였습니다.
이렇게 되면 기존의 H 태그와 TOC 항목은 각각 같은 index로 매칭되겠네요.

그리고 기본적으로 TOC 항목은 클릭 시 해당 H 태그로 이동하는 이벤트를 가지고 있어야 합니다.
H 태그에 이미 id가 있으므로 Anchor 태그의 href를 통해 URL을 변경하여 클릭 즉시 이동시켜도 되지만, 이동한다는 느낌을 주기 위해 스크롤 이벤트를 주기로 하였습니다.
다음과 같이 모든 TOC 항목에 클릭 이벤트를 걸었습니다.

const appendScrollEventsOn = function (basicItem, indexOfHTag) {
  const target = document.querySelector('#' + generateIdOfHTag(indexOfHTag)); // H 태그의 ID로 target H 태그 찾아오기
  basicItem.addEventListener('click', () => window.scrollTo({
    top: target.offsetTop - 10,
    behavior: 'smooth'
  }));
}

scrollTo() 함수는 스크롤을 이동시키는 메소드인데, 위와 같이 세부 정보를 주면 좀 더 정확한 위치와 특별한 모션을 지정해줄 수 있습니다.
top에 target의 offsetTop 위치 정보를 주고, behavior에 smooth 옵션을 주면 부드러운 이동 모션을 볼 수 있습니다.


스크롤 이벤트

이제 TOC Card의 기본적인 기능은 갖추었습니다.
H 태그들을 뽑아서 목차로 만들고, 클릭하면 해당 태그로 부드러운 스크롤 이동까지 해냅니다.
목표한 기능의 절반 정도를 구현했네요. 이제 나머지는 현재 스크롤 이벤트에 관련한 기능들입니다.

window.onscroll = function () {
  TOC_CARD.onscroll();
}

스크롤 이벤트가 발생할 때마다 TOC Card의 onscroll() 함수를 매번 실행시킵니다.
스크롤 시 작동하는 기능은 다음과 같습니다.

const onscroll = function () {
  const tocTag = tocCardService.findCurrentHTag(); // 현재 스크롤 위치인 TOC 항목을 찾는다.

  tocCardService.markCurrentHTag(tocTag); // 현재 TOC 항목을 표시한다.
  tocCardService.scrollToMainTocTag(tocTag); // TOC 항목이 너무 많아 스크롤이 생길 경우, TOC Card도 자동으로 스크롤된다.
  tocCardService.detectTocCardPosition(); // TOC Card가 게시글 범위에만 머물러 있도록 한다.
}

현재 읽고 있는 내용의 H 태그 찾기

전체 게시글에서 현재 내가 읽고 있는 부분이 어디인지를 색깔 등으로 표시해주고 싶습니다.
글이 길어지면 길어질수록 지금 나의 위치와 방향과 맥락을 잃기 십상이기 때문입니다.
따라서 현재 스크롤의 위치를 기준으로 어디를 읽고 있는지, 정확히는 지금의 스크롤 위치가 어느 H 태그 영역에 속하는지를 찾아내야 합니다.

현재 스크롤의 위치가 H 태그 영역에 속한다는 말이 어떤 상태를 의미하는지를 명확하게 정의할 필요가 있었습니다.
단순하게 지금 보고 있는 화면의 꼭대기가 어떤 태그에 속한다고 정의해서는 안되겠다는 생각이 들었습니다.
화면의 상위 10%는 1번 태그 내용이고, 나머지 아래 90%는 2번 태그 내용이면, 상식적으로 1번 태그가 아니라 2번 태그를 읽고 있는 중일테니까요.
그래서 화면의 딱 중간 부분을 기준으로 내 위치를 정의하기로 했습니다.

const findCurrentMainHTag = function () {
  const headArea = document.querySelector('.area_head');
  const headAreaHeight = headArea !== undefined ? headArea.offsetHeight : 0;
  const middleHeight = window.scrollY + (window.innerHeight / 2) - headAreaHeight;

  return [...hTags].reduce((pre, cur) => {
    if (middleHeight < pre.offsetTop && middleHeight < cur.offsetTop) {
      return pre;
    }

    if (pre.offsetTop < middleHeight && middleHeight <= cur.offsetTop) {
      return pre;
    }

    return cur;
  });
}

middleHeight를 계산하는 로직이 살짝 까다로운데요.
window.scrollY를 하면 전체 문서를 기준으로 해서 현재의 스크롤 위치를 구할 수 있습니다.
다만 티스토리에서는 해당 함수가 상단 헤더의 윗부분이 아니라 아랫부분을 가리키고 있습니다.
정확히 헤더를 포함한 화면의 절반을 계산하고 싶었기에, 현재 창의 절반 높이를 더해준 다음, 헤더의 높이를 따로 계산해서 빼주었습니다.
(나중에 보니 헤더가 없는 스킨들도 있어서 체크 로직을 한 번 두었습니다.)

위에서와 같이 reduce함수를 사용하여 현재 해당하는 태그를 구했습니다.

첫 번째 if문은 모든 H 태그보다 스크롤이 위에 있을 경우입니다. 가장 첫 번째 항목이 마지막에 남겠네요.
두 번째 if문은 pre 태그와 cur 태그 사이에 middleHeight가 있는지를 체크합니다. 마지막에는 현재 위치의 H 태그가 남겠네요.
최종적으로 Controller에게는 H 태그 대신 해당하는 TOC 항목을 return해 줍니다.
다음 작업들에서 해당 TOC 항목이 쓰이기 때문입니다.

현재 위치 태그와 부모 태그 표시하기

const markCurrentHTag = function (tocTag) {
  removeAllClassOnTocTags('toc-active');
  tocTag.classList.add('toc-active');
  markParentHTagOf(tocTag);
}

현재 스크롤 위치의 TOC 항목을 찾았으니 이제 표시를 해줍니다.
CSS는 미리 정의해 둔 클래스를 기반으로 적용합니다.
매 순간마다 이전에 적용되었던 해당 클래스를 모든 태그에서 지우고, 현재 태그에만 클래스를 추가합니다.

그리고 다음으로는 해당 태그의 부모 태그들을 찾아 표시해보도록 하겠습니다.
지금 왼쪽에서 보시는 것처럼 상위 레벨 태그들을 찾아 옅은 색으로 표시해주는 작업입니다.

/**
* 현재 active 태그의 부모 레벨 태그를 표시 
* 기준 태그(active 태그)애서 하나씩 위로 올라가면서 부모 태그를 탐색 (재귀)
* */
const compareLevelAndMark = function (levelOfBaseTocTag, indexOfCurrentTocTag) {
  if (levelOfBaseTocTag <= 1 || indexOfCurrentTocTag < 0) {
    return;
  }

  const currentTocTag = document.querySelector(`#toc-${indexOfCurrentTocTag}`);
  const levelOfCurrentTocTag = findLevelOfTocTag(currentTocTag);

  if (levelOfBaseTocTag <= levelOfCurrentTocTag) {
    return compareLevelAndMark(levelOfBaseTocTag, indexOfCurrentTocTag - 1);
  }

  currentTocTag.classList.add('toc-parent-active')
  compareLevelAndMark(levelOfBaseTocTag - 1, indexOfCurrentTocTag - 1);
}

어떻게 구현할까 하다가 재귀로 구현해봤는데요.
마침 태그들이 index 기반이니 현재 TOC 항목을 기준으로 하나씩 거꾸로 위로 올라가면서 부모 태그인지 아닌지를 판별합니다.
레벨이 1이거나, index가 0 미만이면 부모 태그를 다 찾은 것이니 알고리즘을 종료합니다.
각 단계마다 레벨을 비교하여 부모 태그 클래스를 줄지를 결정합니다.

부모 레벨 색깔을 꽤 오래 고민했는데요. 1시간 넘게 정했습..

지인인 디자이너 분께 여쭤보다가 Adobe Color라는 꿀팁 사이트를 알려주셨습니다.
여러 데이터에 기반해서 색을 추천해 주는데요.
가운데 색을 기준으로 유사한 색, 보색 등등의 색깔을 추천해주고, 내가 좋아하는 이미지에서 색깔을 추출해낼 수도 있는 아주 좋은 사이트입니다.
PPT 만들 때도 사용하기 좋을 듯 싶네요!

TOC Card 스크롤 동기화

/**
* TOC 항목이 너무 많아 TOC Card에 스크롤이 생길 경우, 
* 스크롤 이벤트에 따라 활성화된 TOC 태그가 보이도록 TOC Card의 스크롤도 함께 이동한다.
*/
const scrollToMainTocTag = function (tocTag) {
  tocElementsCard.scroll({
    top: tocTag.offsetTop - (tocTag.offsetParent.offsetHeight * 0.3),
    behavior: 'smooth'
  });
}

만약 글이 정말정말 길어져서(이 글은 아쉽게도 그렇지 않지만) TOC 항목들이 일정 height를 넘어가면 스크롤이 생기도록 했습니다.
그런데 스크롤이 생겨서 보이지 않는 아래쪽 태그들은 어떻게 보여줄 수 있을까? 라는 의문이 들었습니다.
사용자가 아래쪽으로 게시글의 스크롤을 내리면 옆에 있는 TOC Card의 스크롤도 동기화되어 같이 움직여야 한다고 생각했습니다.

원리는 위에서 보았던 스크롤 이벤트와 동일합니다. top의 위치만 잘 계산해주면 되겠네요.
지금 이 글에서는 브라우저의 세로 길이를 반으로 확 줄이신 다음 게시글을 스크롤 해보시면 해당 이벤트를 목격하실 수 있습니다...! (뿌듯)

게시글 영역 보존하기

브라우저의 가로 길이도 신경을 써야 하는데요.
현재 TOC Card는 게시글과 같은 레이어가 아니라 위의 헤더와 같은 레이어기 때문에, 아무런 설정도 하지 않으면 브라우저의 가로 길이를 줄였을 때 TOC Card가 글을 뒤덮게 됩니다.
따라서 브라우저가 일정 길이 이하가 되면 TOC Card가 사라지도록 했습니다. 아래와 같이 CSS에서 미디어 쿼리로 간단하게 설정하였습니다.

/* toc.css */
@media (max-width: 83.75rem) {
    .toc-app-common {
        visibility: hidden;
    }
}

게시글과 붙어있기

이제 마지막입니다! 저는 왼쪽의 카드가 맨 아래 footer 영역까지 침범하지 않았으면 좋겠다고 생각했습니다.
그래서 마찬가지로 스크롤을 탐지해서, 맨 아래에서는 position을 absolute, 가운데 게시글 영역에서는 position을 fixed로 주었습니다.

/* toc.css */
.toc-app-basic {
    position: fixed;
}

.toc-app-bottom {
    position: absolute;
}

저에게 조언을 주셨던 선배 개발자 분께서는 보통은 header와 footer 영역에 자리를 차지하는 태그를 두고, Card가 자연스럽게 그 사이에서만 활동하도록 하면 된다고 말씀해 주셨는데요.
기존 티스토리 스킨의 소스 위에 무엇인가를 얹으려는 거라 구조를 바꾸기가 쉽지가 않았습니다.
다 만들고 보니 스크롤 시 동작하는 로직이 너무 많은 것 같다는 조금 아쉬운 생각이 드네요.


정리

필요한 게 없으면 아쉬운 사람이 만들어야 한다는 선배들의 명언을 다시금 체감하게 해준 미니 토이 프로젝트였습니다.
개인 프로젝트고, 바닐라 JS로 하나씩 짜다 보니 또 새롭게 다가오는 것들이 많았네요.
블로그에 대한 애정도 좀 더 생겼습니다. 스킨도 정비 했으니 다시 꾸준한 컨텐츠로 달려보겠습니다. :)

깃헙 저장소는 아래에 있습니다. 혹여나 사용하실 분들은 별 하나씩 눌러주세용ㅎㅎ 감사합니다!

wbluke/tistory-table-of-contents

'Frontend > javascript' 카테고리의 다른 글

이터러블/이터레이터 프로토콜  (0) 2020.05.31
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday