Next.js 블로그 제작기 (6)

user profile img

신현호

TypeScript
Next.js

블로그 제작기

Post Thumbnail

목차

    서론

    생각보다 빠르게 블로그 제작기 6편을 올리게 되었네요, 포스팅의 기조를 바꾸겠다고 했지만 그래도 블로그 유지보수는 재밌기도 하고..
    이왕 시간들여서 고쳐놓은김에 학습한 내용과 구현한 기능들에 대해서 정리하는 건 나쁘지 않겠다는 생각이 들었습니다.

    그럼 바로 이번 블로그 제작기를 시작해보도록 하겠습니다 :)

    변경점

    내부 TOC

    원래 저는 이전까지 TOC를 마크다운을 통해서 작성했었습니다.

    ## 목차
    
    - [나만의 블로그를 가져보면 어떨까](#나만의-블로그를-가져보면-어떨까)
    - [그래서 어떻게 만들까](#그래서-어떻게-만들까)
      - [Gatsby](#gatsby)
      - [Next.js](#nextjs)
    - [저는 Next를 골랐습니다](#저는-next를-골랐습니다)
    - [포스팅 계획](#포스팅-계획)
    

    이렇게 말이죠. 마크다운(mdx) 자체에서 TOC를 지원하기때문에 이런식으로 작성해도 TOC가 잘 생성되었습니다.. 만
    문제가 하나 있었다면, 마크다운 내부에서 TOC를 작성하게되면 별도의 스타일링이 어렵다는 문제가 있었습니다.

    당초 원하던 방향은 목차의 ul태그에만 스타일링을 부여하는 것 이었지만 포스팅 본문에 적어놓은 TOC를 통해 파싱하게되면 ul태그 스타일링시 생성되는 모든 ul태그에 스타일이 적용되었기 때문입니다.
    그래서 그냥 이참에 TOC를 블로그 포스트 문서에 작성하지 않고 태그를 파싱하여 TOC를 생성하는 방향으로 가닥을 잡았습니다.

    제 블로그에서는 포스트 내부의 제목과 소제목에는 일괄적으로 h2태그와 h3태그만을 사용하고있습니다. 이는 컴포넌트에서도 마찬가지입니다.
    따라서, 저는 포스트 내부에만 존재하는 h2태그와 h3태그만을 파싱하면 TOC를 생성할 수 있었습니다.

    이렇게 되면 구현해야하는 내용들이 간단해집니다. 자바스크립트에는 document.querySelector 가 있기때문인데요.
    저는 포스팅 내부에 존재하는 h2태그와 h3태그만을 가져오고싶었기때문에 해당하는 컴포넌트에 id를 추가해주었습니다.

    tsx

    import '../style/Intellij-prism.css'
    import { type Post } from '@/contentlayer/generated'
    import { useMDXComponent } from 'next-contentlayer/hooks'
    import mdxComponents from './MdxComponents'
    
    interface MdxRendererProps {
      post: Post
    }
    
    export default function MdxRenderer({ post }: MdxRendererProps) {
      const MDXContent = useMDXComponent(post.body.code)
    
      return (
        <div
          id="content"
          className="prose prose-neutral font-BlogPost max-w-full overflow-hidden dark:text-white">
          {post.body.code !== undefined && (
            <MDXContent components={mdxComponents} />
          )}
        </div>
      )
    }
    

    그리고 InternalToc.tsx파일을 생성하여 querySelector를 통해 h2, h3태그들을 받아와줬습니다.

    tsx

    
    import getConvertedTextContent from '@/utils/getConvertedTextContent'
    import { ArrowRightIcon, BookmarkIcon } from '@heroicons/react/20/solid'
    import Link from 'next/link'
    import { useEffect, useState } from 'react'
    
    export default function InternalToc() {
      const [headingElements, setHeadingElements] = useState<
        { index: string; size: number }[]
      >([])
    
      useEffect(() => {
        const contentElement = document.querySelector('#content')
    
        if (contentElement) {
          const headingElementList = Array.from(
            contentElement.querySelectorAll('h2, h3')
          )
    
          setHeadingElements(
            headingElementList.map((header) => ({
              index: header.textContent as string,
              size: (+header.nodeName[1] - 1) * 20,
            }))
          )
        }
      }, [])
    
      return (
        ...
      )
    }
    

    간단하게 위 코드에서 한 일을 요약해보자면

    1. querySelector를 사용하여 범위를 지정해주고
    2. querySelectorAll를 사용하여 해당 영역에 존재하는 h2, h3태그를 받아왔습니다.

    size는 제목과 소제목의 크기와 들여쓰기를 적절하게 적용해주기 위해 사용합니다.

    여기에서 생각을 해보셔야하는 점은, querySelector로 받아온 결과값은 NodeList 형태라는 것 입니다.

    NodeList 는 Array는 아니지만 forEach를 사용할 수 있고, Array.from으로 배열화도 가능합니다.

    사이드바 TOC

    다른 블로그들을 보면서 부러웠던 기능이었는데, 의외로 어렵지 않게 구현했던 사이드바 TOC입니다.
    가장 기본적으로는 Intersection Observer에 대해서 아시면 좋습니다. 차후 포스트에서도 따로 다룰 것 같은데,
    간단하게 말하면 Intersection Obeserver는 브라우저 뷰포트와 원하는 요소의 교차점을 관찰하여 요소가 뷰포트에 포함되는지 아닌지 구별하는 기능을 제공합니다.

    따라서 해당 기능을 사용하면 우리가 찾고자 하는 (여기에서는 h2태그와 h3태그가 되겠죠?) 요소들의 뷰포트 포함여부를 쉽게 알 수 있는 것 입니다.

    그럼 Intersection Observer를 통해 Observer를 생성해봅시다

    ts

    import { Dispatch, SetStateAction } from 'react'
    
    const observerOption = {
      threshold: 0.4,
      rootMargin: '-76px 0px 0px 0px',
    }
    
    const getIntersectionObserver = (
      setState: Dispatch<SetStateAction<string>>
    ) => {
      let direction = ''
      let prevYPosition = 0
    
      const checkScrollDirection = (prevY: number) => {
        if (window.scrollY > prevY) {
          direction = 'down'
        } else if (window.scrollY < prevY) {
          direction = 'up'
        }
    
        prevYPosition = window.scrollY
      }
    
      const observer = new IntersectionObserver((entries) => {
        entries.forEach((entry) => {
          checkScrollDirection(prevYPosition)
    
          if (
            (direction === 'down' && !entry.isIntersecting) ||
            (direction === 'up' && entry.isIntersecting)
          ) {
            setState(entry.target.id)
          }
        })
      }, observerOption)
    
      return observer
    }
    
    export default getIntersectionObserver
    

    해당 Observer는 스크롤의 위, 아래 방향을 감지하고 뷰포트 안에 들어온 태그들을 감지하게됩니다.
    그럼 아까 InternalToc에서 이 Observer만 추가해주면 사이드바 TOC를 구현할 수 있다는 걸 알 수 있습니다.

    tsx

    'use client'
    
    import getConvertedTextContent from '@/utils/getConvertedTextContent'
    import getIntersectionObserver from '@/utils/getIntersectionObserver'
    import { ArrowRightIcon } from '@heroicons/react/20/solid'
    import Link from 'next/link'
    import { useEffect, useState } from 'react'
    
    export default function SidebarToc() {
      const [currentId, setCurrentId] = useState<string>('')
      const [headingElements, setHeadingElements] = useState<
        { index: string; convertedIndex: string; size: number }[]
      >([])
    
      useEffect(() => {
        const observer = getIntersectionObserver(setCurrentId)
        const contentElement = document.querySelector('#content')
    
        if (contentElement) {
          const headingElementList = Array.from(
            contentElement.querySelectorAll('h2, h3')
          )
    
          setHeadingElements(
            headingElementList.map((header) => ({
              index: header.textContent as string,
              convertedIndex: getConvertedTextContent(header.textContent as string),
              size: (+header.nodeName[1] - 1) * 20,
            }))
          )
          headingElementList.forEach((header) => observer.observe(header))
        }
      }, [])
    
      return (
        <aside className="font-BlogPost invisible fixed right-10 text-black xl:visible dark:text-white">
          ...
        </aside>
      )
    }
    

    추가된 부분에 대해서 설명을 하자면

    1. 현재 가르키는 요소의 id를 담을 State를 생성했습니다.
    2. IntersectionObserver를 생성했습니다.
    3. headingElementList를 순회하며 생성된 observer가 관찰할 요소들을 추가시켜줍니다.

    이렇게 하면 SidebarToc 또한 쉽게 만들어 줄 수 있습니다.

    제목과 소제목에 바로가기 버튼 추가

    이것 역시 다른 블로그들에 존재하는 기능이었는데, 제목이나 소제목에 마우스 호버 시 바로가기 # 버튼이 띄워지는 기능입니다.
    왜 필요하지? 별거 아니지 않나 싶지만 막상 생성해서 사용해보니까 생각보다 쏠쏠하더라구요, 그리고 화면 사이즈가 크지 않으면 SidebarToc를 보이지 않게 해놓았기에 해당 경우에서 유용하게 사용되었습니다.

    포스트 내부 디자인 수정

    포스팅을 보다보니까 h2/h3 태그 사이에 구분감이 별로 없어서 항목이 넘어간다는 생각이 잘 안들게되더라구요. 그래서 구분선을 통해 확실히 구분시켜주었습니다.
    개인적으로 간단한 작업이었지만 가독성이 많이 올라가서 만족하고있습니다.

    아직 못다한 작업

    yarn-berry 적용

    우선순위가 약간 밀린 yarn-berry.. 언젠간 진행.. 예정입니다..

    useEffect useMemo 를 통한 최적화

    제가 해당 부분에 대한 지식과 테크닉이 많이 부족하다고 생각해서 학습 후 해당 내용을 적용시켜보려고 합니다.
    아마 이 작업이 yarn-berry 적용보다 더 우선순위가 높을 것 같습니다.

    공방, 배움 탭 분리

    해당 페이지는 개별적인 페이지로 분리 작업중입니다. 가장 우선시 하고 있는 작업입니다.

    Profile Image

    신현호

    Frontend Developer

    프론트엔드 개발자를 꿈꾸고 있는 대학생입니다. 끊임없이 배우고 성장하는 개발자가 되기 위해 노력하고 있습니다.

    블로그 제작기

    총 11개의 포스트가 존재합니다.