.env 파일은 API 서버 주소와 같은 환경변수들을 관리하는 파일이다. 다양한 .env 파일이 존재한다.
next dev 사용시 env 파일 적용 순서는 아래와 같다. (.env.development.local이 우선순위 가장 높음)
.env.development.local
.env.local
.env.development
.env
next build 사용시 env 파일 적용 순서는 아래와 같다. (.env.production.local이 우선순위 가장 높음)
.env.production.local
.env.local
.env.production
.env
내가 원하는 건 다양한 환경에 따른 환경변수 실행인데 next dev의 경우 기본적으로 development가 들어가고 next build 및 start의 경우는 produciton으로 자동 세팅이 된다. 환경 파일을 강제로 주입하려고 했는데 프레임워크에 반하는 것이기 때문에 하지 않기로 했다. 로컬에서 개발할 땐 env.local 파일을 사용하고 개발서버의 경우 env.development파일을 운영서버의 경우 env.production 파일이 injection 되었으면 좋겠는데 흠... 굳이 위에 있는 파일을 다 만들지 않는 게 좋을 거 같고 개발의 경우 .env.development를 운영의 경우는 env.production 즉, 2가지만 만들어서 혼동이 없게 하는게 맞는 거 같다.
infisical이라는 걸 통해 서버에서 env파일을 관리하고 injection해서 사용하게 할 수 있다.
Lazy Loading을 사용하는 경우 클라이언트 구성요소는 기본적으로 사전 렌더링(SSR)됩니다
ssr:false 옵션으로 비활성이 가능합니다 → 좀 이상한 점이 처음에 렌더링 될 때 C는 C Is Loading이 나오고 A는 서버에서 렌더링 되어 A Is Loading이 안나오지만 Toogle을 클릭하면 둘다 Is Loading이 나오게 된다 Toogle은 훅으로 조작하고 A랑 C는 바뀐 점이 없기 때문에 가상돔 차이가 있는 B만 렌더링되어야하는데 A랑 C도 왜 그런지 모르겠다
import Image from 'next/image'
import profilePic from '../public/me.png'
export default function Page() {
return <Image src={profilePic} alt="Picture of the author" priority />
}
fill을 사용하면 상위 크기만큼 이미지를 다 채울 수 있습니다 → position:relative 상위 div 필수
📝폰트
next/font모든 글꼴 파일 에 대한 자동 자체 호스팅이 내장되어 있습니다 → CSS 및 글꼴 파일은 빌드 시 다운로드되며 나머지 정적 자산과 함께 자체 호스팅됩니다. 즉, 브라우저는 요청을 Google로 전송하지 않습니다
import { Inter } from 'next/font/google'
// If loading a variable font, you don't need to specify the font weight
const inter = Inter({
subsets: ['latin'],
display: 'swap',
})
/** 가변 글꼴을 사용할 수 없는 경우 가중치 지정 필수 **/
//const roboto = Roboto({
// weight: '400',
// subsets: ['latin'],
// display: 'swap',
//})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className={inter.className}>
<body>{children}</body>
</html>
)
}
weight
가변글꼴에는 줄 필요 없고 나머지는 주는데 브라우저상에서 파악해 어떤 weight를 줄지 알아서 판단한다 만약 style을 따로 줄 경우 style이 우선순위가 높음
subsets
하위 집합 중 미리 로드할 하위 집합을 정의합니다 → latin이 우선순위로 preload되고 나머지언어는 후순위 느낌인데 테스트 해봐도 잘 모르겠다
latin이라고 적혀있으면 latin만 적용시키는게 아닌 다른 언어가 있으면 알아서 파악해서 가져온다
display
block
폰트가 사용 가능하게 되면, 브라우저는 기다리지 않고 즉시 해당 폰트를 사용하여 페이지를 렌더링합니다.
swap
폰트가 다운로드되면, 브라우저는 기존에 사용된 폰트와 즉시 교체하여 더 나은 사용자 경험을 제공합니다.
/** carousel.tsx **/
'use client'
import { Carousel } from 'acme-carousel'
export default Carousel
/** page.tsx **/
import Carousel from './carousel'
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* Works, since Carousel is a Client Component */}
<Carousel />
</div>
)
}
클라이언트 사이드 렌더링이 필요한 라이브러리(Carousel은 외부 라이브러리 컴포넌트)일 경우 이런식으로 처리가 가능하다 → 이렇게 사용하면 서버 컴포넌트 내에서 사용이 가능하다
클라이언트 구성 요소 내에서 사용할 가능성이 높으므로 대부분의 타사 구성 요소를 래핑할 필요가 없을 것으로 예상되지만 컨텍스트 제공자(리덕스, ThemeContext 따위)의 경우 어쩔 수 없이 /app/layout.tsx에서 ‘use client’를 명시해야한다 → 여기에 use client명시한다고 하위 페이지들이 전부 client side render가 되는 건 아니다
📝런타임
Edge 런타임
Edge Runtime의 속도는 최소한의 리소스 사용으로 인해 발생하지만 많은 시나리오에서 제한될 수 있습니다
Vercel의 Edge 런타임에서 실행되는 코드는 1MB에서 4MB 사이를 초과할 수 없습니다., 이 제한에는 가져온 패키지, 글꼴 및 파일이 포함되며 배포 인프라에 따라 달라집니다.
Node.js 런타임
Node.js API와 이에 의존하는 모든 npm 패키지에 액세스할 수 있습니다. 그러나 Edge 런타임을 사용하는 만큼 시작하는 것이 빠르지는 않습니다.
Next.js 애플리케이션을 Node.js 서버에 배포하려면 인프라를 관리, 확장 및 구성해야 합니다
서버리스 Node.js
Vercel의 서버리스 기능을 사용하면 전체 코드 크기는 50MB입니다 가져온 패키지, 글꼴 및 파일을 포함합니다 Vercel에서 지원한 거이기 때문에 가장 이상적이라고 이야기한다
그런 다음 들어오는 요청에 따라 요청 또는 응답 헤더를 다시 작성, 리디렉션, 수정하거나 직접 응답하여 응답을 수정할 수 있습니다.
미들웨어를 정의하려면 프로젝트 루트에 있는 파일 middleware.ts(또는 )을 사용한다
middleware.ts 예제
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
// This function can be marked `async` if using `await` inside
export function middleware(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url))
}
// See "Matching Paths" below to learn more
export const config = {
matcher: '/about/:path*',
}
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/about')) {
return NextResponse.rewrite(new URL('/about-2', request.url))
}
if (request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.rewrite(new URL('/dashboard/user', request.url))
}
}
📝쿠키 사용
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Assume a "Cookie:nextjs=fast" header to be present on the incoming request
// Getting cookies from the request using the `RequestCookies` API
let cookie = request.cookies.get('nextjs')
console.log(cookie) // => { name: 'nextjs', value: 'fast', Path: '/' }
const allCookies = request.cookies.getAll()
console.log(allCookies) // => [{ name: 'nextjs', value: 'fast' }]
request.cookies.has('nextjs') // => true
request.cookies.delete('nextjs')
request.cookies.has('nextjs') // => false
// Setting cookies on the response using the `ResponseCookies` API
const response = NextResponse.next()
response.cookies.set('vercel', 'fast')
response.cookies.set({
name: 'vercel',
value: 'fast',
path: '/',
})
cookie = response.cookies.get('vercel')
console.log(cookie) // => { name: 'vercel', value: 'fast', Path: '/' }
// The outgoing response will have a `Set-Cookie:vercel=fast;path=/test` header.
return response
}
손쉽게 쿠키 조작 가능
📝헤더 설정
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Clone the request headers and set a new header `x-hello-from-middleware1`
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-hello-from-middleware1', 'hello')
// You can also set request headers in NextResponse.rewrite
const response = NextResponse.next({
request: {
// New request headers
headers: requestHeaders,
},
})
// Set a new response header `x-hello-from-middleware2`
response.headers.set('x-hello-from-middleware2', 'hello')
return response
}
손쉽게 헤더 설정 가능
📝응답 설정
import { NextRequest } from 'next/server'
import { isAuthenticated } from '@lib/auth'
// Limit the middleware to paths starting with `/api/`
export const config = {
matcher: '/api/:function*',
}
export function middleware(request: NextRequest) {
// Call our authentication function to check the request
if (!isAuthenticated(request)) {
// Respond with JSON indicating an error message
return Response.json(
{ success: false, message: 'authentication failed' },
{ status: 401 }
)
}
}
콘텐츠가 로드되는 동안 서버에서 즉시 로드 상태를 표시할 수 있습니다. 렌더링이 완료되면 새 콘텐츠가 자동으로 교체됩니다
📝즉각 로딩 상태
라우팅 폴더에 loading.tsx를 넣을 경우 하위에서 로딩이 일어났을 때 해당 tsx파일을 보여줍니다 로딩의 경우 스켈레톤 및 스피너와 같은 걸 많이 사용합니다 (참고로 로딩이 일어난 건 Fetch한 데이터를 화면에 노출시킬 때만 해당 됩니다)
📝서스펜스 로딩 (Suspense)
Suspense 컴포넌트를 이용할 수 있습니다 fallback에는 로딩 시킬 컴포넌트를 적어주고 하위 컴포넌트를 감싸주면 됩니다
Suspense다양한 이점이 있는데 SSR의 경우 위의 사진 처럼 모든 데이터를 가져온 후에 서버가 페이지의 HTML을 렌더링 할 수 있습니다 그 후 클라이언트에서 구성요소에 대한 인터렉티브한 이벤트 요소의 코드가 다운 되어야 UI의 기능을 사용할 수 있습니다(Hydration) 이럴 경우 Blokcing 처리로 지연이 될 수 있습니다
Suspense를 이용하면 페이지의 각 컴포넌트를 청크를 나눠서 서버에서 클라이언트로 보낼 수 있습니다 그렇게 되면 NonBlokcking처럼 병렬처리가 되어서 더 빠른 페이지를 제공 및 더 빠른 Hydrating이 가능해 사용자가 페이지를 빨리 이용할 수 있습니다 (스트리밍)
SSR에서는 스트리밍을 기본적으로 지원하기 때문에 적용 안 시킨 버전하고 어떤 차이가 있는지는 테스트 못 함 하지만 병렬로 요청이 들어가는 것은 확인했고 Suspense를 사용하면 각각 Loading 보여주다가 먼저 들어오는 것부터 보여준다 아마 이렇게 사용해야 청크를 나눠서 하는 것 같다 마치 CSR처럼 데이터 패치 이후에 그려지는 것처럼 보여진다 → Suspense 필수로 적용
물론 Suspense는 Client에서도 사용이 가능하고 청크를 나누진 않기 때문에 다이나믹으로 청크로 나눠서 Lazy Load를 시키면 로딩 성능을 개선 시킬 수 있다 → 서버에서는 자동으로 청크를 나누기 때문에 필요 없다고 한다
프론트 코드 (SSR로 테스트)
'use server';
/** /test/page.tsx **/
export default function Page() {
return (
<div>
<Suspense fallback={<div className="text-amber-900">Loading</div>}>
<Results num={1}/>
</Suspense>
<Suspense fallback={<div className="text-amber-900">Loading</div>}>
<Results2 num={2}/>
</Suspense>
</div>
)
}
/** Results.tsx **/
export default async function Results({num}: { num: number }) {
const now = new Date();
let year = now.getFullYear();
let month = String(now.getMonth() + 1).padStart(2, '0');
let day = String(now.getDate()).padStart(2, '0');
let hours = String(now.getHours()).padStart(2, '0');
let minutes = String(now.getMinutes()).padStart(2, '0');
let seconds = String(now.getSeconds()).padStart(2, '0');
let milliseconds = String(now.getMilliseconds()).padStart(3, '0');
const startDateTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
console.log(`[${num}] ` + startDateTime);
const getData = await fetch('<http://localhost:8080/next>', {cache: 'force-cache'});
let result = await getData.json();
const end = new Date();
year = end.getFullYear();
month = String(end.getMonth() + 1).padStart(2, '0');
day = String(end.getDate()).padStart(2, '0');
hours = String(end.getHours()).padStart(2, '0');
minutes = String(end.getMinutes()).padStart(2, '0');
seconds = String(end.getSeconds()).padStart(2, '0');
milliseconds = String(end.getMilliseconds()).padStart(3, '0');
const endDateTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
console.log(`[${num} end] ` + endDateTime);
console.log(`[${num}] interval ` + (end.getTime() - now.getTime()));
return <div>
{result.id}
</div>
}
/** Results2.tsx **/
export default async function Results2({num}: { num: number }) {
const now = new Date();
let year = now.getFullYear();
let month = String(now.getMonth() + 1).padStart(2, '0');
let day = String(now.getDate()).padStart(2, '0');
let hours = String(now.getHours()).padStart(2, '0');
let minutes = String(now.getMinutes()).padStart(2, '0');
let seconds = String(now.getSeconds()).padStart(2, '0');
let milliseconds = String(now.getMilliseconds()).padStart(3, '0');
const startDateTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
console.log(`[${num}] ` + startDateTime);
const getData = await fetch('<http://localhost:8080/next2>', {cache: 'force-cache'});
let result = await getData.json();
const end = new Date();
year = end.getFullYear();
month = String(end.getMonth() + 1).padStart(2, '0');
day = String(end.getDate()).padStart(2, '0');
hours = String(end.getHours()).padStart(2, '0');
minutes = String(end.getMinutes()).padStart(2, '0');
seconds = String(end.getSeconds()).padStart(2, '0');
milliseconds = String(end.getMilliseconds()).padStart(3, '0');
const endDateTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
console.log(`[${num} end] ` + endDateTime);
console.log(`[${num}] interval ` + (end.getTime() - now.getTime()));
return <div>
{result.id}
</div>
}
백엔드 코드
@CrossOrigin(origins = "*")
@GetMapping("/next")
public @ResponseBody HashMap<String, Object> next() throws InterruptedException {
System.out.println("next call!!");
HashMap<String, Object> map = new HashMap<String, Object>();
map.put("id", "Next 테스트 1");
Thread.sleep(6000);
return map;
}
@CrossOrigin(origins = "*")
@GetMapping("/next2")
public @ResponseBody HashMap<String, Object> next2() throws InterruptedException {
System.out.println("next2 call!!");
HashMap<String, Object> map = new HashMap<String, Object>();
map.put("id", "Next 테스트 2");
Thread.sleep(3000);
return map;
}
결과 → /next2를 호출한 곳이 먼저 그려지고 3초 후에 /next를 호출한 곳이 그려진다
📝스트리밍
스트리밍을 사용하면 서버에서 UI를 점진적으로 렌더링할 수 있습니다
작업은 여러 단위로 분할되어 준비가 되면 클라이언트로 스트리밍됩니다 이를 통해 사용자는 전체 콘텐츠의 렌더링이 완료되기 전에 페이지의 일부를 즉시 볼 수 있습니다
스트리밍은 기본적으로 Next.js 앱 라우터에 내장되어 있습니다 이는 초기 페이지 로딩 성능뿐만 아니라 전체 경로 렌더링을 차단하는 느린 데이터 가져오기에 의존하는 UI를 모두 개선하는 데 도움이 됩니다
위에 예제를 보면 /dashbaord로 URL 접근시 app/layout.tsx의 최상단 레이아웃 안에 children에 dashboard/layout.tsx가 그려지게 되고 page.tsx가 존재할 시 dashboard/layout.tsx의 children에 dashboard/page.tsx가 그려지게 된다
📝Link (페이지 연결)
import Link from 'next/link'
export default function PostList({ posts }) {
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
</li>
))}
</ul>
)
}
<a> 제공하는 내장구성요소로 Next.js에서 경로 간을 탐색하는 기본 방법이다
'use client'
import { usePathname } from 'next/navigation'
import Link from 'next/link'
export function Links() {
const pathname = usePathname()
return (
<nav>
<ul>
<li>
<Link className={`link ${pathname === '/' ? 'active' : ''}`} href="/">
Home
</Link>
</li>
<li>
<Link
className={`link ${pathname === '/about' ? 'active' : ''}`}
href="/about"
>
About
</Link>
</li>
</ul>
</nav>
)
}
서버에서 애플리케이션 코드는 경로 세그먼트 별로 자동으로 코드 분할되고 해당 경로를 미리 가져오고 캐시를 한다 즉, 페이지를 다시 로드하지 않고 변경된 세그먼트만 다시 렌더링하여 성능을 향상시킨다
사용자는 경로 방문 전에 백그라운드에서 경로를 미리 로드한다 → 예를 들면 네이버 메인페이지에 다양한 링크들이 있다 메인페이지에 들어간 후 오프라인 설정한 다음 메인페이지에 있는 링크에 들어가면 로드가 된다 (전부가 로드 되는 건 아님 캐싱에 있는 애들이나 정적으로 만들어진 애들을 로드)
📝라우팅 및 탐색 작동 방식
서버에서 애플리케이션 코드는 경로 세그먼트 별로 자동으로 코드 분할되고 해당 경로를 미리 가져오고 캐시를 한다 즉, 페이지를 다시 로드하지 않고 변경된 세그먼트만 다시 렌더링하여 성능을 향상시킨다
프리패칭 (Prefetching)
사용자는 경로 방문 전에 백그라운드에서 경로를 미리 로드한다 → 예를 들면 네이버 메인페이지에 다양한 링크들이 있다 메인페이지에 들어간 후 오프라인 설정한 다음 메인페이지에 있는 링크에 들어가면 로드가 된다 (전부가 로드 되는 건 아님 캐싱에 있는 애들이나 정적으로 만들어진 애들을 로드)
Next에서 경로를 미리 가져오는 방법은 두가지가 있다
<Link> 컴포넌트 → 뷰포트에 표시되면 자동으로 미리 가져온다
정적 경로일 경우
prefetch의 true가 기본값이고 전체 경로가 프리패치가 되며 캐시도 된다
동적 경로일 경우
prefetch의 true가 기본값이고 loading.tsx가 먼저 프리패치가 된다 → 그 이후에 패치작업을 미리하거나 하는 건 아닌 거 같음