문제의 발단#
위대한 수업에 대한 정리를 블로그에 글을 쓸 때 캡처할 때가 많은데요. 그럴 때 이 이미지의 용량이 보통 3MB를 넘었어요. 게시글에 이미지가 몇 개 없으면 그렇게 느리진 않았는데 글을 쓸 때마다 조금씩 페이지 진입이 느려지는 게 느껴졌어요.
그래서 기존 이미지들을 webp로 변경하면 용량도 줄고 Lighthouse 평가에서 좋은 점수받겠다고 생각하다 오늘 문제를 해결했어요.
기존 방식#
저는 옵시디언을 블로그 글을 작성하는 데 사용하고 있어요. 그래서 제 블로그 get.github.io저장소를 보시면 blog
라는 폴더에 .obsidian
폴더가 들어있어요. 제 PC에선 옵시디언으로 해당 폴더를 열어서 vault로 사용하고 있어요.
옵시디언에서도 설정을 따로 할 수 있지만 저는 블로그 글의 모든 이미지는 blog/assets
폴더에 저장되도록 설정했어요.
그런데 여기서 한 가지 주의할 점은 Next.js는 프로젝트 루트에 위치한 public
폴더에 이미지를 두어야만 참조할 수 있어요.
이미지를 public에 옮길 때 저는 package.json에 아래 스크립트를 추가했어요.
"scripts": {
// ...중략
"copy-dir": "mkdir -p public/blog/assets && cp -r blog/assets/* public/blog/assets/",
"predev": "yarn copy-dir",
"prebuild": "yarn copy-dir",
}
yarn dev
든 yarn build
를 실행하면 copy-dir
이 먼저 돌아가서 이미지를 복사하게끔 했었어요.
(predev와 prebuild 모두 로컬에서 테스트할 때는 잘 되었는데 깃허브 액션에서는 동작하지 않더라구요. 혹시 이유를 아시는 분 있다면 알려주세요...🥲)
이렇게 옮겨진 위치는 옵시디언에서 표시하는 경로와 다르기 때문에 Next.js로 실행시키는 환경에서는 마크다운 파일 내부에서 이미지 경로에 대해 변경이 필요해요. 저는 이 문제를 contentlayer.config.ts
에 있는 remarkPlugins
에 직접 만든 함수도 추가해서 해결했어요.
/**
* @type {import(‘unified’).Plugin<[], Root>}
* @param {string} options.root
*/
const remarkSourceRedirect =
(options?: void | undefined) => async (tree: any, file: any) => {
const images: any[] = []
visit(tree, ‘paragraph’, (node) => {
const image = node.children.find((child: any) => child.type === ‘image’)
if (image) {
image.url = `/blog/${image.url}`
}
}
})
}
그다음 GitHub Pages에서도 잘 보여야 해서 .github/workflows/next.yml
파일도 수정해야 해요.
# jobs.build.steps에 추가
# ...중략
# 중간에 추가해 주세요. 저는 next build 전에 먼저 실행되게끔 했어요
- name: Copy blog images to public folder
run: mkdir -p ${{ github.workspace }}/public/blog/assets && cp -r ${{ github.workspace }}/blog/assets/* ${{ github.workspace }}/public/blog/assets/
이렇게 여러 곳을 건드려서 동작하게 했지만, GitHub Action이 돌아간 Add post의 결과에서 Artifacts 항목을 보면 105MB인 걸 볼 수 있어요. 다운받아 보면 아시겠지만, 대부분이 이미지 용량이었어요. 용량도 용량이지만 UX가 별로였어요. 특정 페이지에 진입하기까지 너무 느렸어요. 그래서 바꿔 보기로 했어요.
개선한 방법#
이미지 용량을 줄이는 데는 sharp를 사용했어요. Next.js도 sharp를 사용하는 걸로 알고 있어요. 리눅스 명령을 그대로 사용하던 기존 방식에서 노드 환경에서 파일을 읽고, 용량을 줄이고, 다른 경로에 파일을 만드는 과정을 수행하는 것이 편할 거라 생각했어요.
더 좋은 방법을 못 찾았지만 Next.js 빌드 단계에서 제공하는 함수를 찾진 못해서 이렇게 노드 환경에서 해결하는 방법을 사용했어요.
먼저 scripts/copy-image.ts
라는 파일을 만들었어요.
import fs from ‘fs’
import path from ‘path’
import sharp from ‘sharp’
const copyImage = async (src: string, dest: string) => {
const image = sharp(src)
const resizedImage = image.webp()
await resizedImage.toFile(dest)
}
const srcDir = path.resolve(__dirname, ‘..’, ‘blog’, ‘assets’)
const destDir = path.resolve(__dirname, ‘..’, ‘public’, ‘blog’, ‘assets’)
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true })
}
fs.readdirSync(srcDir).forEach(async (file) => {
const imagePath = path.resolve(srcDir, file)
const stat = fs.statSync(imagePath)
if (stat.isFile() && /\.(PNG|JPG|JPEG|png|jpg|jpeg)$/.test(file)) {
const copyPath = path
.resolve(destDir, file)
.replace(/\.(PNG|JPG|JPEG|png|jpg|jpeg)$/, ‘.webp’)
await copyImage(imagePath, copyPath)
}
})
__dirname
경로가 scripts 폴더 내부여서 프로젝트 루트로 가기 위해 ..
을 추가했어요. 그런데 저는 ts 파일을 만들고 tsc로 실행하니 같은 경로에 js 파일이 만들어졌어요. 저는 실행만 되고 파일은 생기질 않길 원해서 ts-node를 설치했어요(yarn add -D ts-node
).
기존 스크립트도 이렇게 변경하면 돼요.
"scripts": {
// ...중략
"copy-dir": "ts-node scripts/copy-image.ts",
"predev": "yarn copy-dir",
"prebuild": "yarn copy-dir",
}
그리고 remarkSourceRedirect
함수도 변경되어야 해요. 저는 webp로 파일을 만들었기 때문에 기존 확장자들은 옵시디언에서만 사용되고 webp는 Next.js에서 쓰이도록 알려줘야 해요.
// 이 부분만 바꿨어요!
image.url = `/blog/${image.url}`
// 이렇게 바꾸면 돼요.
image.url = `/blog/${image.url.replace(/\.(PNG|JPG|JPEG|png|jpg|jpeg)$/, '.webp')}`
여기까지만 하면 로컬에서 잘 되는 걸 확인할 수 있어요.
마지막으로 .github/workflows/next.yml
도 바꿔주면 배포 이후에도 잘 돼요. 대신 ts-node와 sharp 등 여러 라이브러리를 사용하고 있어서 의존성 설치 이후에 이미지 복사를 해야 해요.
# jobs.build.steps에 추가
# ...중략
- name: Install dependencies
run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }}
- name: Copy blog images to public folder
run: ${{ steps.detect-package-manager.outputs.runner }} copy-dir
- name: Build with Next.js
run: ${{ steps.detect-package-manager.outputs.runner }} build
이렇게 하면 16MB 용량으로 압축 파일이 만들어져요!
이전에 이미지 문제를 해결했던 방법#
이건 개선 전에 이야기지만, 이미지를 복사하기 전에는 마크다운 파일에 이미지 경로를 획득한 후 해당 이미지를 base64로 변경해서 data:image/webp;base64,UklGRowzAABX
이런 식으로 변경했어요. 이렇게 하면 public
폴더에 이미지를 넣지 않더라도 마크다운에 긴 텍스트로 변경되지만 웹에서 보인다는 장점이 있어요. 그러나 이 방법도 게시글이 많아지다 보니 고민할 수밖에 없었어요. 이번에 webp로 용량을 줄이기 전이라 3MB가 넘는 이미지가 여러 개 있는 게시글은 들어가는 버튼 이벤트가 발생하면 페이지로 전환되기까지 몇 초씩 걸렸던 게 문제였어요. 그래서 결국 이미지 자체를 옮기는 방법을 생각하게 되었어요.
그리고 또 한가지 문제가 있어요. base64로 변경하면 링크를 공유했을 때 이미지가 나오질 않았어요. 이는 SEO 점수가 낮아질 수도 있어요.
Reason and free inquiry are the only effectual agents against error.
— Thomas Jefferson