Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

정적 사이트 생성기 Gatsby

최근 웹사이트를 정적사이트로 만들 일이 있어서 어떤 도구를 이용해서 만들면 좋을지 좀 찾아봤다. 여기서 정적사이트(static site)라고 말하는 것은 HTML, CSS, JavaScript로만 만들어진 사이트이다. 이런 용어의 정의가 아주 엄격한 것은 아니지만 보통 정적사이트는 사용하는 문서도 빌드할 때 모두 HTML로 만들고 가장 많이 사용하는 방식이 Markdown으로 글을 쓰고 정적사이트 생성기를 통해서 블로그로 만드는 것이다. 이렇게 사이트를 만들면 HTML, CSS, JavaScript, 이미지 등의 정적 파일만 CDN 등을 통해서 배포하면 별도의 서버를 운영할 필요도 없고 사용자가 많아져서 서버에 부하가 생길 걱정도 할 필요가 없다.(물론 정적 파일을 배포하는 서버 부하는 생각해야 한다.)

최근 흐름에 따라 정적사이트와 비슷한 맥락의 용어가 몇 가지 더 있다. 용어의 경계를 너무 명확히 할 필요는 없고 이마저도 시간이 지나면서 달라지기 마련이지만 내가 이해한 맥락에서 설명을 해보려고 한다. (잘못된 내용이 있으면 댓글로 의견 주시면 감사하겠습니다.)

JAMstack사이트를 보면 다음과 같이 정의되어 있다.

Modern web development architecture based on client-side JavaScript, reusable APIs, and prebuilt Markup

위 설명대로 JavaScript, API, Markup의 앞글자를 딴 스택이다.(CSS를 안 쓴다는 의미는 아니다.) 템플릿은 미리 만들어서 JavaScript와 함께 배포하고 API로 데이터를 불러오는 사이트를 말한다. 이렇게 보면 API를 전혀 안 쓰는(다고는 했지만, Google Analytics 등 일부 API는 쓸 수도 있다.) 정적사이트는 JAMstack이라고 할 수 있지만 모든 JAMstack이 정적사이트는 아니다.(데이터를 API로 불러오므로...)

또 하나는 Single Page Application을 의미하는 SPA라는 형태가 있다. SPA는 위키피디아에 따르면 다음과 같이 정의되어 있다.

a web application or web site that interacts with the user by dynamically rewriting the current page rather than loading entire new pages from a server.

React나 Vue가 인기를 얻으면서 더욱 많이 사용하는 방식인데 기본적으로 어떤 경로로 접속을 하던 항상 똑같은 HTML과 JavaScript를 받는다. 이후 이 JavaScript가 실행되면서 API로 데이터를 불러오고 마크업으로 화면을 보여주고 사용자가 이동하는 것에 맞추어서 Push state로 히스토리를 바꿔주면서 페이지를 보여주지만 실제로는 모두 프론트엔드에서 처리된다. 이런 면에서 보면 SPA도 JAMstack이라고 할 수 있지만 모든 JAMstack이 SPA는 아니다. JAMstack으로 구성했지만, Single Page로 구성하지 않을 수도 있으므로... 물론 현실에서는 SEO나 성능상의 이유로 SPA와 서버사이드 렌더링을 섞어서 쓰기도 한다.

정적 사이트 생성기(Static Site Generator)

설명이 길어졌는데 여기서 내가 만들고자 하는 것은 정적사이트였고 최신 정적사이트 생성기를 살펴봤다. 프론트엔드를 주로 하지 않은지 좀 되었기에 내가 아는 정적사이트 생성기는 Jekyll에서 머물러 있다. 정적사이트의 시대를 만든 것이나 다름없는 대표 프로젝트고 GitHub Pages도 기본은 Jekyll을 사용한다.

물론 Node.js 프로젝트에서 Metalsmith를 쓰고 있고, Node.js 한국 커뮤니티에서는 Hexo를 쓰고 최근 mocha 웹사이트11ty로 갈아탔지만 대부분 사용방법은 크게 다르지 않다. 문서는 Markdown으로 관리하면서 생성기에서 지원하는 몇 가지 문법으로 HTML 템플릿과 연결해서 빌드하면 정적사이트가 만들어진다.

정적 사이트 생성기가 하는 일이 뻔하고 기능의 차이도 크지 않기 때문에 대부분은 사용성이나 언어의 선호도 정도로 골라서 쓰게 되는 편이라고 생각하고 새로운 생성기가 나와도 크게 신경 쓰고 있지 않았다. 말했다시피 각 도구가 하는 일이 도긴개긴이고 러닝 커브도 높지 않아서 미리 찾아보거나 공부해 볼 필요성을 못 느끼고 있었다.

Gatsby란?

기술 트랜드는 어느 정도 보고 있었기에 정적 사이트 관련해서 NetlifyGatsby를 중심으로 인기를 끌고 있다는 것은 어느 정도 알고 있었다. 내 주변에 쓰는 사람이 없어서 정확히 각 도구/서비스의 기능을 모르고 있었다가 얼마 전 Netlify를 써보고는 반해버렸고(netlify로 정적 사이트 배포하기 참고) 이번에는 Gatsby를 써보고 왜 사람들이 계속 Gatsby를 언급했는지 이해하게 되었다.

Gatsby 웹사이트

처음에는 이게 뭔가? 하고 보고 있었는데 동작하는 구조를 이해하고 나서는 최근 몇 년간 정적 사이트의 발전이 너무 커서 놀라울 정도였다. 그동안 정적 사이트에 대한 요구가 없었던 관계로 React, Vue등을 이용한 SPA 쪽에 더 관심이 있었지(실제로 내가 보는 프로젝트도 이쪽이 많았고...) 정적 사이트는 안 보고 있었는데 안 보는 사이에 (내 처지에서) 놀라울 정도의 발전이 있었다. Netlify도 사실상 이런 흐름을 타고 인기를 얻고 있는 거고(실제로도 잘 만들었지만) 정적사이트 생성기를 정리해 놓은 StaticGen도 Netlify에서 운영하고 있다.

Gatsby를 한마디로 정의하기가 어렵다. 앞에서 정적사이트 생성기를 설명하면서 Gatsby까지 왔지만, Gatsby를 단순히 정적 사이트 생성기라고 말하기에는 Gatsby가 아깝다고 생각한다.

Gatsby가 트위터에서 Gatsby를 어떻게 설명하면 좋을지 설문한 것을 보면 Website generatorWebsite development tool가 우세하다. 한쪽으로 치우치지 않고 Gatsby 쪽도 저렇게 애매한 단어로만 설문한 걸 보면 한마디로 정의하기 힘들어하는 것으로 보인다.

Gatsby는 React를 사용하는데 웹사이트를 쉽게 제작할 수 있도록 편의 기능을 많이 제공하고 있다. React를 잘 알지는 못하지만, React의 사용방법을 크게 헤치지 않으면서도 쉽게 웹사이트를 만들 수 있도록 잘 감싸서 제공하고 있다. React를 잘 아는 사람은 어떻게 느낄지 모르겠는데 나한테는 꽤 재밌고 잘 만들어진 도구로 느껴졌고 정적사이트는 정적 사이트 나름의 한계가 있지만 웬만한 사이트는 Gatsby를 먼저 고려해도 괜찮겠다 싶을 정도였다.

Gatsby 동작 방식

위 그림을 Gatsby 홈페이지에서 가져온 그림으로 동작 방식을 설명하고 있다.

블로그뿐만 아니라 웹사이트를 만들려면 데이터가 필요하다. 이 데이터를 가져오는 곳은 Gatsby에서는 데이터소스라고 부르는데 여기서 데이터 소스는 Wordpress같은 CMS 도구가 될 수도 있고 다른 정적 사이트 생성기처럼 Markdown 파일이 될 수도 있고 API 등을 통해서 다른 곳에서 가져올 수도 있다. 플러그인 시스템이 잘 되어 있어서 다양한 데이터소스에서 데이터를 가져올 수 있다.

Gatsby는 기본적으로 GraphQL을 사용해서 데이터소스에서 데이터를 가져온다. 플러그인을 이용해서 RESTful API에서 데이터를 가져올 수도 있지만 GraphQL이 Gatsby에 포함되어 있어서 훨씬 쉽게 쓸 수 있고 원하는 데이터를 선택해서 가져올 수 있는 GraphQL이 Gatsby의 개념에도 더 어울린다고 생각한다.

웹사이트는 React를 이용해서 만든다. 코드는 React를 이용해서 컴포넌트 작성하듯이 사용하지만, 공통 레이아웃을 관리하거나 페이지를 생성하거나 데이터소스와 컴포넌트를 연결하거나 하는 기능을 Gatsby에서 제공하고 있어서 쉽게 만들 수 있다.

이를 빌드하면 정적 사이트로 만들어 준다. 여기서 SPA라고 하지 않고 정적사이트로 만든다는 부분이 중요한데 CMS나 파일 등의 데이터 소스에서 GraphQL로 가져온 데이터를 빌드할 때 모두 가져와서 정적파일의 데이터로 포함한다. SPA라고 한다면 API로 가져오는 로직이 소스에 들어있고 사용자가 SPA 사이트를 실행할 때 API로 가져오게 되지만 Gatsby는 정적 사이트를 만들어주므로 빌드 시에 GraphQL로 데이터를 가져와서 빌드된 배포 파일에 포함 시킨다. 그러므로 사이트를 운영할 때 데이터소스로 이용한 API 서버나 파일은 제공하지 않아도 된다. 이 부분이 다른 도구들과 Gatsby를 가장 다르게 만들어주는 특징이라고 생각한다.

복잡한 사이트를 아직 안 만들어봤지만, 이 전체 흐름이 아주 깔끔하게 만들어져있다. 필요한 API를 이용하거나 정적 사이트에 필요한 기능이나 빌드 과정까지 기존에 없던 새로운 기술은 아니지만 아주 우아하게 잘 조합하고 감싸서 제공하고 있다. 몇 번 테스트로 만져본 정도지만 사용 경험이 꽤 만족스럽다. 자세한 기능은 문서에서 확인할 수 있다.

Gatsby의 동작 예시

(아직 나도 잘 모르는) 자세한 사용법은 다음 기회에 설명하고 간단한 보일러 플레이트 프로젝트로 Gatsby가 어떻게 동작하는지 이해해 보자.

최근 대부분의 JavaScript 프로젝트처럼 사용하기 쉽게 gatsby-cli를 제공하고 있다.

$ npx gatsby-cli new gatsby-example

npx: 211개의 패키지를 15.471초만에 설치했습니다.
info Creating new site from git: https://github.com/gatsbyjs/gatsby-starter-default.git
Cloning into 'gatsby-example'...
remote: Enumerating objects: 1, done.
remote: Counting objects: 100% (1/1), done.
remote: Total 1176 (delta 0), reused 1 (delta 0), pack-reused 1175
Receiving objects: 100% (1176/1176), 2.55 MiB | 2.43 MiB/s, done.
Resolving deltas: 100% (678/678), done.
success Created starter directory layout
info Installing packages...
yarn install v1.10.1
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.

Done in 16.16s.

npx gatsby-cli new gatsby-example 명령어로 기본 웹사이트를 생성했다. 참고로 gatsby-cli를 전역으로 설치(-g)하면 gatsby 명령어를 사용할 수 있지만, 전역 설치를 안 하기 위해 npx를 이용해서 gatsby-cli를 바로 실행했다. 그래서 npx gatsby-cli new gatsby-examplegatsby new gatsby-example 명령어와 같다.(이후에는 그냥 gatsby 명령어를 사용한다.)

gatsby new PROJECT_NAME STARTER_URL 명령어는 새로운 Gatsby 프로젝트를 만드는 명령어다. 위에서는 STARTER_URL을 생략했으므로 기본값인 gatsby-starter-default를 사용해서 사이트가 생성되었다. 커뮤니티가 만든 다양한 구성의 Starter가 있으므로 원하는 구성에 맞춰서 선택하거나 사용방법을 알아보기 좋다.

이제 gatsby-example 폴더에서 npm run develop 명령어(실제로는 gatsby develop)를 실행하면 개발 모드로 서버를 실행할 수 있다.

$ npm run develop

 DONE  Compiled successfully in 3279ms        21:08:13

You can now view gatsby-starter-default in the browser.

  http://localhost:8000/

View GraphiQL, an in-browser IDE, to explore your site's data and schema

  http://localhost:8000/___graphql

Note that the development build is not optimized.
To create a production build, use gatsby build

ℹ 「wdm」:
ℹ 「wdm」: Compiled successfully.

기본 스타터의 디자인은 중요하지 않지만 http://localhost:8000/로 접속하면 아래와 같은 간단한 데모 페이지를 볼 수 있다. 기본 스타터는 2페이지로 구성되어 있다.

Gatsby 기본 스타터 웹페이지

개발 모드에서는 GraphQL을 테스트하기 쉽게 GraphiQL이 포함되어 있다.

GraphiQL 페이지

소스 파일이 있는 src 디렉터리를 보면 React로 작성한 웹사이트를 만든 정적 파일들이 있다. 상세 소스는 GitHub 저장소에서 볼 수 있다.

src
├── components
│   ├── header.js
│   ├── image.js
│   ├── layout.css
│   ├── layout.js
│   └── seo.js
├── images
│   ├── gatsby-astronaut.png
│   └── gatsby-icon.png
└── pages
    ├── 404.js
    ├── index.js
    └── page-2.js

이를 프로덕션용 정적 사이트를 빌드하기 위해 npm run build 명령어(실제로는 gatsby build)를 실행해 보자.

npm run build

> gatsby-starter-default@0.1.0 build /Users/outsider/temp/gatsby/gatsby-example
> gatsby build

success open and validate gatsby-configs — 0.010 s
success load plugins — 0.153 s
success onPreInit — 0.584 s
success delete html and css files from previous builds — 0.009 s
success initialize cache — 0.010 s
success copy gatsby files — 0.045 s
success onPreBootstrap — 0.003 s
success source and transform nodes — 0.040 s
success building schema — 0.173 s
success createPages — 0.000 s
success createPagesStatefully — 0.024 s
success onPreExtractQueries — 0.003 s
success update schema — 0.082 s
success extract queries from components — 0.076 s
success run graphql queries — 0.066 s — 7/7 109.09 queries/second
success write out page data — 0.002 s
success write out redirect data — 0.000 s
⠂ onPostBootstrapdone generating icons for manifest
success onPostBootstrap — 0.104 s

info bootstrap finished - 3.851 s

success Building production JavaScript and CSS bundles — 5.356 s
success Building static HTML for pages — 0.464 s — 4/4 27.16 pages/second
info Done building in 9.673 sec

빌드 결과는 public 디렉터리에 생기는데 위의 src와 비교했을 때 파일도 많아지고 훨씬 복잡해진 것을 알 수 있다.

public/
├── 0-3a2bd5c9a235c2c48f7f.js
├── 0-3a2bd5c9a235c2c48f7f.js.map
├── 404
│   └── index.html
├── 404.html
├── app-d044d5a4358eaf1a0738.js
├── app-d044d5a4358eaf1a0738.js.map
├── chunk-map.json
├── component---src-pages-404-js-1af22cf06cd461fb89b9.js
├── component---src-pages-404-js-1af22cf06cd461fb89b9.js.map
├── component---src-pages-404-js.a1e3226de724a2570055.css
├── component---src-pages-index-js-20cc3b8fc80f26983ef6.js
├── component---src-pages-index-js-20cc3b8fc80f26983ef6.js.map
├── component---src-pages-index-js.a1e3226de724a2570055.css
├── component---src-pages-page-2-js-b55efdbe24802f10f9a8.js
├── component---src-pages-page-2-js-b55efdbe24802f10f9a8.js.map
├── component---src-pages-page-2-js.a1e3226de724a2570055.css
├── icons
│   ├── icon-144x144.png
│   ├── icon-192x192.png
│   ├── icon-256x256.png
│   ├── icon-384x384.png
│   ├── icon-48x48.png
│   ├── icon-512x512.png
│   ├── icon-72x72.png
│   └── icon-96x96.png
├── index.html
├── manifest.webmanifest
├── page-2
│   └── index.html
├── pages-manifest-4666a1fb53ffe6f02316.js
├── pages-manifest-4666a1fb53ffe6f02316.js.map
├── static
│   ├── 6d91c86c0fde632ba4cd01062fd9ccfa
│   │   ├── 360b8
│   │   │   └── gatsby-astronaut.png
│   │   ├── 5c9de
│   │   │   └── gatsby-astronaut.png
│   │   ├── 7e26c
│   │   │   └── gatsby-astronaut.png
│   │   ├── 893cf
│   │   │   └── gatsby-astronaut.png
│   │   ├── a2541
│   │   │   └── gatsby-astronaut.png
│   │   └── fdadc
│   │       └── gatsby-astronaut.png
│   └── d
│       ├── 1025518380.json
│       ├── 164
│       │   └── path---404-html-516-62a-NZuapzHg3X9TaN1iIixfv1W23E.json
│       ├── 173
│       │   └── path---index-6a9-NZuapzHg3X9TaN1iIixfv1W23E.json
│       ├── 2011440971.json
│       ├── 44
│       │   └── path---404-22-d-bce-NZuapzHg3X9TaN1iIixfv1W23E.json
│       ├── 53
│       │   └── path---page-2-fbc-5a8-NZuapzHg3X9TaN1iIixfv1W23E.json
│       ├── 755544856.json
│       └── 920
│           └── path---dev-404-page-5-f-9-fab-NZuapzHg3X9TaN1iIixfv1W23E.json
├── webpack-runtime-586c75e86997621faa61.js
├── webpack-runtime-586c75e86997621faa61.js.map
└── webpack.stats.json

로컬에서 8000 포트로 웹서버를 띄워서 빌드한 파일을 제공한다고 했을 때 이 프로젝트에는 http://localhost:8000/http://localhost:8000/page-2/ 두 개의 페이지가 있다. 아래는 이 두 페이지를 불러온 소스 파일이다.

두 페이지의 소스 보기 비교

작아서 잘 안 보이지만 각 페이지를 직접 URL로 접근하면 완전한 HTML 페이지가 다운로드 된다. 위 빌드 파일에서 index.htmlpage-2/index.html이 다운로드 된 것이다.

정적 사이트를 빌드했으므로 당연한 얘기인데 위처럼 빌드 결과로 많은 파일이 나온 이유는 최적화를 해주기 때문이다. 다음 화면을 보면 루트 페이지를 불러온 다음에 /page-2로 가는 링크 위에 마우스 커서를 올리면 page-2에 필요한 파일을 미리 다운로드 받는데 이 파일도 전체 HTML 파일과 관련 파일이 아니라 필요한 부분만 분리된 파일이 다운로드 된다. 미리 다운로드를 다 했으므로 클릭했을 때는 당연히 아무 파일도 다운로드하지 않고 바로 /page-2를 불러온다.

사용자 삽입 이미지

개인적으로 이렇게 특별히 신경 쓰지 않아도 쉽게 최적화해주는 부분이 Gatsby의 매력이라고 생각한다. 플러그인이 많이 존재하기 때문에 필요한 기능을 추가하면 쉽게 오프라인을 지원하거나 PWA로 만드는 등의 작업을 쉽게 할 수 있다.

최근에 Vue.js를 사용해 보고 나서 Vue.js에 끌리고 있었는데 Gatsby 때문에 React를 해야겠다고 생각하는 중이다.

2019/02/08 22:35 2019/02/08 22:35