menulogo

React 기반의 Gatsby 프레임워크에서 Nextjs로 블로그 마이그레이션 하기

@corinthioniaApril 18, 2025

저는 2021년부터 2023년까지 React 기반의 정적 사이트 생성(SSG) 프레임워크인 Gatsby를 이용하여 블로그를 개발했습니다. 그러다가 작년 초에 Nextjs를 이용하여 블로그를 다시 개발했고, 현재까지 안정적으로 운영해 오고 있습니다. 이번 글에서는 마이그레이션을 결심하게 된 계기와 그 과정에 대해 간략히 소개해 보려 합니다.


블로그를 직접 개발하게 된 계기

저는 2021년부터 프론트엔드 분야를 공부해 오기 시작했는데요. 이전까지는 전공 수업을 들으면서 그 내용을 네이버 블로그, 노션 등에 정리하곤 했습니다. 그런데 네이버 블로그는 코드 편집기가 불편하게 되어 있어 개발 블로그를 운영하기에는 적절하지 않은 플랫폼이라 생각했고, 노션은 퍼블릭 블로그보다는 개인 공부 기록용으로 적합하다 생각했습니다.

프론트엔드를 본격적으로 공부하기 시작하면서부터는 티스토리, 벨로그 등을 이용해 보긴 했지만 디자인이나 기능을 제 마음대로 커스텀 하기에는 한계가 있었습니다.

다른 블로그 플랫폼 이용 내역

다른 플랫폼에 작성된 글을 모두 합치면 300개는 족히 넘을 것 같네요 🙃 그렇지만 대부분이 공부 기록용 글이었기에 비공개로 작성되었고, 공개된 블로그에는 오로지 제 자신만의 경험이 담긴 글들로 채우고 싶었습니다.

또한 개발자에게도 셀프 브랜딩이 중요하다는 생각이 들었고, 이를 위한 출발점으로 개인 개발 블로그를 직접 구축하고 운영하기로 결심했습니다!


Jekyll로 시작하기

2021년에 블로그 개발에 많이 사용되던 프레임워크로는 Jekyll과 Gatsby가 있었습니다. 당시에는 Gatsby보다 Jekyll의 사용률이 더 높았고, 템플릿의 개수도 훨씬 많았기 때문에 Jekyll의 한 템플릿을 이용하여 블로그를 개발했습니다.

하지만 Jekyll은 루비(Ruby) 기반의 정적 사이트 생성기였기에, 루비 언어를 모르던 개발 초보(me)에게는 유지 보수의 어려움이 있었습니다. 그러다 React 기반의 Gatsby 프레임워크를 알게 되었고, 유지보수 및 개발 편의성을 고려하여 Gatsby로 블로그를 이전했습니다.

Gatsby 블로그 템플릿을 이용하기

Gatsby는 React 기반의 정적 사이트 생성기(Static Site Generator)입니다. 블로그처럼 정적 콘텐츠 중심의 웹사이트를 만들 때는 SSG 프레임워크가 자주 활용되는데, 빌드 시점에 모든 페이지를 미리 렌더링하여 정적 파일로 생성하는 방식입니다. 이러한 방식은 페이지 로딩 속도를 빠르게 하고, 별도의 서버 연산 없이도 콘텐츠를 바로 전달할 수 있어 사용자 경험과 SEO 측면에서 유리하다는 장점이 있습니다.

당시에는 처음부터 디자인과 개발을 모두 하기보다는 템플릿을 이용하여 빠르게 블로그를 구축하고 싶어서, gatsby-starter-bee 템플릿을 이용하여 블로그를 개발했습니다. 사용해 보니 디자인도 매우 마음에 들었고, 코드로 커스텀할 수 있다는 점이 편리했습니다. 지금은 아카이브 되었지만 아직까지도 종종 해당 템플릿을 사용하는 블로그를 찾아볼 수 있습니다 👀

그런데 운영하는 과정에서 수많은 에러를 마주하고 결국 처음부터 끝까지 직접 만들기로 결심하게 되었는데요, 그 중 몇 가지 에러를 소개해 드리려 합니다.

1) 빌드 시 Python 버전 에러

템플릿을 이용하여 블로그를 빌드할 때마다 원인을 알 수 없는 에러가 발생했습니다. 아래 이미지는 그 당시 발생했던 에러 중 하나인데, 뜬금없이 Python 버전 관련 에러가 발생하였고 해당 문제를 해결하기 위해 며칠을 헤매야 했습니다.

Gatsby Python 에러

결과적으로는 파이썬을 삭제했더니 정상 동작했습니다. 너무 오래 전 에러라 정확한 문제 원인이 무엇인지 확신은 못하겠지만, node-gyp가 네이티브 모듈(Node.js에서 사용하는 C/C++로 작성된 모듈)을 빌드하려고 할 때 Python 환경이 제대로 설정되어 있지 않아서 발생하는 문제라고 합니다.

파이썬 삭제로 문제가 해결된 것을 보아 node-gyp가 사용하는 파이썬의 버전과 실제로 설치된 버전이 호환되지 않던 게 아니었을까 추측해 봅니다.

2) 실행 시 무한 새로 고침 현상

npm start 명령어를 통해 프로젝트를 실행할 때 페이지가 무한 reload 되는 현상이 발생했습니다.

무한 새로고침 현상

Gatsby github에서 관련된 이슈를 찾아보니 저와 비슷한 현상을 겪는 분들이 많았습니다. 크롬 브라우저에서 dev mode로 프로젝트를 실행할 때 무한 새로고침 현상이 발생한다는 것이었습니다.

위 문제는 Infinite reload spinning 이슈를 보고 해결할 수 있었습니다. 윈도우 환경에서 발생하는 문제였던 듯한데, 포트 충돌 문제가 생기면 그냥 포트를 바꾸는 게 속편한 해결 방법이라고 말합니다. 🫠

Whoever read this it's because Windows does something to port and cannot use the same port for whatever reason, not worth to investigate. Just change gatsby develop port to something else

실제로 다른 포트로 변경 후 문제가 해결되었습니다.

3) Node sass 환경 에러

이 당시 Window 환경에서 Mac OS로 노트북을 변경했는데, 개발 환경이 변경되니 또 다른 문제가 발생했습니다. gatsby-starter-bee 템플릿에서 node-sass 라이브러리를 사용 중이었고, 저의 개발 환경은 M1 Mac이었기 때문에 발생했던 에러였습니다.

Error: Node Sass does not yet support your current environment: OS X Unsupported architecture (arm64) with Unsupported runtime (93)

현재는 node-sass가 deprecated되어 sass로 변경하면 정상적으로 사용할 수 있습니다.



이 외에도 수많은 에러를 마주하다 보니 템플릿 개발자와의 개발 환경이 달라서 발생하는 문제일 수도 있겠다는 생각이 들었습니다. 🤔 제가 직접 블로그를 만든다면 문제가 발생했을 때 그 원인을 좀 더 쉽고 빠르게 찾을 수 있을 것이라 생각했고, 따라서 Gatsby 기반의 블로그를 직접 개발해 보기로 결심했습니다.


Gatsby 블로그를 직접 만들어 보기

github.com/corinthionia/gatsby-blog

당시 'Gatsby 블로그'를 검색하면 대부분이 템플릿을 이용하는 방법에 대해서만 주로 다루었고 직접 만드는 과정에 대한 글을 찾아보기 어려웠습니다. 특히 Gatsby를 이용하려면 GraphQL을 이용하여 블로그 글을 관리해야 하는데, 이 부분에 대해서는 많은 자료가 없어 더욱 어렵게 느껴졌습니다. (GPT도 없었던 시절이라... 😇)

그래도 공식 문서가 설명이 잘되어 있어 공식 문서와 깃허브 이슈를 살펴보며 블로그 개발을 완성할 수 있었습니다. 하지만 블로그를 운영하면서 여러 문제를 겪었고 결국 Nextjs로 마이그레이션을 결심하게 되었는데, 이제부터 그 이유에 대해 설명 드리겠습니다.

1. 높은 플러그인 의존성

Gatsby Plugin Library

Gatsby는 기능 확장을 위해 다양한 gatsby-plugin-* 패키지에 의존하는 구조를 가지고 있습니다. 블로그를 개발하면서 플러그인 간의 의존성 충돌이나 버전 호환 문제를 직접 경험하면서 상당한 불편함을 느꼈습니다.

실제로 개발 당시 react-helmet을 사용해 Google Search Console 트래킹 코드를 <head>에 삽입했는데, 사이트 소유자 확인이 제대로 되지 않는 문제가 발생했습니다. 개발자 도구를 통해 메타 태그가 지정된 위치에 정상적으로 삽입되어 있는 것도 확인했지만 며칠 동안 원인을 찾지 못해 헤맸던 기억이 있습니다. 알고 보니 gatsby-plugin-react-helmet 플러그인을 별도로 설치하지 않았던 것이 원인이었고, 플러그인 하나를 추가하는 것만으로 문제가 해결되었을 때에는 약간의 허무한 감정이 느껴지기도 했습니다.

또한, 운영 도중 Gatsby가 4.x에서 5.x로 메이저 버전이 업데이트되면서 기존에 사용하던 일부 플러그인과 호환되지 않는 경우가 발생하여 마이그레이션 과정에서도 많은 어려움을 겪었습니다.

이러한 경험을 통해 Gatsby의 높은 플러그인 의존성이 유지 보수성과 개발 효율성을 떨어뜨릴 수 있다는 한계를 체감하게 되었습니다. 실제로 제가 개발했던 블로그의 package.json 파일을 살펴보면 일반 패키지 의존성보다 gatsby-plugin-* 의존성이 더 많다는 것을 확인할 수 있는데요. 플러그인이 많아질수록 각각의 버전 호환성을 관리해야 하는 부담이 커지고, 만약 일부 플러그인의 지원이 중단된다면 전체 프로젝트의 안정성에도 영향을 줄 수 있다는 점이 우려되기도 했습니다.

  "dependencies": {
    "@babel/core": "^7.21.4",
    "@babel/eslint-parser": "^7.21.3",
    "@emotion/react": "^11.10.4",
    "@emotion/styled": "^11.10.4",
    "@mdx-js/react": "^2.1.3",
    "dotenv": "^16.0.3",
    "gatsby": "^5.7.0",
    "gatsby-plugin-canonical-urls": "^5.7.0",
    "gatsby-plugin-emotion": "^8.7.0",
    "gatsby-plugin-google-fonts": "^1.0.1",
    "gatsby-plugin-google-gtag": "^5.7.0",
    "gatsby-plugin-image": "^3.13.1",
    "gatsby-plugin-mdx": "^5.7.0",
    "gatsby-plugin-preload-fonts": "^4.7.0",
    "gatsby-plugin-sharp": "^5.13.1",
    "gatsby-plugin-sitemap": "^6.13.1",
    "gatsby-plugin-typescript": "^5.7.0",
    "gatsby-remark-copy-linked-files": "^6.7.0",
    "gatsby-remark-emojis": "^0.4.3",
    "gatsby-remark-external-links": "^0.0.4",
    "gatsby-remark-images": "^7.7.0",
    "gatsby-remark-prismjs": "^7.7.0",
    "gatsby-remark-smartypants": "^6.7.0",
    "gatsby-source-filesystem": "^5.13.1",
    "gatsby-transformer-remark": "^6.7.0",
    "gatsby-transformer-sharp": "^5.13.1",
    "lodash": "^4.17.21",
    "prism-themes": "PrismJS/prism-themes",
    "prismjs": "^1.29.0",
    "query-string": "^7.1.1",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-helmet": "^6.1.0"
  },

2. GraphQL 기반의 데이터 레이어

Gatsby는 정적 사이트 생성 시 데이터를 가져오기 위해 GraphQL을 중심으로 한 데이터 레이어를 사용합니다. 실제로 공식 문서에서도 GraphQL의 개념과 사용법을 소개하며, 이를 적극 활용할 것을 권장하고 있습니다. (Gatsby - GraphQL Data Layer)

Gatsby는 다양한 데이터 소스를 자체적으로 통합해 GraphQL 스키마로 변환하고, 이를 통해 정적인 페이지를 생성합니다. 이 방식은 다양한 소스 간의 데이터를 일관되게 다룰 수 있다는 장점이 있지만, GraphQL의 쿼리 구조나 문법, 스키마 설계 방식까지 새롭게 익혀야 했기 때문에 개발 진입 장벽이 높게 느껴졌습니다.

예를 들어, 블로그 메인 화면에서 ‘고정된 포스트’만 필터링하는 기능을 구현하기 위해서는 아래와 같은 긴 쿼리를 작성해야 했습니다.

export const getPinnedPostList = graphql`
  query getPinnedPostList {
    site {
      siteMetadata {
        title
        description
        siteUrl
      }
    }
    allMarkdownRemark(
      sort: [{ frontmatter: { date: DESC } }, { frontmatter: { title: ASC } }]
      filter: {
        frontmatter: { categories: { ne: null }, draft: { eq: false }, pinned: { eq: true } }
      }
    ) {
      edges {
        node {
          id
          fields {
            slug
          }
          frontmatter {
            title
            summary
            date(formatString: "MMMM DD, YYYY")
            categories
            thumbnail {
              childImageSharp {
                gatsbyImageData
              }
            }
            draft
          }
        }
      }
    }
    file(name: { eq: "profile-image" }) {
      childImageSharp {
        gatsbyImageData(width: 120, height: 120)
      }
      publicURL
    }
  }
`;

처음에는 이 쿼리의 구조를 이해하고 수정하는 것조차 쉽지 않았고, 단순히 데이터를 가져오기 위해 GraphQL이라는 별도의 기술 스택을 학습해야 했다는 점이 부담스럽게 느껴졌습니다. GraphQL의 스키마 구조나 마크다운 데이터를 가져오는 방식, 그리고 컴포넌트와 쿼리를 연결하는 패턴까지 익숙해지기까지 시간이 꽤 걸렸던 것 같습니다 😅

반면, Nextjs로 마이그레이션한 이후에는 익숙한 JavaScript 코드만으로도 데이터를 가져오고 필터링하는 작업이 훨씬 직관적이고 간단해졌습니다. Nextjs에서는 별도의 데이터 레이어 없이 파일 시스템에 접근하거나, fetch API 또는 직접 작성한 함수만으로도 필요한 데이터를 가져올 수 있기 때문에, 학습 부담이 적고 더 빠르게 개발할 수 있었습니다.

export async function getAllPosts(contentsPath: string): Promise<PostType[]> {
  const paths: string[] = sync(`${getContentsDir(contentsPath)}/**/*.md*`);

  return paths
    .reduce((acc: any, path: string) => {
      const post = getPostInfo(contentsPath, path);
      return [...acc, post];
    }, [])
    .sort((a: PostType, b: PostType) => sortPostByDate(a, b));
}

// 고정된 포스트 가져오기
export async function getPinnedPostList(): Promise<PostType[]> {
  const posts = await getAllPosts(CONTENTS_PATH.POST_PATH);
  return posts.filter((post: any) => post.frontMatter.pinned);
}

결과적으로 단순한 블로그 구조에서 GraphQL은 학습에 필요한 시간 투자 정도에 비해 실질적인 효용은 크지 않다고 생각했고, 오히려 익숙한 방식으로 유연하게 구현할 수 있도록 Nextjs를 사용한다면 더욱 빠르게 개발할 수 있을 뿐만 아니라 유지 보수성도 높아질 것이라 생각했습니다.

3. 커뮤니티의 크기

Nextjs는 Vercel을 중심으로 빠르게 성장하고 있는 프레임워크로, 사용률이 꾸준히 증가하고 있으며 활발한 커뮤니티를 바탕으로 안정적인 개발 생태계를 유지하고 있습니다. 덕분에 코드 레퍼런스를 찾기 쉬울 뿐만 아니라, 문제가 발생했을 때 관련된 이슈와 해결 방안을 비교적 쉽고 빠르게 찾아볼 수 있었습니다. 또한 새로운 기능이나 업데이트도 지속적으로 진행되고 있어서 아직까지는 deprecated 되는 것에 대한 우려는 없습니다.

반면 Gatsby는 상대적으로 생태계의 성장세가 정체되어 있는 느낌을 받았습니다. 실제로 커뮤니티의 활발함이나 플러그인 생태계, 관련 레퍼런스의 업데이트 속도 등에서 차이를 체감할 수 있었습니다.

npm trends

이처럼 프론트엔드 생태계가 점차 Nextjs 중심으로 넘어가는 흐름 속에서, 장기적인 관점에서 유지보수성과 확장성을 고려한다면 Nextjs로 마이그레이션 하는 것이 유리하겠다고 생각했습니다.



Nextjs로 마이그레이션 하기

github.com/corinthionia/corinthionia.github.io

Gatsby를 이용하여 블로그를 직접 만들었을 때에는 피그마로 간단히 디자인을 마친 후 개발에 들어갔는데, 혼자 진행하는 프로젝트였기에 그 과정이 상당히 비효율적이라는 것을 깨달았습니다. 따라서 이번에는 디자인을 먼저 진행하지 않고 기능을 구현한 다음 손이 가는 대로 디자인을 입혀 보았습니다. 결과적으로는 꽤나 만족스러운 블로그가 되었네요 😁

1. 블로그 글 관리하기

블로그를 직접 개발하는 경우에는 블로그 글을 Markdown 파일로 작성하여 관리하는 것이 일반적입니다. Nextjs 공식 문서에도 마크다운 파일을 이용하여 페이지를 구성하는 방법에 대해 아주 자세히 다룹니다.

공식 문서에서는 MDX를 렌더링 하는 방법으로 크게 2가지를 소개하고 있습니다. 먼저 File based routing 방법을 사용한다면, /app 디렉토리 하위에 저장된 마크다운 파일들은 모두 페이지로 변환됩니다. 하지만 저는 임시 저장 글이나, 비공개 글들도 있었기에 해당 방법을 사용하지 않았습니다. 대신 Dynamic imports 방식을 사용하여 별도의 디렉토리에 마크다운 파일을 저장하고, 필요한 파일을 동적으로 불러오는 방식을 사용했습니다.


이 과정에서 globgray-matter 라이브러리를 이용하여 마크다운 파일을 불러오고, frontmatter를 파싱하여 블로그 글을 관리하는 방법을 사용했습니다.

예를 들어, 아래의 getMatchedPostContents는 특정 경로에 해당하는 마크다운 파일의 정보를 가져오고, frontmatter를 파싱하여 가공한 다음 포스트 데이터를 리턴합니다. frontmatter란 마크다운 파일에서 사용되는 메타데이터의 블록으로, 포스트의 제목, 발행일, 카테고리 등의 데이터를 포함합니다.

const getMatchedPostContents = (base: string, path: string): PostType | undefined => {
  const file = fs.readFileSync(path, { encoding: 'utf8' });
  const { data, content } = matter(file);

  const slug = path.slice(path.indexOf(base) + base.length + 1).replace(/\.mdx?/g, '');
  if (data.draft) return;

  return {
    frontMatter: {
      ...data,
    },
    content,
    fields: {
      slug,
    },
    path,
  };
};

블로그 글을 개별적인 URL로 접근할 수 있도록 하기 위해서는 동적 라우팅(Dynamic Routing) 방식을 사용했습니다. 이때 각 포스트 페이지를 빌드 타임에 미리 생성 하기 위해 Nextjs의 generateStaticParams를 사용하여 구현했습니다.

The generateStaticParams function can be used in combination with dynamic route segments to statically generate routes at build time instead of on-demand at request time.

export async function generateStaticParams() {
  const posts: PostType[] = await getAllPosts(POST_PATH);

  if (!posts || posts.length === 0) {
    return [{ slug: 'not-found' }];
  }

  return posts.map(post => ({
    slug: post.fields.slug.split('/'),
  }));
}

위 코드는 마크다운 파일에서 포스트 데이터를 불러온 후, 각 포스트의 slug를 기준으로 정적 경로 목록을 생성합니다. 이를 통해 Nextjs는 /posts/hello-world와 같은 URL에 해당하는 HTML 파일을 빌드 시점에 미리 만들어 두게 됩니다.

2. 블로그 기능 추가하기

추가적으로 블로그 기능을 구현하거나 마크다운을 편리하게 사용하기 위한 다양한 라이브러리가 존재하는데, 제가 사용한 라이브러리 몇 가지를 소개해 드리겠습니다.


  • 마크다운 문법을 편하게 사용하기 위한 플러그인

    • remark-breaks - 엔터 입력으로 줄바꿈이 가능합니다.
    • remark-gfm - GFM(GitHub Flavored Markdown) 문법을 사용합니다. 기존의 마크다운 문법을 확장하여 표, 코드 블록, 링크 등을 쉽게 작성할 수 있습니다.
  • 목차(TOC) 기능을 구현하기 위한 플러그인

  • 마크다운을 예쁘게 꾸미기 위한 플러그인

    • rehype-code-titles 코드 블록에 제목을 부여합니다.
    • remark-prism 코드 블록에 문법 하이라이팅을 적용합니다.
    • prism-themes 코드 블록에 다양한 테마를 적용할 수 있습니다.

3. 정적 사이트로 배포하기

블로그는 주로 변경이 적은 정적인 콘텐츠를 다루기 때문에, 정적 사이트 생성(SSG) 방식으로 배포하는 것이 일반적입니다. Nextjs는 SSG를 기본적으로 지원하고, 정적 페이지를 빌드 타임에 미리 생성해 두는 방식으로 페이지 로딩 속도가 빠를 뿐만 아니라 SEO에도 유리하다는 장점이 있습니다. 👍🏻

저는 빌드한 정적 파일을 GitHub Pages를 통해 배포하고 있습니다. 덕분에 별도의 서버 없이도 안정적이고 비용 면에서도 효율적으로 블로그를 운영할 수 있게 되었습니다.


정적 배포와 관련한 내용은 별도의 포스트에서 다루었으니 궁금하신 분들은 한번 참고해 주세요 👀
Nextjs 프로젝트를 정적 페이지로 배포하기



Reference

← 이전 글LG 유플러스 SW 교육 과정 유레카 1기 프론트엔드 대면 과정 회고