새해가 밝은게 엊그제 같은데 벌써 두 달이라는 시간이 흘렀네요. 평소답지 않게 두 달이나 블로그에 글을 쓰지 않고 자리를 비웠는데요, 사실 그 동안 적당히 빈둥대며 놀았다는 걸 부정할 순 없지만(…) 그동안 숙원 사업이었던 블로그 마이그레이션 작업도 조금씩 진행하고 있었답니다.
제가 5년 전 블로그에 처음으로 작성한 글을 보면 아시겠지만, 저는 여태껏 Github Page와 Jekyll을 이용해 블로그를 운영하고 있었습니다. 하지만 그 시간동안 개발 환경과 트렌드도 많이 변했고, 제가 선호하는 기술 스택에도 변화가 있었죠.
그래서 나중에 기회가 된다면 한 번쯤은 새로운 기술을 이용해 블로그 마이그레이션을 진행해보려 했습니다. 하지만 잘 돌아가고 있는 블로그를 바닥부터 새로 만든다는 게 엄두가 안 나기도 했고, 일정이 그렇게 여유롭지도 않았기 때문에… 지난 몇 년동안 상상 속에서 머물러 있는 프로젝트가 되고 있었습니다.
그러다가 올해 상반기, 간만에 여유로운 시간이 생겼습니다. 그래서 틈틈이 Gatsby를 이용해 다시 바닥부터 새 블로그를 만들기 시작했죠. 그렇게 시간이 흘러 결과물이 어느정도 나온 덕에 오늘의 포스트를 작성하고 있습니다. 이번 포스트는 아마 Jekyll 환경에서 작성하는 마지막 글이 될 것 같군요.
이번 포스트를 통해 Gatsby를 사용해 블로그를 구축하시려는 분들께 많은 도움이 되기를 바랍니다.
왜 Jekyll을 떠나려 하는가?
Jekyll은 Ruby로 작성된 정적 사이트 생성기(SSG, Static Site Generator) 입니다. Github Page에서 별도 설정 없이 기본적으로 지원하기 때문에, Jekyll 테마 템플릿을 리포지터리에 올리기만 해도 웹 페이지를 호스팅할 수 있습니다. 오래 전에 나온만큼 템플릿도 쉽게 구할 수 있고, Github의 지원 덕분에 아직까지도 전세계적으로 가장 많이 사용되는 정적 사이트 생성기 중 하나입니다.
하지만 몇 년동안 사용하다 보니 일부 불편함이 느껴진 것도 사실입니다. 우선 저는 프론트엔드 개발을 주로 해왔기 때문에 Ruby 생태계에 익숙한 편이 아닙니다. Jekyll의 템플릿 엔진과 내부 데이터 쿼리는 Liquid라는 언어를 사용하는데, 이것도 그렇게 친절한(?) 언어가 아니라서 썩 맘에 들지 않았습니다.
이로 인해 Node.js 환경에서 사용할 수 있는 라이브러리를 사용하기가 쉽지 않았고, 제가 알고 있는 방식의 성능 최적화 기법을 적용하기 어려웠습니다. Gulp나 Grunt 등의 태스크 러너를 이용하면 이런 불편함을 해결할 수 있었지만, 애초에 저물어가는 기술을 굳이 공부해가며 적용할 이유까지는 없었습니다.
마지막은 커뮤니티 활성화 정도입니다. Jekyll은 공식 문서도 나름 잘 작성된 편이고 사용자도 많다고는 합니다만… 애초에 Ruby가 인기 있는 언어가 아니라 그런지, Jekyll 라이브러리도 업데이트가 잘 안되는 것 같았구요. 주변에서도 Jekyll을 딥하게 쓰는 분도 찾기가 어려웠습니다.
그래도 블로그를 완전히 갈아엎을 만큼의 불편함까지는 아니었기에, 여태껏 필요한 부분만 고쳐가며 운영을 해오고 있었죠. 하지만 블로그 마이그레이션을 결심한 지금은 이야기가 다릅니다. Jekyll보다 더 나은 여러 선택지가 있었고, 저는 큰 고민없이 Gatsby를 그 대상으로 선택하게 됩니다.
왜 Gatsby를 사용하려 하는가?
Gatsby는 React와 GraphQL 기반으로 동작하는 정적 사이트 생성기입니다. 그렇다면 저는 왜 Jekyll에서 Gatsby로의 마이그레이션을 선택했을까요?
아무래도 가장 큰 특징은 JavaScript 기반의 프레임워크 라는 점입니다. 따라서 JavaScript에 능숙하다면 Gatsby에 적응하는 것은 어려운 일이 아닐 것입니다. 블로그 환경을 구성하는 문법에 익숙하다는 것만으로도 진입 장벽이 어느 정도 낮아지니까요.
Gatsby는 화면에 렌더되는 UI는 React, 데이터 쿼리는 GraphQL, 개발 및 빌드 과정에서 필요한 모든 설정은 Node.js를 통해 동작합니다. React를 이용하면 컴포넌트 기반의 UI를 구축할 수 있고, GraphQL을 이용하면 클라이언트에서 필요한 데이터만 쿼리할 수 있다는 특징은 이미 널리 알려진 사실이죠. 게다가 JavaScript를 쓸 수 있다면 TypeScript 적용도 가능하기 때문에, 신뢰성 있는 애플리케이션 개발 환경 구축에도 도움이 됩니다.
서드파티 라이브러리 적용 및 성능 최적화와 관련된 커뮤니티의 활동도 활발합니다. Webpack과 React에서 사용되는 라이브러리를 Gatsby에도 적용할 수 있고, 이를 보다 편리하게 쓰기 위한 플러그인 개발도 꽤 활발하게 이루어지고 있었습니다. 이 덕분인지 국내에서도 Gatsby를 이용해 빌드한 정적 사이트를 어렵지 않게 만나 볼 수 있습니다.
제가 Gatsby를 선택한 이유로는 React를 이용한 컴포넌트 개발 환경 구성이 가장 컸고, 이 외에도 풍부한 플러그인 지원, 프리로드(preload) 등 자체적으로 성능 개선을 위한 다양한 방법을 제공하는 것이 선택에 도움을 줬습니다.
호락호락하지 않다
과연 생각한 것처럼 잘 될 수 있을까?
아무튼 위에서 말한 Gatsby의 장점들을 한 번 경험해보고자 프로토타이핑을 해보기로 결정했습니다. 하지만 처음 겪어보는 기술 스택을 쓰다보니 오랜 시간에 걸쳐 삽질을 많이 했는데요, 지금부터 이에 대해 하나씩 정리해봅시다. 이제 본격적으로 기술 이야기가 나올 것 같군요…!
기획과 디자인의 불명확
첫 번째로 마주친 문제는 기획과 디자인의 불명확이었습니다. 새 기술 스택으로 프로토타이핑을 하는 건 좋은데, 어떤 기능을 어떻게 구현할 것인지 가 마음 속에서 명확하게 정해지지 않았습니다.
기존 블로그 형태를 어느정도 유지하긴 하지만, 기존 블로그에서 부족했던 부분을 개선하는 작업도 함께 고려하지 않을 수 없었습니다.
- 어떤 기능을 유지할 것인지
- 새로 추가할 기능은 무엇인지
- 구현하고자 하는 기능은 어떤 기술을 써야 구현 가능한지
- 글꼴, 사이즈, 색상, 여백을 어떤 기준으로 나눌 것인지
- 블로그에서 전역적으로 사용할 컬러 팔레트는 어떻게 구성할 것인지
여기에서 사실 꽤 많은 고민을 했습니다만, 이 문제에 대한 해결은 결국 최대한 많은 레퍼런스를 찾아보는 것밖에 없었습니다.
우선 내가 필요하다고 생각하는 기능과 디자인을 아이패드에 스케치로 정리했습니다. 그 후 다른 개발자, 다른 회사에서 운영하는 블로그를 최대한 많이 리서치했습니다. 저는 특히 다음과 같은 곳에서 영감을 많이 얻었습니다.
하지만 그럼에도 불구하고 처음부터 완벽한 기획을 만드는 것은 굉장히 어려웠습니다. 그래서 저는 대충 큰 틀을 먼저 잡은 후, 일단 개발을 진행해가며 세부 스펙을 완성해나가는 방식을 선택했습니다.
프레임워크에 대한 이해 부족
공식 문서가 나름 잘 작성된 편이니 이를 참고하도록 하자
그래서 일단은 러프한 기획을 바탕으로 개발을 시작하게 됐습니다. 다행스럽게도 Gatsby에서는 블로그 관련 필수 플러그인이 미리 설치된 스타터(gatsby-starter-blog)를 제공해주고 있었기 때문에 이를 이용해 초기 개발 환경을 구성했습니다.
개발 환경을 로컬에 구축하는 것은 어렵지 않았지만, Gatsby의 설정을 건드리면서 제가 원하는 결과물이 나오도록 만드는 과정은 꽤 어려웠습니다. 아무래도 처음 써보는 프레임워크다보니… 물론 공식 문서에 설명이 잘 되어 있긴 했지만, 그래도 잘 모르는 부분이 생겼습니다.
가령 Gatsby에는 gatsby-node.js
, gatsby-config.js
, gatsby-ssr.js
와 같은 설정 파일을 통해 빌드 관련한 옵션을 설정할 수 있는데, 내부 API가 어떤 원리로 동작하는지를 모르고 구현을 시작한 탓에 삽질을 많이 했습니다. 그래서 이 부분은 다른 Gatsby 블로그 코드를 뜯어보면서 아이디어를 얻었죠.
GraphQL 쿼리 결과를 쉽게 볼 수 있는 도구가 있으니 이걸 쓰자
GraphQL도 사실 처음 써봐서 처음에 좀 많이 헤맸습니다. Gatsby에서는 마크다운, 이미지 파일, 메타데이터 등 내부 데이터를 불러오기 위해 GraphQL을 쓰는데… 문법도 헷갈리는 마당에 어떤 이름의 데이터가 무엇을 가리키는지를 모르겠어서 심히 당황했습니다.
그러다가 Gatsby에서 GraphiQL이라는 도구가 내장되어 있길래, 이를 이용해 쿼리 작성을 조금씩 연습해봤습니다. 가령 마크다운 파일의 프론트매터(frontmatter)를 화면에 렌더해보고, 이미지 파일을 불러와 배경에 적용하고, 사이트 메타데이터를 SEO에 적용해보면서 사용법을 익혔습니다.
하지만 이것은 단순히 GraphQL 문법을 다루는 연습이었을 뿐, Gatsby가 내부적으로 GraphQL을 처리하는 방법도 따로 이해해야 했습니다. Gatsby에서는 크게 정적 쿼리(Static Query)와 페이지 쿼리(Page Query)라는 이름으로 쿼리 종류를 구별하기 때문입니다. 선언 위치, 파라미터 적용 여부처럼 각자 특징이 있는데, 어떤 쿼리를 어떤 식으로 사용해야 하는지 익히는데도 좀 시간이 걸렸습니다.
뭐 비슷한 이야기긴 하지만… 결론적으로 프레임워크 사용법을 이해하기 위해서는 물리적으로 많은 시간을 투자해서 삽질하는 게 최고의 공부 방법인 듯 합니다.
호환성 문제
새로운 버전을 릴리스할 때는 이전 버전을 호환해야만 합니다. 그것이… 약속이니까.
프레임워크를 변경하면서 내부적으로 포스트와 이미지를 관리하는 디렉토리 구조, 그리고 빌드 결과물로 생성되는 URL이 변경되었습니다. 하지만 URL은 섣부르게 변경하면 안 되는데, 외부 웹 사이트에서 기존 URL로 링크가 걸려있는 게시물에 접근할 수 없기 때문입니다.
이는 블로그 유입에 있어서 큰 문제가 될 뿐만 아니라 SEO 점수에도 영향을 끼칠 수 있기 때문에 반드시 신경을 써주어야 합니다. 따라서 이전 버전 URL을 호환해줄 수 있도록 Gatsby에서 별도의 설정이 필요했는데, 이 과정에 대해 알아봅시다.
우선 제가 쓰고 있던 Jekyll 테마의 디렉토리 구조와 빌드 결과물을 봅시다.
# Jekyll
# 디렉토리 구조
_posts
├── YYYY-MM-DD-포스트1.md
├── YYYY-MM-DD-포스트2.md
└── YYYY-MM-DD-포스트3.md
# 빌드 후 URL
/YYYY/MM/DD/포스트1.html
/YYYY/MM/DD/포스트2.html
/YYYY/MM/DD/포스트3.html
Jekyll에서는 마크다운 파일 이름을 YYYY-MM-DD-제목
포맷으로 작성하여야 하는 규칙이 있습니다. 그러면 하이픈에 따라 구분된 날짜가 연/월/일 디렉토리로 생성되고, 그 안에 HTML 파일이 생성되는 형태입니다. 따라서 사용자는 /YYYY/MM/DD/포스트1.html
의 경로로 접근할 수 있습니다.
한편, Gatsby에서 제가 구성하고자 하는 디렉토리 구조와 빌드 결과물은 다음과 같았습니다.
# Gatsby
# 디렉토리 구조
posts
├── 포스트1
│ └── assets
│ └── image1.png
│ └── image2.png
│ └── index.md
├── 포스트2
│ └── index.md
└── 포스트3
└── index.md
# 빌드 후 URL
/포스트1/index.html
/포스트2/index.html
/포스트3/index.html
각 포스트의 이름을 가진 디렉토리를 만들고 내부에 index.md
파일을 생성하는 방식으로 바꾼 것을 볼 수 있습니다. 이런 식으로 구성한 이유는 본문에 첨부된 이미지 파일을 같은 디렉토리 내에서 관리하기 위해서입니다.
한편 Gatsby의 빌드 후 URL은 포스트 디렉토리 내에 index.html
파일이 생성되는 방식입니다. 따라서 /포스트/index.html
, 혹은 /포스트/
라는 URL로 접근할 수 있습니다.
Jekyll 방식을 호환하기 위해 디렉토리 이름을 YYYY/MM/DD
형태로 만드는 건 섹시하지 않기 때문에, 저는 마크다운 파일의 프론트매터의 날짜 값을 바탕으로 URL이 정해지는 구조를 고려했습니다. 이를 호환하기 위해서 저는 gatsby-node.js
파일을 아래와 같이 수정하였습니다.
// gatsby-node.js
exports.onCreateNode = ({ node, actions, getNode }) => {
// ...
if (node.internal.type === `MarkdownRemark`) {
const filePath = createFilePath({ node, getNode });
const trailingSlashRemovedPath = filePath.replace(/\/$/, '');
// 마크다운 파일의 프론트매터에서 날짜 값을 가져와, 이를 Jekyll URL 형식으로 변환
const date = node.frontmatter.date
.slice(0, 10)
.replace(/^(\d{4})-(\d{2})-(\d{2})/, '/$1/$2/$3');
// 이 날짜 값을 포함한 노드 필드를 `path`라는 이름으로 생성
createNodeField({
name: `path`,
node,
value: date + trailingSlashRemovedPath + '.html',
});
}
};
exports.createPages = async ({ graphql, actions, reporter }) => {
// ...
if (posts.length > 0) {
posts.forEach((post, _index) => {
createPage({
// 실제 페이지를 생성할 때 아까 설정한 포스트의 path를 적용
path: post.fields.path,
component: blogPost,
context: {
id: post.id,
},
});
});
}
};
이렇게 하면 포스트에 있는 date
프론트매터를 기준으로 동적 URL이 만들어지고, 해당 경로에 단일 HTML이 생성됩니다. 즉, 디렉토리 구조는 새롭게 변경하면서 결과물을 동일하게 유지할 수 있습니다.
URL이 기존과 같은 형태로 유지되는 것을 볼 수 있다
이제 여태껏 작성한 모든 포스트를 새 디렉토리 규칙에 맞추어 옮기기만 하면 되는데… 이미지 디렉토리 경로가 함께 바뀌는 탓에 자동화하기가 좀 난감한 부분이 있어서 일일이 노가다로 옮겼습니다(ㅠㅠ).
스타일 관리 방법
호환성에 대한 고민을 덜고 난 후에는 어떤 도구로 스타일을 관리할지 고민했습니다. Jekyll에서는 SCSS를 이용해 스타일을 관리하고 있었는데, Gatsby에서는 보다 다양한 선택지를 고를 수 있었기 때문입니다. 제가 고려한 선택지는 다음과 같았습니다.
- 전처리기 쓰기: SCSS
- CSS 라이브러리 쓰기: Bootstrap, Bulma, tailwind CSS
- CSS in JS 라이브러리 쓰기: Styled-Components, Emotion
각각의 장단점을 고려하느라 선택에 있어서도 시간이 많이 걸렸습니다. 결론적으로 제가 선택한 것은 Emotion 입니다.
전처리기나 CSS 라이브러리를 선택하지 않은 이유는 제가 이미 경험해봐서 익숙한 방법이기도 하고, CSS in JS 방식을 이용해 좀 더 프로그래머티컬한 스타일 관리 방법을 경험해보고 싶었기 때문입니다.
2021년 자료에 따르면 Emotion보다는 Styled-Components의 점유율이 아직 더 높은 것을 볼 수 있는데, 그럼에도 불구하고 Emotion을 선택한 이유는 크게 두 가지였습니다.
첫 번째 이유는 제가 느끼기에 Emotion은 Styled-Components의 슈퍼셋(Superset) 이었습니다. Styled-Components 방식의 스타일 방법도 지원하면서, CSS Prop, 합성(Composition) 처럼 복잡한 기능을 모두 지원했습니다. 두 번째 이유는 토스 프론트엔드 챕터에서 주로 쓰고 있다고 해서… 큰 회사가 선택한데는 다 이유가 있을 거라는 생각에 골랐습니다.
React UI 라이브러리
마침 MUI v5가 Emotion을 기본 스타일 관리 도구로 선택했길래 썼다
처음에는 UI 라이브러리 없이 모든 컴포넌트를 Emotion으로 직접 제작하려 했습니다. 하지만 그리드나 컨테이너, 버튼, 아이콘 등 필수적인 컴포넌트를 하나씩 일일이 제작하려다보니 시간도 오래 걸렸고, 이게 처음 의도했던 블로그 마이그레이션이 맞나 라는 생각이 들더군요.
그렇게 React UI 라이브러리를 찾아보다가, MUI의 기본 스타일 관리 도구가 Emotion인 것을 발견했습니다. 잠깐이긴 하지만 MUI를 써 본 경험도 있었고, 내부에서 제공하는 유틸리티 기능도 많았기에 고민을 했는데요. 결국 개발 시간 절약 및 안정성을 위해 MUI를 적용하기로 했습니다.
대신 머티리얼 UI가 특유의 좀 저렴한(?) 느낌이 있는데 그걸 최대한 지양하고자 일부 컴포넌트만 취사 선택해 쓰기로 했습니다.
다크 모드
왼쪽은 다크모드를 적용한 모습, 오른쪽은 그렇지 않은 모습
기존 블로그에도 있었던 기능 중 하나인 다크모드는 반드시 구현하고자 하는 기능 중 하나였습니다. 다크모드는 어두운 장소에서 눈부심을 방지해주고, 가독성을 증가시켜주는 역할을 하니까요. 특히 블로그는 장문의 글이 많기 때문에 이를 적용하는 것이 필수라고 생각했습니다.
다크모드를 활성화하는 조건은 다음과 같이 설정하고자 했습니다.
- 기본적으로 시스템 설정의 다크모드를 따름
- 하지만 사용자가 매뉴얼하게 다크모드 여부를 조정하면 해당 값은 유지됨
아마 다크모드를 구현해보신 분이라면 아시겠지만, CSS의 미디어 쿼리 중 하나인 prefers-color-scheme
를 이용하면 스타일 구현 자체는 어려운 것이 아닙니다.
@media (prefers-color-scheme: light) {
/* 라이트모드에서만 적용되는 스타일 */
}
@media (prefers-color-scheme: dark) {
/* 다크모드에서만 적용되는 스타일 */
}
하지만 두 번째 조건이었던 사용자가 매뉴얼하게 다크모드 여부를 조정하면 이 값을 저장하기 는 별도의 처리를 해줘야 하는데요, 이를 위해 로컬 스토리지를 이용하고자 합니다.
저는 다크모드 여부를 포함한 각종 설정값을 JavaScript 객체로 관리하기 위해, 로컬 스토리지에 settings
라는 키에 저장하려 합니다. 로컬 스토리지에 저장되는 값은 필요에 따라 앞으로도 계속 추가가 될 수 있다는 생각하에 저장할 때는 객체를 JSON.stringify
메소드로 직렬화, 불러올 때는 JSON.parse
메소드로 비직렬화하는 방식으로 구현했습니다.
로컬 스토리지에 저장된 특정한 값을 불러오고 저장하기 때문에 싱글톤(Singleton) 패턴처럼 사용되지만, 로직을 분리하기 위해 별도의 훅(Hook)으로 만들었습니다.
const [settings, setSettings] = useState<BlogSettings>(() => {
// 시스템 다크모드가 활성화 되어 있는지
const systemDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
// 서버 사이드로 빌드할 때 window 없는 경우 예외 처리
if (typeof window === 'undefined') {
return {
manualDarkMode: null,
systemDarkMode: false,
};
}
// 로컬 스토리지에 저장된 값이 있다면 불러오고, 없다면 초기값(빈 객체)을 설정
const savedSettings = JSON.parse(
window.localStorage.getItem(SETTING_KEY) || '{}'
);
// 빈 객체라면 manualDarkMode를 null로 초기화
// 시스템 다크 모드는 항상 새것으로 갱신
if (typeof savedSettings.systemDarkMode !== 'boolean') {
return {
manualDarkMode: null,
systemDarkMode,
};
}
return {
...savedSettings,
systemDarkMode,
};
});
이렇게 로컬 스토리지에서 메모리에 불러온 settings
를 근거로, 다크모드 여부를 확인하는 값을 메모이제이션 가능하게 만들 수 있습니다.
const isDarkMode = useMemo(() => {
if (typeof settings.manualDarkMode === 'boolean') {
return settings.manualDarkMode;
}
return settings.systemDarkMode;
}, [settings]);
이렇게 설정된 isDarkMode
값은 React의 Context Provider를 이용해 애플리케이션 전체에 주입됩니다. 내부에서 다크모드 여부를 불러올 때에는 useContext
를 이용해 참조하면 됩니다.
import { useSettingsContext } from '@/contexts';
export const DarkModeButton = () => {
const { isDarkMode, toggleManualDarkMode } = useSettingsContext();
return (
<IconItem onClick={() => toggleManualDarkMode()}>
{isDarkMode ? <LightModeIcon /> : <DarkModeIcon />}
</IconItem>
);
};
이렇게 하면 다크모드를 컴포넌트에서 참조할 수도 있고, 값을 변경시키는 메소드를 호출하게 만들 수도 있습니다.
FOUC 방지
다크모드가 활성화된 상태에서 처음 블로그에 접속하면 흰 화면이 번쩍이는데, 이게 심히 거슬린다
그런데 조금 거슬리는 부분이 있군요. 다크모드가 활성화된 상태에서 페이지를 새로 불러오게 되면 라이트모드의 화면이 잠깐 떴다가 다크모드로 전환이 되는 현상이 발생하고 있습니다. 이를 FOUC(Flash Of Unstyled Content) 현상의 일종(?)이라고도 부를 수 있을 것 같네요.
이게 어색하게 느껴질 수도 있지만 사실은 당연한 현상입니다. 그 이유는 꽤나 복합적인데, Gatsby의 빌드 결과물이 정적 HTML 파일이라는 것과 브라우저의 파싱 순서는 동기적 이라는 이유 때문입니다.
우선 Gatsby의 빌드 결과물이 정적 HTML 파일이라는 점에 주목해봅시다. 정적 HTML 파일이라는 것은 어떤 사용자가 접근하더라도 똑같은 HTML 파일을 보게 된다는 것을 의미합니다. 하지만 다크모드는 사용자의 설정에 따라 달라질 수 있습니다. 따라서 우리는 JavaScript를 이용해 클라이언트 쪽에서 다크모드 로직을 제어해야 합니다.
여기에서 브라우저가 동기적으로 HTML을 파싱하기 때문에 문제가 발생합니다. 일반적으로 빌드된 JavaScript 파일은 <body>
태그의 제일 마지막 부분에 위치합니다. 따라서 사용자는 일단 라이트모드 스타일이 적용된 HTML 파일을 보게 되고, 그 후 JavaScript가 실행이 되면서 다크모드가 동적으로 적용되게 됩니다. 이 때 다크모드가 적용되기까지 걸리는 시간동안 사용자는 라이트모드를 보게 되죠.
결론적으로 이 문제를 해결하기 위해서 저는 크게 세 가지의 방법을 적용했습니다.
- CSS 변수(variables)로 컬러 팔레트를 구현
gatsby-ssr.js
의setPreBodyComponents
API를 이용해<body>
태그 상단에 다크모드 여부를 확인하는 스크립트를 구현- MUI 테마에서 CSS 변수를 상속하도록 구현
각 방법에 대해서 조금 더 자세히 알아봅시다.
CSS 변수로 컬러 팔레트를 구현
우선 첫 번째로 :root
에 CSS 변수를 선언하고, 다크모드에서는 이를 오버라이딩하는 방식으로 컬러 팔레트를 만들었습니다. 저는 HTML 데이터 속성을 썼지만, CSS 클래스나 ID를 쓰는 것도 상관 없습니다. 라이트모드와 다크모드를 구별할 수 있기만 하면 돼요.
:root {
--color-background: var(--white100);
--color-text: var(--grey800);
/* ... */
}
[data-theme='dark'] {
--color-background: var(--dark500);
--color-text: var(--white100);
/* ... */
}
이렇게 하면 <body>
태그에 data-theme="dark"
속성을 토글해주는 방식으로 다크모드를 활성화/비활성화 할 수 있습니다.
<Component css={css`
background-color: var(--color-background);
color: var(--color-text);
`}>
또한 컴포넌트 레벨에서 다크모드를 전혀 신경쓰지 않아도 됩니다. 즉, 위의 컴포넌트는 <body>
의 data-theme="dark"
여부에 따라 색이 자동으로 바뀝니다.
서버 사이드 렌더링 API 사용하기
두 번째로 Gatsby의 서버 사이드 렌더링(SSR) API를 사용하도록 gatsby-ssr.js
파일을 만드는 것입니다.
<body>
를 렌더하기 전에, 미디어 쿼리 및 로컬 스토리지에 저장된 값을 불러와 data-theme
을 토글해주는 로직을 작성합니다. 이렇게 되면 HTML이 렌더되기 전에 CSS 팔레트가 오버라이딩 되기 때문에, 화면이 번쩍이는 현상을 방지할 수 있습니다.
// gatsby-ssr.js
exports.onRenderBody = ({ setPreBodyComponents }) => {
setPreBodyComponents([
React.createElement('script', {
key: 'darkmode',
dangerouslySetInnerHTML: {
__html: `
(function () {
// 다크모드 여부에 따라 document.body.dataset.theme 값을 토글
})();
`,
},
}),
]);
};
방금 작성한 스크립트 코드가 <body>
앞에 잘 들어간 모습
빌드 후 결과물을 살펴보면, 위에서 작성한 스크립트가 <body>
제일 앞에 들어가 있는 것을 확인할 수 있습니다. 그 덕에 data-theme="light"
가 잘 적용되어 있네요.
MUI 테마 오버라이딩
import { createTheme } from '@mui/material';
// 👌 mode 변수를 쓰면 다크모드도 쉽게 활성화 할 수 있다
const theme = createTheme({
mode: isDarkMode ? 'dark' : 'light',
status: {
danger: isDarkMode ? '#123123' : '#ABCABC',
},
});
세 번째로 MUI의 컴포넌트도 CSS 변수를 따르도록 스타일을 오버라이딩하게 만들었습니다. MUI v5는 Emotion에서 제공하는 Theming API를 테마 관리 용도로 사용합니다. 즉, 위와 같은 방법으로 테마의 색상을 커스터마이징 할 수 있습니다.
import { createTheme } from '@mui/material';
// ❌ 아래 코드는 오류가 난다
const theme = createTheme({
mode: isDarkMode ? 'dark' : 'light',
status: {
danger: 'var(--color-danger)',
},
});
하지만 현재는 위처럼 CSS 변수를 직접 넣는 방식으로는 테마를 설정할 수 없습니다. MUI 내부적으로 아직 CSS 변수를 받을 수 있는 기능이 개발되지 않았기 때문입니다.
아마 HEX, RGB 값을 연산하여 다른 컬러를 설정하는 기능이 내부적으로 포함되어 있기 때문인듯 하고, CSS 변수는 브라우저 런타임에서 값을 얻어와야 하기 때문에 현재 구조로는 CSS 변수를 Theme API에 넣기가 불가능하다고 합니다.
따라서 현재는 필요한 컴포넌트의 스타일을 테마 단계에서 직접 오버라이딩하는 방법으로 CSS 변수를 적용할 수 있습니다.
import { createTheme } from '@mui/material';
const theme = createTheme({
mode: isDarkMode ? 'dark' : 'light',
components: {
MuiIconButton: {
styleOverrides: {
root: {
color: 'var(--color-text)',
},
},
},
MuiButton: {
styleOverrides: {
root: {
color: 'var(--color-text)',
'&:hover': {
backgroundColor: 'var(--color-button-hover)',
},
},
},
},
},
});
이 외에도 MUI의 공식 Gatsby 구현 예제 코드를 살펴보면서 FOUC 이 발생하지 않게 추가 작업을 진행했습니다. 그 결과물은…
솔루션 적용 후에는 새로고침을 해도 매우 깔끔함
짠, 다크모드에서 새로고침을 할 때에도 화면이 번쩍이지 않는 것을 볼 수 있습니다. 글로 적고나니 굉장히 쉽게 해결한 것 같지만, 사실 이 문제를 풀기 위해 일주일이 넘는 시간에 걸쳐 삽질을 했다는 뒷 이야기가 있습니다… 😢 여러분은 헛고생하지 마시고 편하게 답 얻어가셔요.
배포
Github Action으로 자동 배포가 되게 만들어놨다
Gatsby의 배포는 Github Action을 이용해 별도의 배포 스크립트를 작성했습니다. 사실 Jekyll의 경우에는 Github Page에서 기본 지원하기 때문에 빌드할 필요 없이 리포지터리에 올려놓기만 해도 배포가 되는데요, Gatsby는 직접 빌드해서 정적으로 생성된 파일을 올려야만 합니다.
그런데 제 노트북에서 빌드를 할 때마다 쿨링 팬이 엄청 빨리 돌더라구요. 빌드 로그를 살펴보니 이미지 파일을 최적화하는 작업이 아무래도 원인인 듯 한데… 배포할 때마다 시끄러운 쿨링 팬 소리가 듣기 싫기도 했고, 제 노트북 리소스를 아끼자는 차원에서 빌드 자체도 Github Action으로 원격에서 돌게 했습니다. 그리고 배포가 완료되면 개인 슬랙 채널에 알람까지 오게 해 두었죠.
라이트하우스
새로 마이그레이션을 하면서 라이트하우스 점수도 신경을 쓰려 노력했습니다. 대부분은 Gatsby가 알아서 챙겨주지만, 배경과 텍스트 색상 대비율, ARIA 같은 접근성 속성, SEO, 시멘틱 태그 부분은 직접 신경썼습니다. 덕분에 80점대 점수를 90점 중반대까지 끌어올릴 수 있었습니다.
직접 마이그레이션 해보니 어때?
Gatsby행 비행기가 곧 떠납니다
이렇게 해서 블로그 마이그레이션 준비가 끝이 났습니다. 위에서 언급한 내용 말고도 실제로 더 많은 개발 이슈가 있었는데, 여백이 부족해서 적지 못하겠군요… 😇
사실 Gatsby로 마이그레이션을 하면서 여러가지 느낀 점이 있는데요, 그 중에서도 JavaScript 만으로 모든 개발 환경을 구성할 수 있다는 것이 압도적인 장점이었습니다. 그리고 Gatsby 자체적으로도 성능 개선 기능을 많이 지원하고, 커뮤니티의 활성화도 잘 되어 있어서 원하는 플러그인을 쉽게 구할 수 있었습니다. 내부 데이터 쿼리를 GraphQL로 하는 것도 적응되니 금방 할만하더라구요.
하지만 단점도 있었습니다. 제가 개발 과정에서 겪었던 가장 큰 어려움은 바로 개발 환경에서 만든 결과와 빌드 후 결과물이 달라질 수 있다는 것 이었습니다. 아무래도 개발 환경은 Webpack DevServer 기반으로 동작하는데 반해, 빌드 후 결과물은 정적 HTML 파일로 동작하는 것이기 때문에… 이 환경 차이에서 발생하는 문제가 있는 경우에 디버깅을 하기가 좀 까다로운 면이 있었습니다. 그리고 커스타미이징을 할 수 있는 다양한 방법을 제공하지만 이게 오히려 초보자에게는 장황해서 독이 되어 보일 것 같다는 생각도 들었어요.
그럼에도 불구하고 Jekyll보다 Gatsby에서의 개발 경험이 좋았느냐 라고 묻는다면 그렇다 라고 이야기할 수 있을 것 같습니다. 이렇게 제가 체험해본 Gatsby의 전체적인 만족도는 80~90점 정도 되는 것 같습니다!
아무튼 이렇게 마이그레이션 여정을 마쳐보려고 합니다. 조만간 새 블로그로 찾아뵙겠습니다!