앱 최적화
# 이미지 컴포넌트
NextJS는 <Image/> 라는 특정 이미지 컴포넌트를 제공한다. 이미지는 웹 사이트 성능에 꽤 무거운 부분을 차지하기 때문에 이미지에 대한 부분만 최적화를 시켜도 페이지의 성능을 높일 수 있다. 이 컴포넌트의 역할은 로드된 이미지 파일의 사이즈나 포맷 형태를 자동으로 최적화 시켜주어 사이트 성능과 속도 향상에 도움을 준다.
width / height 프로퍼티
width와 height 속성은 <Image/> 컴포넌트에 반드시 들어가야 할 속성이다. 해당 이미지가 들어갈 적합한 공간을 미리 마련해 레이아웃 이동을 방지해야 하기 때문이다. 만약 로컬 파일에 있는 이미지를 사용한다면 src 속성에 import한 이미지의 .src를 제거하고 사용해야 Next가 logo의 width와 height 속성을 인식할 수 있다.
import logo from "@/assets/logo.png";
import Image from "next/image";
// 일반 이미지 태그
<img src={logo.src} alt='logo' />
// NextJS의 이미지 컴포넌트
<Image src={logo} alt='logo' />
불러온 logo 이미지를 콘솔에 찍으면 위와 같이 이미지 객체 내에 여러 속성이 존재하는 것을 알 수 있다.
srcset 프로퍼티
<Image/>컴포넌트는 srcset 속성을 동적으로 생성하는데, 이 속성은 다양한 화면 해상도에 맞춰 이미지를 로드하도록 브라우저에게 명령하는 속성이다. 하나 이상의 해상도가 ,로 구분되어 디스플레이 해상도에 따라 출력할 이미지를 결정해준다. 이 속성이 크기를 알맞기 조정해준다 해도 원본 입력 파일이 너무 크지 않도록 주의한다.
sizes 프로퍼티
이 속성은 반응형으로 사용하거나 이미지 원본 크기가 너무 큰 경우에 크기를 조절하기 위해 사용된다. 예를 들어, 이미지가 뷰포트의 10%만큼만 차지할 것을 미리 알고 있다면, sizes 속성에 ‘10vw’ 를 설정하여 이미지가 뷰포트의 10%만 차지하도록 조정할 수 있다. 이로 인해 이미지의 불필요한 로딩을 줄이고 페이지 로딩 속도를 개선할 수 있다.
<Image src={logo} size='10vw' alt='logo' />
loading / priority 프로퍼티
<Image />컴포넌트의 loading 속성에는 기본적으로 ’lazy’값이 적용된다. 이는 브라우저가 페이지 성능에 큰 영향을 미치는 이미지를 지연 로드하여 페이지 로딩 시간을 향상시키기 위한 것이다. 그러나 페이지에 미리 표시되어야 하는 이미지가 있는 경우, priority 속성을 추가하여 이 이미지의 지연 로딩을 비활성화 처리해줄 수 있다.
<Image src={logo} priority alt='logo' />
fill 프로퍼티
fill 속성은 이미지의 크기를 미리 알 수 없을 때 유용하다. 이미지 컴포넌트에서는 width와 height 속성을 필수로 지정해야 하지만, 이를 직접 문자열로 지정하는 것은 바람직하지 않다. 이때 fill 속성을 사용하면, 이미지를 부모 컨테이너로 감싸고 부모 컨테이너의 스타일링을 통해 이미지 크기를 조절할 수 있다.
fill 속성을 사용할 때는 이미지의 너비와 높이를 미리 알 수 없으므로, sizes 속성을 추가하여 사용할 수 있는 뷰포트 너비를 지정해주는 것이 좋다.
외부 링크 설정
Next는 보안 상의 이유로 외부 이미지 링크를 불러오게 되면 오류를 표시한다. 따라서 외부 사이트의 링크를 허용하려면 next.config.mjs 파일에서 설정을 추가해야 한다. 예를 들어, 아래와 같이 설정하면 res.cloudinary.com 주소의 이미지를 로드할 수 있다.
const nextConfig = {
images: {
remotePatterns: [{ hostname: "res.cloudinary.com" }]
},
};
export default nextConfig;
# 메타데이터
정적 메타데이터
page.js 또는 layout.js 파일에서 metadata 상수를 export 하여 정적인 메타데이터를 설정 할 수 있다.
export const metadata = {
title: 'Title',
description: 'Description'
}
동적 메타데이터
page.js 또는 layout.js 파일에서 generateMetadata() 를 비동기 함수를 export 하여 설정할 수 있다. 이 함수는 메타데이터 객체를 반환하며, 비동기 작업을 통해 동적인 데이터를 사용할 수 있다. 동적인 페이지가 아닌 정적인 페이지에서도 아래와 같이 동적 데이터를 사용할 수도 있다.
export async function generateMetadata() {
const posts = await getPosts();
const numberOfPosts = posts.length;
return {
title: `${numberOfPosts} posts`,
description: `Description`
}
}
레이아웃에서 메타데이터
페이지에 자체 메타데이터가 설정되지 않은 경우, 가장 가까운 layout.js에서 설정한 메타데이터가 적용된다. 만약 페이지에서 title에 대한 메타데이터만 설정되어 있다면, description은 layout.js에 있는 것과 병합될 것이다. 또한, layout보다 page에 적용된 메타데이터의 우선순위가 높다. 이를 통해 특정 페이지에 맞춤형 메타데이터를 설정할 수 있으며, 기본값은 레이아웃에서 제공할 수 있다.
캐싱 처리
NextJS는 성능을 최대한 향상시키고 비용을 줄이기 위해 가능한 많은 것들을 캐싱처리한다. 데이터 캐싱은 한 번 요청된 데이터나 계산 결과를 저장해 두었다가 같은 요청에 대해 빠르게 응답할 수 있게 한다. 총 4가지 유형의 캐싱 메커니즘을 제공하며, 사용 목적과 상황에 따라 적절하게 적용할 수 있다.
# 요청 메모이제이션 (Request Memoization)
요청 메모이제이션은 중복 요청을 방지하는 메커니즘이다. 동일한 설정을 가진 요청들이 여러 번 발생하면 해당 요청을 캐싱하여 한 번만 수행하도록 한다. 이렇게 하면 데이터 소스에 대해 불필요한 중복 요청이 발생하는 것을 방지할 수 있다. 단, 요청 URL이 같더라도 헤더, 쿼리 매개변수 등 설정이 다르면 다른 요청으로 인식되므로 주의해야 한다.
const response = await fetch("http://localhost:8080/messages");
// 위 코드와 api 요청 주소는 같아도 헤더 구성이 다르므로 요청이 두 번 발생하게 된다.
const response1 = await fetch("http://localhost:8080/messages", {
headers: {
'X-ID': 'layout',
},
});
💡요청 메모이제이션은 NextJS의 기능이아닌 React의 기능이다. 따라서 리액트의 컴포넌트 내에서만 발생한다.
# 데이터 캐시 (Data Cache)
fetch API를 사용해 데이터를 가져오는 경우, 응답 데이터를 내부적으로 관리되는 서버 측 캐시에 저장하고 계속해서 이 데이터를 재사용하게 된다. 이는 사용자가 재사용하지 말라고 지시하기 전까지는 영구적으로 캐싱된 데이터를 사용하게 된다. 따라서 캐싱된 데이터가 아닌 새로운 데이터를 받고 싶다면 fetch API 설정을 변경하여 재검증을 통해 새로운 데이터를 받아오면 된다.
// fetch() 에서 cache 설정 변경
const response = await fetch("http://localhost:8080/messages", {
cache: "no-store",
});
// 5초마다 한 번씩 새로운 데이터를 가져올 수 있도록 next설정 재구성
const response = await fetch("http://localhost:8080/messages", {
next: { revalidate: 5 },
});
// 상단에 해당 상수를 통해 파일 전체에 설정 추가
export const revalidate = 5;
export const dynamic = 'force-dynamic';
- revalidate와 dynamic상수는 예약어로, export를 해줘야 Next가 인식한다.
- dynamic의 기본값은 ‘auto’ 이다. ‘force-dynamic’ 값은 {cache: ‘no-store’} 설정과 동일하다.
- ‘force-static’값은 새로운 데이터를 가져오지 않는다.
import { unstable_noStore } from "next/cache";
export default async function MessagesPage() {
unstable_noStore();
const response = await fetch("http://localhost:8080/messages");
return <Messages messages={messages} />;
}
위 코드와 같이 unstable_noStore() 를 불러와서 사용하는 방법도 있다. 이는 데이터가 캐시되지 않도록 확실하게 하고자 하는 컴포넌트 내에서 해당 함수를 호출하여 사용할 수 있다. 이는 const dynamic = ‘force-dynamic’ 설정과 동일한 효과를 제공하지만, 같은 페이지에 컴포넌트가 여러 개 있고 특정 컴포넌트에서만 캐싱을 방지하고 싶은 경우 유용하다.
# 전체 라우트 캐시 (Full Route Cache)
NextJS는 따로 설정이 없다면 기본적으로 빌드 시에 전체 경로에 대해 캐싱을 수행하고, 이를 정적 페이지로 렌더링한다. 이렇게 미리 렌더링된 페이지는 재렌더링이나 업데이트를 하지 않기 때문에, 데이터 변경이 필요한 페이지에는 문제가 될 수 있다.
// app/messages/page.js
import Messages from "@/components/messages";
import { getMessages } from "@/lib/messages";
// 동적인 페이지임을 알림
export const dynamic = "force-dynamic";
export default async function MessagesPage() {
const messages = await getMessages();
return <Messages messages={messages} />;
}
따라서 특정 경로에서 항상 최신 데이터를 가져오도록 설정할 수 있다. 이를 위해 해당 페이지에 export const dynamic = 'force-dynamic'; 코드를 추가한다. 이렇게 하면 빌드 시에 해당 페이지가 동적인 페이지로 표시되어 항상 최신 데이터를 가져오도록 보장된다. 이 설정을 통해 데이터 캐시뿐만 아니라 전체 라우트 캐시도 무효화되어 해당 페이지를 여러 번 새로고침하더라도 항상 최신 데이터를 가져오도록 재검증을 수행한다.
// 페이지
export default async function MessagesPage() {
const response = await fetch("http://localhost:8080/messages",{
next: { tags: ['msg'] }
});
return <Messages messages={messages} />;
}
// 서버 액션 함수
async function createMessage(formData) {
'use server';
const message = formData.get('message');
addMessage(message);
revalidatePath('/messages');
revalidateTag('msg');
}
이외에도revalidatePath()와 revalidateTag()를 사용해 데이터 재검증을 수행할 수 있다.
# 라우터 캐시 (Router Cache)
라우터 캐시는 클라이언트 측에 저장되는 캐시로, Client-side Cache 또는 Prefetch Cache라고 불린다. 이는 사용자가 앱 내에서 페이지를 전환할 때 이를 최적화하여 성능을 향상시키는 메커니즘으로 <Link /> 와 같은 컴포넌트가 이에 해당한다. 전체 라우트 캐시와 마찬가지로 revalidatePath(), revalidateTag() 등을 사용해 캐시 재검증을 할 수 있다.
# 커스텀 데이터 소스
외부 API와의 통신 대신 직접 데이터베이스 소스를 사용하는 경우, fetch()를 사용하지 않을 수도 있다. 이로인해 fetch()에서 제공하는 여러 캐시 옵션들(cache: "no-store", next: { revalidate: 5 })을 사용할 수 없고, 중복 요청을 제거하는 기능 또한 사라지게 된다. 특히 NextJS의 강력한 캐싱 기능은 문제가 아닌 성능 최적화에 필수적이다. 따라서 이러한 상황에서는 React의 cache() 와 NextJS의 unstable_cache()를 활용해 커스텀 데이터의 캐싱을 관리할 수 있다.
// cache() 사용
import { cache } from "react";
export const getMessages = cache(function getMessages() {
return db.prepare("SELECT * FROM messages").all();
});
cache()는 중복 요청 제거가 발생해야 하는 함수를 감싸는 데 사용한다. 만약 getMessages() 가 여러 곳에서 호출되었다면 이에 대한 요청은 한 번만 발생하게 된다.
// unstable_cache(), cache() 사용
import { cache } from "react";
import { unstable_cache } from "next/cache";
export const getMessages = unstable_cache(
cache(function getMessages() {
return db.prepare("SELECT * FROM messages").all();
}),['messages']
);
// messages/page.js
import { getMessages } from "@/lib/messages";
export default async function MessagesPage({ children }) {
const messages = await getMessages();
return <Messages messages={messages} />;
}
unstable_cache() 은 비동기적으로 데이터를 로드하고 캐시하는데 사용한다. 항상 프로미스를 반환하므로 감싸진 함수를 사용하는 경우에는 async await을 사용하여 비동기 함수로 만들어줘야 한다.
💡 unstable_cache()와 cache()를 감싸서 함께 사용할 수 있다. 둘은 다른 역할임을 기억하기
import { revalidatePath, revalidateTag } from 'next/cache';
async function createMessage(formData) {
'use server';
const message = formData.get('message');
addMessage(message);
// 메시지 추가 함수 내에 업데이트 할 경로를 추가하여 캐싱된 데이터 업데이트
revalidatePath('/messages')
// 메시지 추가 함수 내에 업데이트 할 태그를 추가하여 캐싱된 데이터 업데이트
revalidateTag('msg')
redirect('/messages');
}
export const getMessages = unstable_cache(
cache(function getMessages() {
console.log("Fetching messages from db");
return db.prepare("SELECT * FROM messages").all();
}),
["messages"],
// 배열 내에 있는 태그가 호출된 곳에서만 캐싱된 데이터 업데이트
{ tags: ['msg'] } // or { revalidate: 5 }
);
데이터가 업데이트 되어야 할 페이지에는 revalidatePath(), revalidateTag()를 사용하면 된다. unstable_cache()의 두 번째 인수에는 내부적으로 캐시된 데이터를 식별하는데 사용할 수 있는 캐시 키를 넣을 수 있는데 이는 배열로 지정해야 한다. (태그와 다르다) unstable_cache()의 세 번째 인수에는 revalidate 또는 tags 값이 들어갈 수 있다.
유데미 Next.js 14 & React - 완벽 가이드를 수강하고 직접 정리한 내용입니다.
'개발 > 프론트엔드' 카테고리의 다른 글
[Typescript] 타입스크립트는 왜 쓰는걸까? (4) | 2024.09.05 |
---|---|
[React] 리액트로 스톱워치 Chrome Extension 만들기 (0) | 2024.08.29 |
[NextJS]App Router - 라우팅(Routing) 종류 (0) | 2024.07.05 |
[NextJS] 14버전 앱 라우터에 대해 알아보자 (0) | 2024.06.25 |
[WIL] ReactJS 함수형과 클래스형 (0) | 2022.08.07 |