이 포스트에서는 Table of Contents(목차) Component를 만들고 본문 우측 상단에 위치시키는 과정에 대해서 알아본다.
TableOfContent 필드 확인
각 markdown 문서의 목차는 이미 gatsby-transformer-remark
플러그인에 의해서 HTML로 만들어져 있다. 이 플러그인은 gatsby-starter-blog
에 기본적으로 들어 있는 플러그인으로 markdown문서를 HTML 문서로 변환해주는 역할을 한다.
{
allMarkdownRemark {
nodes {
id
fields {
slug
}
tableOfContents // highlight-line
}
}
}
GraphQL 콘솔에서 위와 같이 쿼리해보면 노드의 tableOfContents 필드에 HTML 이 들어 있는 것을 알 수 있다. 스타일은 적용되어 있지 않으며, <a href='/slug/#header-title'>
형태의 링크까지 제공된다.
어떤 블로그에서는 결과물로써 HTML이 나오기 때문에 이 것을 활용하려면 파싱이 필요하다 한다. HTML이 아닌 데이터구조로 목차가 제공되는 mdx
로의 전환을 고려하는 방법도 있다. 아마 특정 UI framework을 적용 했을 경우 제공되는 List
와 ListItem
류의 Component를 사용하길 원해서일 것이라 추측된다.
하지만 내 생각은 조금 다르다. 결과적으로 그런 Component가 최종적으로 만들어내는 결과물은 은 결국 <ul>
와 <li>
이기 때문에 다를 바가 없다. 목차는 고 수준의 기능을 요하는 것이 아니기에 간단한 스타일만 입혀 준다면 그 결과는 같다.
{
"id": "26d3eb03-c789-568f-b537-6803e3c98ec0",
"fields": {
"slug": "/2020/12/03/add-table-of-content-gatsby/"
},
"tableOfContents": "<ul>\n<li><a href=\"/2020/12/03/add-table-of-content-gatsby/#%EC%BF%BC%EB%A6%AC%EB%A5%BC-%ED%86%B5%ED%95%B4%EC%84%9C-tableofcontent%EB%A5%BC-%ED%99%95%EC%9D%B8%ED%95%98%EC%9E%90\">쿼리를 통해서 tableOfContent를 확인하자</a></li>\n<li><a href=\"/2020/12/03/add-table-of-content-gatsby/#%EB%AA%A9%EC%B0%A8-%EC%BB%B4%ED%8D%BC%EB%84%8C%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0\">목차 Component 만들기</a></li>\n<li><a href=\"/2020/12/03/add-table-of-content-gatsby/#%EB%AA%A9%EC%B0%A8%EB%A5%BC-blog-post-template%EC%97%90-%EB%B6%99%EC%9D%B4%EA%B8%B0\">목차를 blog post template에 붙이기</a></li>\n</ul>"
}
Table of Contents Component 만들기
tableOfContents 필드에서 제공하는 HTML을 그대로 사용할 수 있는 React Component를 사용한다.
React Component 생성
import React from "react"
const TableOfContents = ({ content }) => {
return (
<div
// 스타일링을 위해서 클래스이름 부여 한다.
className="table-of-content"
// dangerouslySetInnerHTML는 보안 관점에서 위험하지만
// innerHTML을 사용하겠다는 뜻이다.
dangerouslySetInnerHTML={{ __html: content }}
/>
)
}
export default TableOfContents
스타일 적용
React Component에 스타일을 적용하는 방법은 여러가지가 있다.
style.css
같은 global CSS파일을 수정하는 전통적인 방법.- CSS-module: Component를 위한 CSS를 작성하고 그 것을 각 Component에서
import
하는 방식. - CSS-in-JS: 코드 안에서 스타일을 작성는 방식.
어떤 것을 사용해도 상관없지만 적어도 CSS
를 고수하는 사람이 있다면 SCSS
을 사용을 추천한다. CSS
의 superset으로 CSS
를 그대로 사용할 수 도 있으며, 다음과 같은 계층구조를 가진 CSS
작성이 가능하다. 계층구조에서는 .table-of-content
을 반복적으로 쓰지 않아도 된다. (플러그인: gatsby-plugin-sass
)
.table-of-content {
ul {
margin-left: "0px";
li {
color: blue;
a {
decoration: none;
}
}
}
}
우리는 미리 만들어진 HTML에 스타일을 적용해야 한다는 점이 특히 중요하다. React Component를 생성 시점에 <ul>
, <li>
각각에 className
혹은 style
을 할당하는 일반적인 상황이 아니다. 따라서 위의 예제와 같이 .table-of-contents
클래스를 기준으로 내부에 있는 ul
및 li
그리고 a
까지 스타일을 설정해야 한다.
만든 Component 적용하기
이제 TableOfContents
Component를 가져다 쓰기만 하면 되는데 여기서부터 HTML와 css
에 대한 약간의 지식이 필요하다.
레이아웃 수정
우선 기본으로 구성되어 있던 레이아웃을 다음과 같이 변경하고자 한다.

포스트의 헤딩, 본문, 풋터 전체를 감싸고 있던 컨테이너인 <article>
외부에 flexbox 컨테이너를 만들어 감싼다. 그 후 <article>
을 하나의 flex item으로 만들고, 새로 추가하는 <TableOfContents>
Component도 flex item으로 만든다.
src/templates/blog-post.tsx:
import TableOfContents from "../components/table-of-contents"
const BlogPostTemplate: React.FC<PageProps<BlogPostBySlugQuery>> = ({
data,
location,
}) => {
return (
<Layout location={location} title={siteTitle}>
<div className="blog-post-container">
<div className="content">
<article>...</article>
<nav className="blog-post-nav">...</nav>
</div>
<TableOfContents content={data.markdownRemark.tableOfContents} /> </div>
</Layout>
)
}
export default BlogPostTemplate
export const pageQuery = graphql`
query BlogPostBySlug(...) {
...
markdownRemark(id: { eq: $id }) {
...
tableOfContents
}
...
}
`
스타일 적용
목표한 레이아웃과 같게 태그를 구성하고, 쿼리문에는 tableOfContents
필드만 추가한다. 그리고 다음과 같이 대략적인 레이아웃을 잡기 위한 스타일을 추가한다.
.blog-post-container {
display: flex;
flex-wrap: wrap;
flex-direction: row;
.content {
flex-grow: 0;
max-width: calc(100% * 2 / 3);
flex-basis: calc(100% * 2 / 3);
}
.table-of-content {
top: 0;
flex-grow: 0;
max-width: calc(100% / 3);
flex-basis: calc(100% / 3);
margin-left: 1rem;
max-width: 18rem;
max-height: calc(100vh - 200px);
position: sticky; overflow: auto;
}
}
한가지 눈 여겨봐야할 css
속성은 .table-of-content
에 쓰인 position: sticky
이다. 페이지 전체를 스크롤 할때 같이 올라가다가 최상단에 이르면 더이상 위로 올라가지 않게 만들 때 쓰인다. 본문이 스크롤된다고 해서 목차 메뉴가 사라지면 매우 불편할 것이기 때문에 반드시 적용해야하는 속성이다.
이제 그림과 같이 목차 메뉴가 본문 우측에 배치되었다.

링크가 동작하도록 하는 방법
만들어진 목차의 아이템 중 하나를 클릭해 보면 브라우저의 주소창에 해시 부분이 변경되지만 아무일도 일어나지 않는 것을 알 수 있다. URL의 가장 뒤에 붙은 #해쉬
와 같은 id를 가진 element가 있어야 하기 때문이다. 현재는 헤딩 태그(<h1>
, <h2>
) 에 id가 할당되어 있지 않기 때문이다.
그 작업을 해주는 플러그인이 gatsby-remark-autolink-headers
이다. npm
또는 yarn
으로 설치하고 다음과 같이 gatsby-config.js
에 추가한다. 한가지 재미 있는 사실은 이 플러그인이 "플러그인의 플러그인"이라는 사실이다. 이게 가능하게 만들 것이라고는 상상하지도 못한 것인데 역시 Gatsby는 훌륭하다.
{
resolve: `gatsby-transformer-remark`,
options: {
plugins: [
// 생략
`gatsby-remark-autolink-headers`,
],
}
},
플러그인 설치 후 바뀐점 확인
모양부터 기능에 영향을 미치는 다양한 옵션이 설정 가능한 것처럼 보이지만 일단은 그냥 추가했다. 이제 목차의 아이템 중 하나를 클릭하면 해당 섹션으로 이동하는 모습을 볼 수있다. 플러그인 설치 후 달라진 것은 각 헤딩 제목 좌측에 링크를 뜻하는 듯한 아이콘이 추가된 것이다.

그리고 markdown에 의해서 만들어진 헤딩 태그에 id
가 부여된 것을 확인할 수 있다. 브라우저는 url
뒤에 붙어있는 해쉬 값을 읽고 그 값을 id
로 가지고 있는 엘리멘트를 찾아 화면을 가장 상단에 보여주도록 스크롤한다.

끝.