6/5/2024
앞선 포스트에서 설명했듯이 Github Pages는 정적 웹 호스팅 서비스이다. Spring이나 Express같은 서버 프로그램을 실행 시킬 수 없다.
때문에, 작성한 포스트를 동적으로 보여주기 위해서는 외부에 또다른 서버를 두어야 한다. 서버를 운영하는 것이 무료였다면 전혀 문제될 것이 없겠다만,
당연히 비용이 발생하기 때문에 외부 서버를 둔다는 선택지는 제외하였다.
(AWS 프리티어를 사용하는 선택지도 있지만, 이미 프리티어를 다른 서비스를 위해 사용중인 마당에 머리 아프게 더 추가 하고 싶지는 않았다.)
그리하여, 서버 사이드 렌더링(SSR)도, API도 활용할 수 없는 상황이다.
그렇다면 남은 선택지는 정적인 HTML파일 만으로 블로그를 보여주는 방법 뿐이다.
HTML 파일을 만드는 것은 두 가지 방법을 들 수 있다.
간단하게 말 그대로 하나하나의 HTML파일을 만들거나. React, Vue 같은 SPA에 특화된 프레임워크(라이브러리)를 사용하거나.
전자의 방식을 택했다면 엄청난 노가다가 필요했을 것이다. 사소한 변경 사항에도 모든 HTML을 수정해야 한다는 것을 생각하면 끔찍하다.
그렇다면 익숙한 React로 만들면 되었겠다만, 이상한 도전정신이 발생하여 React도 Vue도 아닌 Next로 만들어 봐야겠다는 결정을 내려버렸다.
Next는 React를 기반으로 하는 웹 개발 프레임워크다.
React를 기반으로 한다는데, 무슨 차이냐? 하면 React만 사용한다면 이는 SPA가 될 것이다. 빌드(실행)시 HTML 파일은 index.html 하나 뿐일 것이고,
그 html에서는 각종 JS파일을 읽어 화면을 구성할 것이다. 하지만 Next를 사용한다면 빌드(실행)시 HTML 파일은 여러개가 될 수 있고, 그 html에서는 SPA처럼 각종 JS파일을 읽어 화면을 구성한다.
HTML 파일이 여러개가 될 수 있다는 것은 사실 Next는 SSR을 지원한다는 것을 얘기한다.
React 방식으로 서술된 화면을 Next 서버가 렌더링하여 이를 클라이언트에 보내준다. 그 이후 클라이언트에서 발생하는 상호작용은 기존의 React SPA처럼 동작하는 것이다.
이러한 특성으로 Next는 React 개발 방법을 이어간다는 장점과 함께 SEO도 챙길 수 있다는 장점이 있어 각광받는 프레임워크이다.
그런데, Github Pages에서는 서버를 구동할 수 없다고 하지 않았나? 그렇다. 그래서 나는 Next를 서버로 사용하지 않는다.
그저 SSR 기능을 활용하여 모든 포스트에 대한 정적 HTML을 만드는 방법으로 활용하는 것이다.
그렇게 할 수 있었던 것에는 Next에서 제공하는 generateStaticParams 같은 기능이 있었기 때문이다.
Next로 정적 페이지를 만드는 것을 서술하기 전에 우선 Next의 라우팅 방식에 대해서 간략하게 설명하겠다.
React에서 라우팅 기능을 사용하기 위해서는 react-router 같은 라이브러리를 활용하는데, 각 라우팅 경로에 대한 설정을 수동으로 지정해주어야 한다.
<Routes> <Route path="/" element={<Root />} /> <Route path="login" element={<LoginPage />} /> <Route path="sign" element={<SignPage />} /> <Route path="welcome" element={<WelcomePage/>}/> </Routes>
위 코드는 react-router에서 라우팅 경로에 대한 컴포넌트를 설정하는 예시로, 새로운 경로가 생길 때 마다 컴포넌트를 만드는 것 외에도 저 코드에 route를 추가해 주어야 한다. 개발하면서 page 컴포넌트를 만들고 router 파일에 한줄 추가하는 일이 번거롭거나 귀찮다고 느낀 적은 거의 없다. 하지만 아마 많은 이들이 "누가 자동으로 알아서 매핑해주면 좋겠다~" 라는 생각을 해보지 않았을까?
Next의 시작이 그것이었을진 모르겠다. 다만, Next의 큰 특장점이 바로 파일(디렉토리)만 만들면 알아서 라우팅 해준다는 점이다.
Next는 디렉토리 구조를 가지고 라우팅 경로와 컴포넌트를 매핑해준다.
Next 프로젝트를 생성하면 src 폴더 안에 app이라는 폴더가 있을 것이다.(Next 13v 이상의 app router일 경우)
그 안에는 page.tsx가 있을 것인데, 이것이 바로 root 경로의 페이지가 된다.
만약 /something 이라는 경로를 만들고 싶다면, 간단하게 app 폴더 안에 something 이라는 폴더를 만들고 그 안에 page.tsx를 만들면 된다.
그 뿐만 아니라 동적인 경로 처리도 가능한데, 폴더명을 대괄호[]
로 감싸주면 된다.
예를들어 /something/[id] 처럼 something뒤에 id를 path parameter로 동적인 경로를 만들고 싶다면 something 폴더 안에 [id] 라는 폴더를 만들고,
그 안에 page.tsx를 만들어주면 된다. 해당 page.tsx의 페이지 컴포넌트는 props로 id를 받을 수 있는데, 아래 예시를 참고하면 된다.
export default function Page({ params }: { params: { slug: string } }) { return <div>My Post: {params.slug}</div> }
path parameter에 대한 라우팅은 위 방법 뿐 아니라 여러 방법이 있다. 아래는 공식 문서에서 제시해주고 있는 방식들이다.
Route params Type Definition app/blog/[slug]/page.js
{ slug: string }
app/shop/[...slug]/page.js
{ slug: string[] }
app/shop/[[...slug]]/page.js
{ slug?: string[] }
app/[categoryId]/[itemId]/page.js
{ categoryId: string, itemId: string }
하지만 이렇게 한다고 해서 Next가 정적인 html 파일을 만들어주는 것은 아니다.
위 방법대로만 한다면, 정적인 것이 아니라 요청 받는 경로에 따라 Next가 동적으로 해당하는 페이지 컴포넌트를 렌더링해서 응답을 보내줄 뿐이다.
정적 html 파일을 만들기 위해서는 generateStaticParams 기능을 활용해야 한다.
generateStaticParams 함수는 Next에서 제공하는 기능으로, Page.tsx에서 해당 함수를 export 하면 동적 라우팅 경로를 정적인 html로 생성해준다.
아래는 공식 문서에 있는 예시이다.
export async function generateStaticParams() { const posts = await fetch('https://.../posts').then((res) => res.json()) return posts.map((post) => ({ slug: post.slug, })) } export default function Page({ params }) { const { slug } = params // ... }
위 예시 코드는 API를 통해 post 목록을 가져오고, generateStaticParams 함수는 가져온 post의 목록으로 slug 값을 가진 객체 배열을 만들어 리턴한다.
리턴하는 값은 단일 라우팅 경로가 되어야 하고, Next는 해당 경로들에 대한 결과물을 미리 만들어둔다.
리턴한 것을 보면 배열로 되어 있는 것을 볼 수 있는데, 배열에 들어있는 항목만큼 static한 html이 만들어진다고 보면 된다.
return된 배열은 하나하나 해당 Page의 props로 전달되는데, params라는 이름을 가지고 있다.
공식 문서에는 동적 라우팅 경로를 return하는 방식을 여러개 제시해주고 있다.
Example Route generateStaticParams Return Type /product/[id] { id: string }[] /products/[category]/[product] { category: string, product: string }[] /products/[...slug] { slug: string[] }[]
이렇게 generateStaticParams 함수를 정의해주고 나서 빌드(npm run build
)를 하게되면 generateStaticParams 함수가 리턴한 경로에 맞게 html 파일을 만들어 준다.
만약에 아래 예시처럼 정의했다고 해보자.
// app/product/[...slug]/page.tsx export function generateStaticParams() { return [{ slug: ['a', '1'] }, { slug: ['b', '2'] }, { slug: ['c', '3'] }] } export default function Page({ params }: { params: { slug: string[] } }) { const { slug } = params // ... }
위 정의에 따르면 동적으로 제공된 경로는 아래와 같다.
그렇다면 만들어지는 파일도 아래와 같이 만들어 지는 것이다.
/product |_ /a |_ 1.html |_ /b |_ 2.html |_ /c |_ 3.html
지금까지 generateStaticParams 함수를 사용하여 정적 html 파일을 만드는 방법에 대해서 알아보았다.
이러한 기능을 사용하면 자주 변동되지 않을 문서들에 대해 미리 렌더링해 둠으로써 렌더링 타임도 단축하고, SEO도 챙길 수 있을 것이다.
다만, 자주 변동되는 데이터를 사용한다면 이를 활용하는 것은 불가능 할 것이다. 또한 너무 많은 정적 html을 만드려 한다면 빌드 속도가 많이 느려질 것이다.
역시 늘 그렇듯, 모든 기능은 상황과 필요에 맞게 사용해야 하는 것이다.
Next에서 페이지 마다 메타데이터를 구성하는 방법은 두가지가 있다.
import { Metadata } from 'next' // either Static metadata export const metadata: Metadata = { title: '...', } // or Dynamic metadata export async function generateMetadata({ params }) { return { title: '...', } }
위 코드블럭에도 써있듯 generateMetadata 함수는 동적인 메타데이터를 만들 때 사용하는 것이다.
generateStaticParams 함수와 함께 활용한다면, 동적인 경로에 따라 각기 다른 메타데이터를 설정할 수 있다.
블로그 서비스를 만들면서 나는 포스트들을 카테고리로 묶을 수 있었으면 했다. 크게는 Blog(개발 외적인 사소한 내용들), Develop(개발 관련 내용) 등으로 나누고,
그 안에서는 관련된 주제끼리 묶은 컬렉션으로 나눈다. 이러한 구조가 포스트를 만들때도 눈에 잘 보였으면 했고, 그 구조 그대로 포스트들의 URL Path가 되길 바랐다.
파일 시스템을 활용해서 그러한 디렉토리 구조로 저장된 mdx 파일들을 수집하고, 수집된 파일들의 경로를 그대로 generateStaticParams의 리턴값으로 만들었다.
// app/[...slug]/page.tsx export async function generateStaticParams() { const pathList = getAllPosts().map(post=>post.path); return pathList.concat(getCategories().map(category=>[category]),getAllCollections().map((subject:Collection)=>subject.path!)).map((path) => { return ({ slug : [...path] })}); }
app 폴더 밑에 [...slug] 라는 디렉토리를 만들어 모든 경로에 대해 라우팅 되도록 하였고, 나머지 경로는 generateStaticParams 함수를 통해 동적으로 제공한다.
아래는 실제로 mdx 파일들이 들어있는 폴더의 구조와 이를 빌드 하였을 때 만들어지는 html 파일들의 디렉토리 구조를 보여준다.
//mdx 파일 구조 /contents |_ /blog |_ howtomarkdown.mdx |_ postplan.mdx |_ /develop |_ /refactoring |_ basicToObject.mdx |_ index.mdx
//빌드된 html 파일 구조 /blog |_ /howtomarkdown |_ index.html |_ /postplan |_ index.html /develop |_ refactoring |_ index.html |_ /basicToObject |_ index.html
이렇게 빌드된 html 파일 구조를 그대로 github pages 리포지토리에 업로드 하면 블로그가 만들어지는 것이다.
다음에는 mdx 파일로 어떻게 블로그 포스트의 내용을 구성하였는지에 대해 서술하겠다.