- #Blog
- #Next.js
- #Optimization
블로그(1) - @next/font, Image 컴포넌트로 최적화하기
Prolip
2024-02-12
Posts 목록 데이터 받아와 사용자에게 표시하기, @next/font와 Image 컴포넌트로 최적화하기
시작하며..
오늘은 인트로에서 왜 Next를 이용해 블로그를 제작했는가에 이어 어떤 방식으로 개발했는지에 대해 기록하려고 합니다..
다들 무슨 폰트 사용하세요?
저는 G마켓 산스, 노토 산스 등을 돌고 돌아 결국 프리텐다드를 주로 사용하고 있습니다.
웹 페이지에 들어왔을 때 폰트 하나만으로 UI가 더 예뻐 보이고 그런데 저만 그런가요??
그래서 전 이번에도 프로젝트를 시작하자마자 폰트부터 적용했습니다.
Font Optimization
우리의 웹 페이지에 지정한 폰트가 있다고 가정해봅시다. 이용자가 페이지에 방문했을 때 폰트가 로드되기 전까진 브라우저의 기본 폰트를 이용해 렌더하게 됩니다.. 못생겼죠.
그리고 이후 폰트가 모두 로드되면 페이지에 적용되며 리렌더링이 이루어집니다. 이 때 브라우저의 기본 폰트와 직접 설정한 폰트의 굵기, 높이 등 크기가 달라 레이아웃 시프트가 발생합니다.
Layout shift
레이아웃 시프트란 먼저 로딩된 컨텐츠가 후에 로드된 컨텐츠로 인해 밀리는 현상입니다. 가장 흔한 이미지로 인한 현상을 설명 드리겠습니다.
- 사용자가 웹 페이지에 접속합니다.
- 사실 이 웹 페이지엔 대문짝만한 로고 이미지가 중앙에 존재하지만 사용자는 이 이미지가 로드 되지 않아 로고 이미지 밑에 먼저 로드 된 카테고리 콘텐츠를 먼저 보게 됩니다.
- 이후 이미지가 로드 되어 사용자는 갑자기 카테고리 콘텐츠가 밑으로 밀려나가는 현상을 보게 됩니다.
@next/font
Next엔 폰트를 최적화하기 위한 next/font가 있습니다.
next/font는 사용하는 글꼴에 대한 자체 호스팅 기능이 포함되어 있습니다. 또한 size-adjust라는 CSS 프로퍼티를 이용해 레이아웃 시프트 없이 웹 폰트를 최적화합니다.
또한 13 버전부터는 모든 Google 폰트를 자동으로 자체 호스팅합니다. 이는 배포에 포함되며 동일한 도메인에서 제공되어 브라우저에서 Google로 요청을 보내지 않습니다.
import { Inter } from 'next/font/google' const inter = Inter({ subsets: ['latin'], display: 'swap', }) export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="en" className={inter.className}> <body>{children}</body> </html> ) }
공식 문서에 따라 상위 layout 파일에서 글꼴을 설정하면 됩니다. 또한 가변 글꼴을 사용하는 것이 성능과 유연성 제공에 유리합니다.
프리텐다드는 Google 폰트에 없어요..
그렇습니다 프리텐다드는 Google 폰트에 없습니다..만 Next에선 로컬 폰트에 대한 최적화도 동일하게 지원합니다.
import localFont from "next/font/local"; const pretendard = localFont({ src: "./fonts/PretendardVariable.woff2", display: "swap", }); <html className={pretendard.className}> <body> {children} </body> </html>
이렇게 가변글꼴을 프로젝트 app 폴더 내의 fonts 폴더에 저장한 후 적용했습니다.
1. 포스팅 목록 보여주기
이제 우리가 Intro에서 정리한 기능을 토대로 하나씩 구현해보도록 합시다.
우선 작성한 게시물들을 어떻게 관리할지 정해봅시다.
주인장은 마크다운 형식으로 글을 작성하고 이를 불러와 사용자에게 표시합니다.
그렇다면 이 게시물의 간략한 소개(title, description, category 등)를 카드 형태로 보여주고 싶다면 어떻게 구현할까요??
게시물마다 고유한 정보를 담을 json 파일을 생성해 관리하면 편리하겠군요!
.json 파일로 관리하기
주인장은 이를 관리하기 위한 posts.json 파일을 만들었습니다.
{ "title": "Mock Title", "description": "Mock Description", "date": "2024-01-02", "category": "Blog", "path": "mock-path", },
이런 구조로 말입니다..
이렇게 게시물이 가질 고유한 속성을 설정해놓으면 카드 형태의 간략한 소개를 작성하기 편리해집니다. 내가 게시물을 하나 작성할 때마다 이 템플릿을 복사해 수정 후 저장만 해준다면 게시물이 뿅뿅 추가될 겁니다.
블로그를 제작할 당시엔 작성한 글이 없어 Mock 데이터를 이용해 화면을 구성하는 방식으로 개발했습니다. path는 블로그 내부에서 보여줄 해당 블로그 썸네일 이미지와 md 파일을 불러오기 위한 경로입니다.
.json 파일을 읽어 객체 표시해보기
이제 이 파일을 서버에서 읽어 사용자에게 표시해봅시다.
컴포넌트 내부에 직접 코드를 작성해도 되겠지만 전 서비스에 관련된 코드는 sevices 폴더 등을 생성해 분리하여 작성하는 편입니다.
// posts.ts export type PostType = { title: string; description: string; date: Date; category: string; path: string; }; export async function getPostList(): Promise<Post[]> { const filePath = path.join(process.cwd(), "data", "posts.json"); return readFile(filePath, "utf-8") .then<Post[]>(JSON.parse) .then((data) => data.sort((a, b) => (a.date > b.date ? -1 : 1))); }
Next는 기본적으로 서버에서 동작하는 서버 컴포넌트로 작동합니다.
그런고로 서버에 있는 파일을 읽고, 쓰고, 서버 경로에 접근도 가능합니다.
로컬 상태의 json 파일을 읽어오기 위해 fs 모듈을 이용했습니다.
그리고 타입 스크립트를 사용하기 때문에 해당 포스트 데이터의 타입도 지정해줍니다.
마지막에 sort를 해주는 이유는 작성한 날짜를 이용해 정렬하기 위해서 입니다.
작성한 함수를 이용해 포스트 목록 불러오기
// Posts.tsx export default async function Posts() { const postList = await getPostList(); console.log(postList); return ( <section> <h1>Latest Posts</h1> </section> ); }
이렇게 실행하면 성공적으로 터미널에 작성한 json 파일을 불러오는걸 확인할 수 있습니다.
이제 이를 이용해 카드 형태로 사용자에게 보여주는 일만 남았군요!
아 그리고 await를 사용하지 않으면 getPostList 함수는 Promise를 반환하기 때문에 작성하는 것입니다.. 당연히 await를 사용하기 위해 컴포넌트에 async도 적어줍시다. 이거 서버 컴포넌트에서만 가능합니다.
우선 이 postList를 이용해 카드 형태로 표현할 Post 컴포넌트를 먼저 만들어주는게 좋겠습니다.
// Post.tsx export default function Post({ post: { title, description, category, date, path }}: { post: PostType }) { return ( <section> <Image src={`/images/posts/${path}.png`} width={400} height={400} alt={title} /> <section> <article> <p>{date}</p> <p>{title}</p> </article> <article> <p>{description}</p> <p>{category}</p> </article> </section> </section> </section> ); }
간단합니다.. 받아온 목록을 사용하면 됩니다.
post.title, post.description 앞에 post를 쓰고 싶지 않으니 구조분해할당을 통해 받아주도록 합시다.
또한 타입은 아까 posts.ts에서 생성해둔 PostType을 import해 지정해주도록 합시다.
CSS는 굳이 적지 않겠습니다.. 저보다 뛰어난 미적 감각을 가지고 계실테니까요..
이제 postList를 순회하며 post 데이터를 이 컴포넌트로 전달해주기만 한다면 json 파일에 작성한 파일 목록을 사용자에게 모두 보여줄 수 있게 됩니다.
// Posts.tsx export default async function Posts() { const postList = await getPostList(); return ( <section> <h1>Latest Posts</h1> <ul> {postList && postList.map(post => ( <li key={post.path}> <Post post={post} /> </li> ))} </ul> {postList && } </section> ); }
여기까지 1, 2번 기능에 대한 구현이 완료되었습니다! 정말 너무 쉽죠..
물론 제 현재 코드는 상당히 다르지만 기초적인 원리는 똑같습니다..
근데 중간에 사용한 Image 컴포넌트는 무엇인가요?
일반적으로 우리는 이미지를 사용할 때 img 태그를 사용합니다. 하지만 Next엔 Image라는 컴포넌트가 있습니다.
Image
- Image 컴포넌트는 우리가 사용하는 이미지를 자체적으로 스크린 사이즈별로 이미지 리소스를 최적화해줍니다.
- 우리가 로컬, 서버 상에 있는 이미지를 static하게 import하는 경우에는 Next가 이미지에 대한 정보를 가지고 있습니다. 그래서 이를 통해 width, height 등을 알아서 지정해줍니다. (굉장합니다.)
잠깐..! 만약 인터넷 상에 있는 이미지를 URL 형식으로 받는다면요??
- static하게 가지고 있지 않은 image는 width,height을 자체적으로 지정해줘야 됩니다. 또한, next.config.js에서 해당 URL에 대한 정보를 입력해줘야 됩니다.
아래는 unsplash에서 URL 형식으로 이미지를 받을 때 config 파일에 작성해야되는 예시입니다.
const nextConfig = { images: { remotePatterns: [ { protocol: "https", hostname: "images.unsplash.com", }, ], }, };
그래서 장점이 뭡니까??
Image 컴포넌트를 통해 최적화된 이미지를 사용한다면 레이아웃 시프트가 일어나지 않습니다.
이유는 Next에서 해당 이미지에 대한 고정된 사이즈가 있기 때문에 이미지가 다운로드 되는 동안 그 자리를 비워두고 레이아웃을 구성하기 때문입니다.
그리고 사이트의 대표적인 이미지인 배너 이미지 등을 사용 중이라면 해당 이미지의 우선순위 또한 지정해 가장 먼저 로드 되도록 priority 옵션을 줄 수 있습니다..
Image 컴포넌트에 대한 더 자세한 링크는 Components: <Image> | Next.js (nextjs.org) 공식 문서를 참조하시면 좋습니다.
마치며..
기능에 대한 기록을 하려다 폰트며.. 이미지며.. 자꾸 다른 얘기로 빠지는 거 같지만 제가 이걸 왜 사용했는지 첨부하는 것입니다만.. 저도 다시 한번 복습하는 과정이기도 합니다.. 단순히 코드를 따라치는 것만으로는 제 것이 되지 않습니다. 내가 이걸 ‘왜’ 사용하는지 항상 나에게 질문을 던지고 찾아보는 것이 좋습니다.. 물론 다들 그러실 거라고 믿습니다.