
표준 HTML img 태그를 확장한 것.
이미지에 명확한 크기(width와 height 속성)를 지정
-> 브라우저가 페이지 로드 전에 적절한 공간을 확보
-> 이미지 로딩 후 레이아웃의 변동을 방지
-> CLS(시각적 안정성)점수 상승!
로딩 함수를 지정하여 이미지 URL을 동적으로 생성해주는 속성.
import Image from 'next/image'
const myLoader = ({ src, width, quality }) => {
return `https://example.com/${src}?w=${width}&q=${quality}`
}
export default function MyPage() {
return (
<Image
loader={myLoader}
src="logo.png"
alt="회사 로고"
width={500}
height={300}
quality={90}
/>
)
}
이미지가 부모 컨테이너를 완전히 채우게 하는 속성.
import Image from 'next/image'
export default function ResponsiveImage() {
return (
<div style={{ position: 'relative', width: '100%', height: '500px' }}>
<Image
src="/static/images/logo.png"
alt="회사 로고"
fill
style={{ objectFit: 'cover' }} //비율 유지
/>
</div>
)
}
import Image from 'next/image'
export default function Home() {
return (
<Image
src="/static/images/logo.png"
alt="로고"
placeholder="blur"
blurDataURL="data:image/png;base64,..."
width={300}
height={100}
/>
)
}
미디어 쿼리를 활용하여 브라우저가 화면 크기에 적절한 크기 및 포맷을 선택하여 로드한다.
import Image from 'next/image'
export default function ResponsiveImage() {
return (
<div>
<Image
src="/static/images/logo.png"
alt="반응형 로고 이미지"
width={1000}
height={600}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
layout="responsive"
/>
</div>
);
}
sizes와 pictures의 차이
특징 Next.js <Image>+sizesHTML <picture>최적화 지원 Next.js가 자동으로 이미지 최적화 및 로딩 처리 수동으로 최적화된 이미지를 제공해야 함 사용 편의성 단일 태그로 설정 간단, Next.js 기능과 연동 더 복잡한 HTML 구조 필요 호환성 Next.js 프로젝트에서만 사용 가능 모든 브라우저에서 동작 구체적인 제어 sizes속성으로 간단히 크기 힌트 제공<source>로 다양한 이미지 파일 선택다중 형식 지원 (webp 등) 기본적으로 지원하지 않음 <source>로 형식 교체 가능
import Image from 'next/image';
import { useEffect } from 'react';
function OptimizedPage() {
useEffect(() => {
// 페이지가 로드된 후 추가적인 자원을 동적으로 로드
const script = document.createElement('script');
script.src = "https://example.com/additional-feature.js";
script.async = true;
document.body.appendChild(script);
}, []);
return (
<div>
<h1>페이지 최적화 예시</h1>
<Image
src="/path/to/image.jpg"
alt="Optimized Image"
width={640}
height={480}
priority //해당 속성을 사용하면 중요한 이미지를 먼저 로드 가능
/>
</div>
);
}
onLoadingComplete, onLoad, onError
function handleLoadingComplete({naturalWidth, naturalHeight}) {
console.log(`로딩 완료: 너비 ${naturalWidth}, 높이 ${naturalHeight}`);
}
<Image
src="/static/images/logo.png"
alt="로고"
onLoadingComplete={handleLoadingComplete}
onLoad={() => console.log('이미지가 로드되었습니다.')}
onError={() => console.log('이미지 로드에 실패했습니다.')}
width={500}
height={300}
/>
Next.js의 이미지 최적화 기능을 사용자 정의 로더로 대체
외부 이미지 소스의 사용을 제한
-> 서버에 허용된 출처에서만 이미지 로드 가능
이미지가 다양한 디스플레이 크기에서 어떻게 보여질지 결정하는 데 사용
// next.config.js
module.exports = {
images: {
loader: 'custom',
loaderFile: './custom-image-loader.js',
remotePatterns: [
{protocol: 'https', hostname: 'example.com'}
],
images: {
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384]
},
},
}
SVG는 XSS 공격과 같은 보안 문제를 야기할 수 있다!
dangerouslyAllowSVG를 활성화하면 Next.js는 SVG 이미지에 대한 최적화를 시도한다.
-> 추가적인 보안 조치를 취하는 것이 좋음!
// next.config.js
module.exports = {
images: {
dangerouslyAllowSVG: true,
contentSecurityPolicy: "default-src 'self'; img-src 'self' data: https:;"
},
}
next.config.js에 도메인을 등록한 후 사용
module.exports = {
images: {
domains: ['example.com'],
},
}
Next.js는 폰트 로딩 시점을 최적화하여 초기 로드 시 폰트가 페이지 렌더링을 방해하지 않도록 관리한다!
자체적으로 폰트를 최적화하고 필요한 폰트만 불러오는 로딩 방식.
기본적으로 모든 Google Fonts를 자체 호스팅으로 지원
-> 폰트 파일을 사전에 다운로드하고 프로젝트에 포함시켜 브라우저가 폰트 파일을 캐시
-> 즉, 구글 API로 별도 요청을 전송하지 않는다!
최신 폰트 포맷(WOFF2)을 사용하여 파일 크기를 최소화하고 로딩 속도를 최적화
Script 컴포넌트는 스크립트를 효율적으로 관리하여 페이지 로딩 시간에 미치는 영향을 최소화한다!
next/dynamic을 사용한 동적 임포트로 컴포넌트를 비동기적으로 로드
코드를 분할하여 사용자가 필요한 순간에만 스크립트를 로드
| 로드 방식 | 설명 | 주요 사용 사례 |
|---|---|---|
| beforeInteractive | 페이지의 상호작용 가능한 요소들이 로드되기 전 스크립트 로드 | 필수적인 스크립트 로드가 사용자 경험을 방해하지 않도록 할 때 사용 |
| afterInteractive | 사용자와의 첫 상호작용 이후 스크립트 로드 | 페이지 로딩에 영향을 덜 미치는 비필수 스크립트 로드에 사용 |
| lazyOnload | 페이지가 완전히 로드된 후, 브라우저가 유휴 상태가 되었을 때 스크립트 로드 | 꼭 필요하지 않은 기능의 스크립트 로드에 사용 |
import Script from 'next/script';
function Home() {
return (
<div>
<Script
src="https://example.com/essential-script.js"
strategy="beforeInteractive" //필수 스크립트는 초기에 로드
/>
<Script
src="https://example.com/non-essential-script.js"
strategy="lazyOnload" //중요하지 않은 스크립트는 나중에 로드
onLoad={() => console.log('Non-essential Script loaded')}
/>
<p>Next.js 홈페이지 예시</p>
</div>
);
}
export default Home;
이동하는 페이지를 전체 새로고침하는 것이 아닌, 최적화된 번들만 일부 로드한다!
미리 가져오기 기능은 제품(Production) 모드에서만 사용 가능!
Link 컴포넌트는 prefetch 옵션을 통해 뷰포트에 보여질 때, 연결된 경로의 데이터를 미리 가져와 탐색 성능을 크게 향상시킨다.
export default function Links() {
return (
<>
<Link href={someLink}>null</Link>
<Link prefetch={true} href={someLink}>true</Link>
<Link prefetch={false} href={someLink}>false</Link>
</>
)
}
useRouter도 prefetch를 사용할 수 있다!
const router = useRouter() useEffect(() => { router.prefetch('/movies') }, [router])
| 기능 | 설명 |
|---|---|
| 구조화된 데이터 마크업 | next-seo 패키지를 사용하여 검색 엔진이 콘텐츠의 맥락을 이해하도록 도와줌. |
| 사이트맵 생성 | sitemap 패키지를 사용하여 검색 엔진이 웹 페이지를 효율적으로 검색하고 색인 생성하도록 도움. |
| 페이지 매김 처리 | next/link 구성 요소와 rel="next/prev" 속성을 사용하여 페이지 매김을 지원. |
| 이미지 최적화 | 이미지 최적화는 페이지 로드 시간을 줄이고, SEO에 긍정적인 영향을 미침. |
| 페이지 공유 카드 구현 | next-seo 패키지를 사용하여 Open Graph 및 Twitter 카드 기능을 구현할 수 있음. |
서버측 렌더링 프로세스 중에 데이터를 가져와 페이지에 소품으로 전달
-> 검색 엔진이 페이지의 전체 콘텐츠를 볼 수 있다!
export async function getServerSideProps(context) {
const res = await fetch('https://api.example.com/data');
const data = await res.json();
return { props: { data }, };
}
layout.tsx 혹은 page.tsx 에서 metadata 객체를 내보s내기
import Header from '@/components/Header'
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: '제목!',
description: '설명..',
openGraph: {
title: '제목',
// ...
},
twitter: {
title: '제목',
// ...
}
}
generateMetadata함수는 페이지와 같은 인수를 받아서 처리
-> API 요청으로 생성한 메타데이터를 가져올 수 있다!
//app/movies/[movieId]/page.tsxTSX
import type DetailedMovie from '@/stores/movies'
function fetchMovie(id, plot) {
const res = await fetch(`https://omdbapi.com/i=${id}&plot=${plot}`)
return await res.json()
}
//해당 함수를 사용하여 param으로 넘어온 값을 사용해 메타데이터를 만들 수 있음!
export async function generateMetadata({
params,
searchParams
}) {
const movie = await fetchMovie(params.movieId, searchParams.plot)
return {
title: movie.Title,
description: movie.Plot,
}
}
export default async function MovieDetails({
params,
searchParams
}) {
const movie = await fetchMovie(params.movieId, searchParams.plot)
return (
<>
<h1>{movie.Title}</h1>
<p>{movie.Plot}</p>
</>
)
}
title을 객체 타입으로 지정해 템플릿(template)과 기본값(default)을 제공할 수 있다!
-> %s 치환 문자에 동적으로 값 삽입됨
(%s에 넣고 싶은 부분은 어디다 쓰는거지..?)
//app/layout.tsxTSX
import Header from '@/components/Header'
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: {
template: '%s | 사이트이름',
default: '사이트이름'
},
description: '설명..',
}
| 기능 | 설명 |
|---|---|
| 비동기 요청 API | 데이터 요청 없는 컴포넌트, 주요 API(params, searchParams)를 비동기로 처리하여 초기 로드 속도 향상. |
| 캐싱 기본값 변경 | 기본 캐싱 설정을 해제하여 라이브러리의 캐싱 기능과 중복되지 않음. force-static 옵션으로 재캐싱 가능. |
| Form 컴포넌트 | form 요소를 확장한 최적화 컴포넌트로 제출 경로를 프리패칭하고 제출 데이터를 쿼리스트링으로 보존. |
| 자체 호스팅 개선 | 캐시 제어 헤더에 대한 더 많은 제어 기능 추가, 이미지 최적화 성능 향상. |
| 보안 강화 | 서버 액션이 공개 엔드포인트가 되는 것을 방지하기 위한 기능 추가. |
| 빌드 및 개발 성능 개선 | 정적 페이지 생성 최적화, 서버 컴포넌트의 HMR 기능 강화. |
{
"extends": [
"next/core-web-vitals",
"next/typescript",
"prettier"
]
}
{
"semi": false,
"singleQuote": true,
"singleAttributePerLine": true,
"bracketSameLine": true,
"endOfLine": "lf",
"trailingComma": "none",
"arrowParens": "avoid",
"plugins": ["prettier-plugin-tailwindcss"]
}
{
"[javascript]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
클라이언트 컴포넌트 또한 일부 정적 요소는 서버에서 렌더링한다!
서버사이드에서의 비동기 컴포넌트 스트리밍
export default async function Xyz() { await wait(3000) //이렇게 하면 서버사이드에서도 timeout처럼 사용가능 return <h2>Xyz 컴포넌트!</h2> }
| 파일명 | 설명 |
|---|---|
| error | 에러 페이지 |
| layout | 고정 레이아웃 |
| loading | 로딩 페이지 |
| not-found | 찾을 수 없는(404) 페이지 |
| page | 기본 페이지 |
| template | 변화 레이아웃(탐색 시) |
| default | |
| global-error | |
| route |
error.tsx는 클라이언트 컴포넌트여야 한다
클라이언트에서 발생하는 에러 상황까지 처리하기 위해서!
ex) 사용자 입력의 유효성 검사, 잘못된 API 요청..
소괄호를 사용해 URL 경로에 영향을 주지 않는 그룹을 만들 수 있다.
-> 각자의 레이아웃(layout.tsx)을 가질 수 있다!
│ │ ├─movies/
│ │ │ ├─[movieId]/
│ │ │ │ └─page.tsx
│ │ └─layout.tsx <== (movies) 그룹에서만 동작하는 레이아웃
│ ├─layout.tsx <== 루트 레이아웃
│ └─page.tsx
@ 접두사의 폴더는 URL 경로에 영향을 주지 않는 페이지
-> 하나의 레이아웃에서 병렬로 경로 처리 가능
-> 여러 페이지나 컴포넌트를 병렬로 로드 및 렌더링 가능
-> 순차적 로딩 대비 전체 로딩 시간이 단축
-> 각 컴포넌트의 로딩 상태 독립적으로 표시 가능
├─app
│ ├─async
│ │ ├─@abc <==병렬 처리된다
│ │ │ ├─loading.tsx
│ │ │ └─page.tsx
│ │ ├─@xyz <==병렬 처리된다
│ │ │ ├─loading.tsx
│ │ │ └─page.tsx
│ │ ├─layout.tsx
│ │ ├─loading.tsx
│ │ └─page.tsx
적용되지 않으면 .next 폴더 삭제 후 서버 재시작하기
특정 URL에 접근했을 때 기존 페이지가 아닌, 지정된 다른 UI나 레이아웃을 표시할 수 있다.
-> 경로 가로채기를 사용하면 특정 하위 경로가 아닌 다른 경로에서도 상위 레이아웃을 재사용할 수 있음.
파일명에 (.)온점이 하나 추가될 때마다 상위로 올라간다.
| 폴더 경로 | URL | 설명 |
|---|---|---|
| /app/a/b/(.)x | /a/b/x | 같은 레벨 세그먼트 |
| /app/a/b/(..)x | /a/x | 상위 레벨 세그먼트 |
| /app/a/b/(...)x | /x | 루트 레벨 세그먼트 |
├─app
│ ├─a
│ │ └─b
│ │ └─c
│ │ ├─(...)x <======1번x
│ │ │ └─page.tsx
│ │ │ └─layout.tsx <===1번 layout
│ │ └─page.tsx
│ └─x <========2번x
│ └─page.tsx
│ └─layout.tsx <======2번 layout
경로 가로채기로 url이 /x일때 a,b,c가 모두 렌더링되고 1번 x가 보임!
미들웨어로 가능한데 경로 가로채기를 쓰는 이유
구분 미들웨어 경로 가로채기 처리 위치 서버 측 클라이언트 측 실행 시점 요청이 서버로 전달되기 전에 실행 프론트엔드 컴포넌트 렌더링 시점에서 실행 목적 서버의 URL 리디렉션, 인증, 헤더 수정 등 사전 처리 URL 해석 후 페이지 및 레이아웃 렌더링 제어 주요 처리 리디렉션, 인증, 헤더 수정 등 요청 처리 페이지 렌더링 방식, 동적 로딩, 경로 기반 최적화
주의사항
- 미들웨어는 호출이 끝나야 경로 접근이 가능 -> 복잡하거나 오래 걸리는 작업 피하기
- 쿠키나 헤더는 미들웨어 실행 후에만 수정 가능!
복잡한 경로 매칭 처리에 사용하면 좋다!
//middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { match } from 'path-to-regexp'
const getSession = async () => {
return false // 임시 데이터 반환
}
const matchersForAuth = [
'/dashboard{/*path}',
'/myaccount{/*path}',
'/settings{/*path}'
]
// 경로 일치 확인 함수. 여기서 path-to-regexp 라이브러리의 match를 쓴다
function isMatch(pathname: string, urls: string[]) {
return urls.some(url => !!match(url)(pathname))
}
// 미들웨어 함수
export async function middleware(request: NextRequest) {
// 인증이 필요한 페이지 접근 제어!
if (isMatch(request.nextUrl.pathname, matchersForAuth)) {
return (await getSession())
? NextResponse.next()
: NextResponse.redirect(new URL('/signin', request.url))
}
return NextResponse.next()
}
api폴더에서는 page.tsx 등의 기본 파일 규칙이 아닌, route.ts 파일을 사용한다!
/app/api 폴더 내 구조를 통해 서버리스 API 엔드포인트 정의 가능
├─app
│ ├─api
│ │ ├─movies
│ │ │ └─[movieId]
│ │ │ └─route.ts
│ │ └─users
│ │ └─route.ts
//app/api/movies/[movieId]/route.tsTS
import type { NextRequest } from 'next/server'
type Context = {
params: { movieId: string }
}
export async function GET(request: NextRequest, context: Context) {
const { movieId } = context.params // 동적 경로
const res = await fetch(`https://omdbapi.com/?apikey=7035c60c&i=${movieId}`)
const data = await res.json()
return Response.json(data)
}
서버에서만 실행되는 함수.
서버, 클라이언트 컴포넌트 둘 다 서버 액션(함수)를 가져와 사용할 수 있다!
//serverActions/index.ts
'use server'
export async function wait(duration = 1000): Promise<{ message: string }> {
console.log(`Run 'wait' function`)
return new Promise(resolve =>
setTimeout(() => resolve({ message: `Waited for ${duration}ms` }), duration)
)
}
form태그의 action 속성에 서버 액션을 넣으면..
-> 서버 액션은 FormData 객체를 자동으로 받아 처리한다!
서버측에서 비동기로 실행됨
-> 서버와 클라이언트 간 통신 동안 사용자 인터페이스가 멈추지 않는다!
서버 액션에서 사용되는 인자와 반환 값은 React에 의해 직렬화 가능해야 한다!
ex)문자열, 숫자, 날짜, 배열..
// 클라이언트 컴포넌트
import { updateProfile } from './actions';
function UserProfile({ userId }) {
const handleUpdate = updateProfile.bind(null, userId); //바인딩 함수를 사용하여 추가 가능
return (
<form action={handleUpdate}>
<textarea name="bio"></textarea>
<button type="submit">업데이트</button>
</form>
);
}
데이터 변형 후 클라이언트에 최신 정보를 반영하기 위해서는 데이터 캐시를 재검증해야 한다!
특정 경로의 최신 데이터를 생성하여 캐시를 갱신하는 함수
'use server';
import { revalidatePath } from 'next/server';
export async function updateProfile(formData) {
// 프로필 업데이트 로직
await userProfile.update(formData);
// 업데이트된 프로필 페이지 캐시 재검증
await revalidatePath('/profile');
return { success: true, message: '프로필 업데이트 완료' };
}
// next.config.js에서 안전한 출처 설정
module.exports = {
serverActions: {
allowedOrigins: ['https://trusteddomain.com'],
},
}
서버 액션 호출시 Origin 헤더와 Host 헤더가 일치하지 않는 경우 요청 거부
환경변수는 기본적으로 서버 컴포넌트에서만 접근 가능
-> NEXTPUBLIC~~로 명명시 클라이언트 컴포넌트에서 접근 가능
주의사항
보안이 요구되는 API 키 등의 중요한 정보는 서버에서만 사용하기!
환경변수 자동완성을 위해 사용
//types/env.d.tsTS
export declare global {
namespace NodeJS {
interface ProcessEnv {
OMDB_API_KEY_KEY: string
NEXT_PUBLIC_BASE_URL: string
NEXT_PUBLIC_SITE_NAME: string
}
}
}
빌드 시점에 HTML을 미리 생성해 빠른 배포 및 성능 제공.
개발 중 실시간 코드 변경 반영.
클라이언트에서 로드되기 전에 페이지의 초기 상태를 미리 렌더링.
기본 설정으로 코드 트랜스파일 및 번들링.
일부 정적 페이지를 런타임에 재생성.
페이지별 번들 크기 분석 도구 제공.
HTML을 서버에서 미리 렌더링하여 초기 로드 속도 향상.
-> 브라우저가 처리해야 할 스크립트 양을 줄여 더 빠르게 인터랙티브 상태가 된다.