logo

𝝅번째 알파카의 개발 낙서장

정신차려보니 블로그를 4번이나 갈아 엎은 건에 대하여

프로젝트
⏰ 2023-09-08 18:00:59

D O W N

https://github.com/RWB0104/blog.itcode.dev/assets/50317129/2bb32e45-a33c-4cd2-9ad7-86a36f14abd9
https://user-images.githubusercontent.com/50317129/260317030-e4b8575b-f09e-47f4-ab70-168a817268c6.png

Table of Contents

https://user-images.githubusercontent.com/50317129/260317030-e4b8575b-f09e-47f4-ab70-168a817268c6.png

정신차려보니 블로그를 4번이나 갈아 엎은 건에 대하여

공사 중...

블로그 마음에 안 든다고 세 번째 갈아엎은지가 얼마 안 된 거 같은데, 눈 떠보니 어느새 네 차례나 갈아엎고 있는 나를 봤다.

블로그 마지막 글이 거의 반 년 전 글인데, 토이 프로젝트 개발하느라 블로그에 거의 신경을 안 썼다. 더군다나, 3차 개편 이후로 여러 프론트 기술을 습득하게 됐다. 이 때문에 덩달아 내 눈도 높아져버린 탓에, 블로그의 UI가 너무 마음에 들지 않았던 것도 한 몫했다.

다행히 써둔 글들도 꽤 있었고, 그 중엔 나름 효자라 불릴만한 게시글도 몇 개 있어서 방문자가 감소하지는 않았다.

개편 내용

생각해보면, 3차 개편은 사실 "했어야만" 했던 개편이다. 2차까지만 해도 내 React 실력은 과도기였다. 만들면서도 석연찮은 부분이 많았으며, 분명 더 깔끔하고 정교한 패턴이 있을거라 짐작하면서도 뭐가 뭔지 모르니 찾아보기도 애매했다. 3차가 되서야 UI는 물론, 구조적으로도 기틀이 잡힌 셈이다.

하지만 엄밀히 말하면 기틀만 잡힌 셈이지, 막상 까보면 게임 중반의 젠가마냥 허술한 부분이 한 두 가지가 아니였다. 숭숭 뚫린 구멍이 보임에도, 그 땐 이를 메울만한 역량이 되지 못 했다. 구멍 몇 개 쯤 있어도 블로그가 돌아가는데 큰 문제가 없던 덕분에, 딱히 고칠 생각도 시간도 내지 않았다.

4차 개편에서 중점적으로 다룬 사항은 UI의 개선과 더불어, 위와 같은 구멍들을 메꾸는 것이였다. 대략적인 개편 내용은 아래와 같다.

1. Next.js 13 적용

원래부터 이 블로그는 Next.js를 사용하고 있었다. Next.js가 13으로 버전업을 하게 되면서, 몇 가지 눈에 띄는 변경점이 생겼다.

  • app 폴더 구조 적용
  • client / server 컴포넌트 구분 명시

app 폴더 구조 적용

작년까지만 해도 실험 기능이였던 app 폴더 구조가 메인으로 올라온 듯 하다. 그 당시 토이 프로젝트를 구축하려고 create-next-app 스크립트를 돌렸을 때, 해당 옵션의 활성화 여부를 물어봤던 기억이 난다.

그 땐 실험적 기능(experimental)이라 하기에 쓰질 않았어서 직접 사용하는 건 이번이 처음이였다. app 하위에 페이지 컴포넌트가 위치하게 되고, 파일 자체가 페이지의 명세가 되는 구조라 한다. 예를 들면 아래와 같다.

TXT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
🏠 project
├─ 📂 app/
│   ├─ layout.tsx
│   ├─ template.tsx
│   ├─ error.tsx
│   ├─ loading.tsx
│   ├─ page.tsx
│   └─ 📂 home/
│       ├─ layout.tsx
│       ├─ template.tsx
│       ├─ error.tsx
│       ├─ loading.tsx
│       └─ page.tsx
└─ ...

위와 같은 방식으로 명세가 이루어지고, 각 파일은 그에 해당하는 역할이 주어져 있다. 이것이 파일 구조만으로 명세가 되는 이유이다.

위의 구조는 내부적으로 아래와 같이 적용된다.

TSX

1
2
3
4
5
6
7
8
9
10
11
12
// root
<Layout>
    <Template>
        <ErrorBoundary fallback={<Error />}>
            <Suspense fallback={<Loading />}>
                <ErrorBoundary fallback={<NotFound />}>
                    <Page />
                </ErrorBoundary>
            </Suspense>
        </ErrorBoundary>
    </Template>
</Layout>

루트 페이지는 위와 같이 구현된다. 위 예시의 home과 같은 하위 페이지는 아래와 같이 중첩으로 적용된다.

TSX

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// home
<Layout>
    <Template>
        <ErrorBoundary fallback={<Error />}>
            <Suspense fallback={<Loading />}>
                <ErrorBoundary fallback={<NotFound />}>
                    <Layout>
                        <Template>
                            <ErrorBoundary fallback={<Error />}>
                                <Suspense fallback={<Loading />}>
                                    <ErrorBoundary fallback={<NotFound />}>
                                        <Page />
                                    </ErrorBoundary>
                                </Suspense>
                            </ErrorBoundary>
                        </Template>
                    </Layout>
                </ErrorBoundary>
            </Suspense>
        </ErrorBoundary>
    </Template>
</Layout>

Next.js 13을 설명하는 게시글은 아니므로, 이 정도의 변경점이 있다는 정도만 짚고 넘어가고자 한다. 중요한 점은, 페이지 코드만 기술했던 구버전과 달리, 페이지의 레이아웃이나 템플릿도 페이지 단계에서 관리가 가능하다는 점이다.

이러한 구조를 정확하게 지킨다면, 페이지의 구조를 직관적으로 파악할 수 있을 것이다. 물론, 강제는 아니라서 페이지만 기술하고 레이아웃이나 템플릿은 기존처럼 관리해도 상관은 없다. 나 역시도 레이아웃 같은 건 편의상 그냥 기존처럼 컴포넌트 따로 따서 import 시키는 방식을 차용하고 있다.

자세한 내용은 🔗 Next.js 라우팅 공식 문서에서 확인할 수 있다.

컴포넌트 구분 명시

조금 생소했던 변경점으로, 컴포넌트에 client / server 컴포넌트임을 반드시 명시해줘야한다. 기본값은 서버 컴포넌트인듯 하다. 컴포넌트의 구분에 따라 할 수 있는 기능이 다르다.

기능serverclient
데이터 Fetch
백엔드 리소스에 직접 접근
서버에 민감한 정보 관리 (토큰 등)
서버의 큰 종속성 유지 / 클라이언트 측 JS 코드 축소
이벤트 리스너 사용 (onChange 등)
상태관리 및 수명주기 사용 (useEffect 등)
브라우저용 API 사용 (Navigator API 등)
커스텀 Hook 사용
React class 컴포넌트 사용

각 구분에 따른 기능은 이와 같다. 새로울 건 없다. 구버전은 이러한 기능이 컴포넌트에 통합되어 있었으나, 13부터는 이를 명시적으로 구분하겠다는 것이다.

getServerProps와 같은 기능은 server 컴포넌트에서 사용 가능하다. 반대로, useState 혹은 쿠키, localStorage 등을 활용하기 위해선 client 컴포넌트에서 사용 가능하다.

각 사용 방법은 아래와 같다.

TSX

1
2
3
4
5
6
// 서버 컴포넌트 (명시하지 않아도 됨)

export default function Component(): ReactNode
{
    // ...
}

TSX

1
2
3
4
5
6
7
// 클라이언트 컴포넌트
'use client'

export default function Component(): ReactNode
{
    // ...
}

만약, 각 컴포넌트에 위반하는 기능을 사용했을 경우, 친절하게 오류를 띄우며 알려주니 보고 바꿔주면 된다. 만약, 서버/클라이언트 기능이 혼용되었다면, 이는 구조적으로 올바르지 않은 것이므로 컴포넌트를 분리해야한다.

이와 관련된 자세한 내용은 🔗 Next.js 렌더 공식 문서에서 확인할 수 있다.

2. atomic 폴더 구조 적용

예전에 컴포넌트 구조를 짜면서, 뭔가 분리해서 관리하는 걸 좋아했던 걸로 기억한다. 그 가치관이 조금 괴악하게 적용이 됐는데, 아래와 같은 구조다.

TSX

1
2
3
4
5
6
7
8
9
10
🏠 project
├─ 📂 src/
│   ├─ 📂 components/
│   │   ├─ 📂 Card/
│   │   │   ├─ Card.tsx
│   │   │   └─ index.ts
│   └─ 📂 styles/
│       └─ 📂 Card/
│            └─ Card.module.scss
└─ ...

왜 그랬는지 모르겠는데, 암튼 그 땐 저런식으로 구조를 가져갔다. 아무리봐도 해괴한 구조다.

TSX

1
2
3
4
5
6
7
8
9
10
11
12
🏠 project
├─ 📂 src/
│   └─ 📂 components/
│       ├─ 📂 atom/
│       │   └─ 📂 Card/
│       │       ├─ Card.tsx
│       │       ├─ Card.module.scss
│       │       └─ index.ts
│       ├─ 📂 molecule/
│       ├─ 📂 organism/
│       └─ 📂 template/
└─ ...

위와 같이 atomic 기반의 폴더 구조를 적용했다.

3. 마크다운 변환 로직 개선

이 개편에서 제일 마음에 들었던 부분. 기존의 마크다운 변환 로직은 문제가 좀 많았다 marked 라이브러리를 기반으로 변환 로직을 구성했으나, 렌더링이 string 기반이라, React 컴포넌트를 적용하기 매우 어려웠다는 단점이 있었다.

때문에 마크다운만큼은 순수한 HTML, JavaScript, CSS만으로 구성해야했다. 블로그의 기반이 React인 탓에, 가장 중요한 컨텐츠가 따로국밥이 되어버린 점이 항상 어쉬웠다. 더군다나, HTML 태그를 순수 문자열로 관리하는 작업은 유지보수성이 떨어질 뿐더러, 못생기기까지 했다.

TS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 코드블럭 렌더링
renderer.code = (code: string, lang: string = 'txt'): string =>
{
    // 유효한 언어가 있을 경우
    if (lang && renderer?.options?.highlight)
    {
        // 블록 수식일 경우
        if (lang === 'latex-block')
        {
            const katexText = katex.renderToString(code, { output: 'html', throwOnError: true });

            return `<div class="katex-block">${katexText}</div>`;
        }

        // 아닐 경우
        code = renderer.options.highlight(code, lang as string) as string;

        const langClass = `language-${lang}`;

        while (COMMENT_REGX.test(code))
        {
            const [ origin, target ] = COMMENT_REGX.exec(code) as string[];

            const newer = target.split('\n').map((item) => `<span class="token comment" data-tag="new">${item}</span>`).join('\n');

            code = code.replace(origin, newer);
        }

        const line = code.split('\n').map((item, index) => `<tr data-number=${index}><td class="line-number" data-number="${index}">${index}</td><td class="line-code" data-number=${index}>${item}</td></tr>`).join('\n').replace(/\t|\\n/, '');

        return `
            <div class="block-code">
                <div class="top">
                    <p>${lang.toUpperCase()}</p>
                    <div></div>
                    <div></div>
                    <div></div>
                </div>

                <button onclick="copyCode(this);"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" data-icon="clipboard" class="i-clipboard"><path fill="currentColor" d="M336 64h-80c0-35.3-28.7-64-64-64s-64 28.7-64 64H48C21.5 64 0 85.5 0 112v352c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48zM192 40c13.3 0 24 10.7 24 24s-10.7 24-24 24-24-10.7-24-24 10.7-24 24-24zm144 418c0 3.3-2.7 6-6 6H54c-3.3 0-6-2.7-6-6V118c0-3.3 2.7-6 6-6h42v36c0 6.6 5.4 12 12 12h168c6.6 0 12-5.4 12-12v-36h42c3.3 0 6 2.7 6 6z"></path></svg></button>

                <pre class="${langClass}"><table><tbody>${line}</tbody></table></pre>
            </div>
        `;
    }

    return '';
};

마크다운 변환 로직 중, 가장 규모가 크고 중요한 코드블럭 로직이다. 한 눈에 봐도 못생긴 코드임은 말 할 필요도 없을 뿐더러, 저런 식이면 후에 커스터마이징하기도 골치아프다.

이번 개편 때 이 부분은 최대한 바꾸고자 고민을 많이 했다. marked를 계속 사용하되, 좀 더 나은 방법을 찾던가, 비슷한 라이브러리인 unified를 사용하는 방법이 있었다. 이전에도 했던 고민이지만, 그 당시엔 marked의 공식문서가 더 직관적이라고 느꼈고, 실제로 내가 원하는 것도 구현할 수 있었다. 하지만 unified의 방대한 자료와 서드파티가 끌려서, 이 쪽을 알아보기로 했다.

처음엔 unified를 직접 사용하여 변환하고자 했으나, 서드파티의 버전 문제 등 여러 난관에 봉착했다. 그냥 이렇게 고생하지말고, 적절한 리액트용 라이브러리를 찾는 게 어떨까 싶었다. 그러다 찾은 것이 react-markdown이다. React 기반으로, 아래와 같이 사용할 수 있었다.

TSX

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
export default function Viewer(): ReactNode
{
    return (
        <ReactMarkdown
            className={cn('markdown')}
            data-component='MarkdownViewer'
            rehypePlugins={[[ rehypeKatex, { output: 'mathml' }], rehypeRaw ]}
            remarkPlugins={[ remarkGfm, remarkMath ]}
            components={{
                a: MarkdownA,
                blockquote: MarkdownBlockquote,
                code: handleCode,
                h1: MarkdownHeading,
                h2: MarkdownHeading,
                h3: MarkdownHeading,
                h4: MarkdownHeading,
                h5: MarkdownHeading,
                h6: MarkdownHeading,
                img: MarkdownImg,
                table: MarkdownTable,
                td: MarkdownCell,
                th: MarkdownCell,
                tr: MarkdownTr
            }}
        >
            {text}
        </ReactMarkdown>
    );
}

위와 같이, 각 태그에 대응하는 컴포넌트를 만들어 적용했다. React 컴포넌트라서 블로그의 디자인과 융화하기 쉬웠고, useState와 같은 상태관리를 적용하기도 편했다. 즉, 마크다운의 컨텐츠와 블로그의 직접적인 상호작용이 가능하다. 이러한 장점은 이미지 컴포넌트에서 빛을 발했는데, 이미지 클릭 시 이미지 모달을 띄우도록 구현했다.

무엇보다, 기존의 문자열보다 훨씬 깔끔하고 이쁘다. 블로그의 큰 구멍 중 하나를 완벽하게 막은 것 같아 마음에 든다.

4. UI 변경

2차 개편까지만 해도 Material UI를 사용하고 있었다. 그 테마는 미려한 만큼 꽤나 무거웠는데, 그래서인지 조금만 잘 못 써도 퍼포먼스 저하가 심했다. 거기다 내 처참한 React 실력까지 더해져 심각할 정도의 퍼포먼스를 보여줬다. 그 당시 버전은 4였는데, 지금의 5버전에 비해 성능 이슈가 많았다고 한다.

때문에 3차에선 디자인 시스템이 아니라 모든 컴포넌트를 직접 개발했는데, 처음엔 이쁘다고 생각했지만, 가면 갈수록 어딘가 모르게 촌스럽다는 느낌이 들었다. 여러 사이트, 레퍼런스를 참조하여 블로그의 UI를 개선했다.

또한, 여러번의 토이 프로젝트를 통해 익숙해진 Materia UI를 다시 차용하여, 디자인의 기초를 강화했다.

인기게시글의 변화

게시글 및 카테고리의 변화

좀 더 깔끔하고 심플하게 보이도록 폰트와 레이아웃을 변경했다. 게시글은 그리드 패턴을 활용하여, 한 줄에 더욱 많은 게시글을 확인할 수 있도록 변경했다. 기존의 게시글 카드는 너무 쓸데없이 길었다.

카테고리 또한 선택 시, CSS의 filter를 통해 선택한 카테고리를 충분히 강조할 수 있으면서도 독특한 느낌을 주도록 구현했다.

또한 게시글의 페이지 변경, 카테고리 선택, 키워드 검색 등이 URL 파라미터에 충실히 반영될 수 있도록 상태관리를 구성했다. 이로써 사용자는 URL을 입력하는 것 만으로도 현재 선택한 카테고리, 키워드 등의 정보를 동일하게 볼 수 있다.

또한 사내 업무를 통해 배운 framer-motion을 적극 활용하여 소소한 애니메이션을 구현했다. 게시글 피드 뿐만 아니라, 블로그 상당수의 애니메이션을 구현하는데 많은 도움이 됐다.

눈부신 발전까진 아니더라도, 이전에 비하면 훨씬 세련됐다고 생각한다.

5. Giscus 적용

기존의 댓글 시스템은 Utterances를 사용하고 있었다. GitHub Issue를 댓글처럼 활용하는 방식이였는데, 꽤나 괜찮은 댓글 라이브러리였지만, 아래와 같은 문제가 있었다.

  • 댓글 작성 시, GitHub 계정만을 강제함. 비회원도 불가능
  • React와 친밀하지 않음. 특히 테마 변경 시 별도의 로직이 필요
  • 대댓글 불가능

GitHub Issue는 원래 그러라고 만든 것이 아닌 탓에, 일반적인 댓글 UX와는 다소 차이가 있었다. 혹시 댓글이 필요할 때면, GitHub 아이디를 멘션하는 것으로 댓글을 대신했다.

우연히 찾아보다가 알게 된 서비스로, GitHub에서 이러한 니즈를 충족시키기 위해 Discussions 기능을 추가했다. 이쪽은 말 그대로 댓글을 위한 서비스라 GitHub Issue 보다 훨씬 좋았다.

GitHub 계정이 여전히 강제되지만, 그 외의 문제점은 말끔히 해결됐다. Utterances -> Discussions으로의 마이그레이션도 간단해서, 옮기는 데 그리 많은 공수가 들지도 않았다. 단, 일괄 이전은 안 돼서, 양이 많으면 귀찮을거다.

Giscus의 다양한 기능은 🔗 Giscus 공식문서에서 확인할 수 있다. 설명도 쉽게 작성되어 있으며, Utterances 보다 다양한 기능을 제공해준다.

마치며

개편의 개편을 거듭할수록 블로그가 나아지는 것 같아 좋다. 어느정도 뗌질을 했으니, 당분간은 블로그에 애정을 붙일 수 있지 않을까 기대한다.

게시글도 슬슬 다시 써야한다. 특히 OpenLayers 안내서는 글 보충을 조금 하고 싶다. 가끔 훝어보다보면, 거슬리는 부분이 있기도 하고. 거기다 쓰고 싶은 주제도 생각해놓은 건 많은데, 글 쓰는 게 생각보다 너무 귀찮다...

3차 -> 4차 개편까지의 기간은 약 1년 정도다. 이번 개편은 얼마나 갈지 궁금하다.

🏷️ Related Tag

# React
# Next.js
# Material UI
# Giscus

😍 읽어주셔서 감사합니다!
도움이 되셨다면, 💝공감이나 🗨️댓글을 달아주시는 건 어떤가요?
블로그 운영에 큰 힘이 됩니다!
https://blog.itcode.dev/projects/2023/09/09/4th-renewal