성능 측정 기능은 useReportWebVitals 훅을 사용하거나 Observability 를 사용하면 자동으로 수집된다.
만약 더 고도화 된 분석이 필요하다면 intrumentation-client.ts
파일을 루트 디렉토리에 생성한다. 이 파일은 FE 코드 중 가장 먼저 실행된다.
실제 사용했다는 사람이나 사례가 나오지 않는다. 잘 모르겠는데...
// Initialize analytics before the app starts
console.log('Analytics initialized')
// Set up global error tracking
window.addEventListener('error', (event) => {
// Send to your error tracking service
reportError(event.error)
})
useReportWebVitals
를 사용하려면 'use client'
디렉티브가 필요하므로 Root layout 이 컴포넌트화 된 useReportWebVitals
를 임포트 하면 된다.
'use client'
import { useReportWebVitals } from 'next/web-vitals'
export function WebVitals() {
useReportWebVitals((metric) => {
console.log(metric)
})
}
import { WebVitals } from './_components/web-vitals'
export default function Layout({ children }) {
return (
<html>
<body>
<WebVitals />
{children}
</body>
</html>
)
}
사용자의 UX 경험을 수집한다.
위의 약자를 name 프로퍼티에 넣도록 하자.
'use client'
import { useReportWebVitals } from 'next/web-vitals'
export function WebVitals() {
useReportWebVitals((metric) => {
switch (metric.name) {
case 'FCP': {
// handle FCP results
}
case 'LCP': {
// handle LCP results
}
// ...
}
})
}
외부 시스템에 수집한 데이터를 보내는 것은 해당 시스템이 요구하는 사양을 따르도록 하자.
.next/cache
는 빌드 시간을 줄이기 위해 Next.js 가 관리하는 디렉토리이다. 아래는 그 예시의 일부이며, .next/cache
관련 설정이 없다면 No Cache Detected 에러가 발생한다.
기본 구성됨. Turborepo 를 사용한다면 링크를 참고하세요.
.circleci/config.yml
을 아래와 같이 설정하여 .next/cache
를 생성하도록 한다. 아래 보이는 save_cache 가 없으면 링크 참조.
steps:
- save_cache:
key: dependency-cache-{{ checksum "yarn.lock" }}
paths:
- ./node_modules
- ./.next/cache
나머지는 각자 확인하면 되므로 목록만 나열함.
- Travis CI
- GitLab CI
- Netlify CI
- AWS CodeBuild
- GitHub Actions
- Bitbucket Pipelines
- Heroku
- Azure Pipelines
- Jenkins (Pipeline)
cross-site scripting (XSS), clickjacking 등의 보안위협을 막기 위해 Content Security Policy (CSP) 를 사용한다. content sources, scripts, stylesheets, images, fonts, objects, media (audio/video), iframes 등이 실행되는 시작점을 필터링한다.
nonce 는 한 번 사용되는 유니크한 문자열로, CSP 와 같이 사용되어 특정 스크립트나 스타일들을 실행하거나 넘긴다. Next.js 버전 13.4.2 이상을 권고한다.
CSP 가 악성 스크립트를 방지하더라도, 그 스크립트를 실행해야 하는 경우가 있습니다. 만약 적절한 nonce 를 사용한다면 이런 상황을 허용시킨다.
Middleware 를 이용해서 헤더를 추가하고 nonce 를 페이지 렌더링 전에 생성한다. 페이지 렌더링마다 새로운 nonce 생성이 필요하므로 dynamic rendering 이 필요하다.
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
style-src 'self' 'nonce-${nonce}';
img-src 'self' blob: data:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`;
// Replace newline characters and spaces
const contentSecurityPolicyHeaderValue = cspHeader
.replace(/\s{2,}/g, ' ')
.trim();
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-nonce', nonce);
requestHeaders.set(
'Content-Security-Policy',
contentSecurityPolicyHeaderValue
);
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
});
response.headers.set(
'Content-Security-Policy',
contentSecurityPolicyHeaderValue
);
return response;
}
기본적으로 Middleware 는 모든 리퀘스트에 관여한다. matcher 를 사용하면 특정 상황에서만 Middleware 를 사용하도록 할 수 있다. CSP 가 필요없는 경우에 대한 대응도 필요하다.
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
{
source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
missing: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
},
],
};
Server Components 를 headers 를 이용해서 읽을 수 있다.
import { headers } from 'next/headers';
import Script from 'next/script';
export default async function Page() {
const nonce = (await headers()).get('x-nonce');
return (
<Script
src="https://www.googletagmanager.com/gtag/js"
strategy="afterInteractive"
nonce={nonce}
/>
)
}
nonce 가 필요없는 앱이라면 CSP 헤더를 직접 next.config.ts
파일에 넣으면 된다.
const cspHeader = `
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' blob: data:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`;
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: cspHeader.replace(/\n/g, ''),
},
],
},
]
},
}
app 디렉토리에 Client Component 에 적용할 수 있는 CSS 라이브러리들은 다음과 같다.
CSS-in-JS 를 효율적으로 적용하기 위해서는
style-registry
useServerInsertedHTML
훅을 이용해서 컨텐츠 사용 전에 규칙을 삽입한다.style-registry
로 앱을 래핑하는 클라이언트 구성 요소styled-jsx 5.1.0 이상의 버전이 필요하다. 우선 registry 를 등록한다.
'use client'
import React, { useState } from 'react'
import { useServerInsertedHTML } from 'next/navigation'
import { StyleRegistry, createStyleRegistry } from 'styled-jsx'
export default function StyledJsxRegistry({
children,
}: {
children: React.ReactNode
}) {
// Only create stylesheet once with lazy initial state
// x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
const [jsxStyleRegistry] = useState(() => createStyleRegistry())
useServerInsertedHTML(() => {
const styles = jsxStyleRegistry.styles()
jsxStyleRegistry.flush()
return <>{styles}</>
})
return <StyleRegistry registry={jsxStyleRegistry}>{children}</StyleRegistry>
}
그리고 Root Layout 을 감싼다.
import StyledJsxRegistry from './registry'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<StyledJsxRegistry>{children}</StyledJsxRegistry>
</body>
</html>
)
}
styled-component 6버전 이상을 사용한다. 우선 next.config.js
를 수정한다.
module.exports = {
compiler: {
styledComponents: true,
},
}
그리고 styled-components API 를 이용해 global registry 컴포넌트를 만든다. 이 컴포넌트는 CSS 를 렌더링할 때 생성한다. useServerInsertedHTML 훅을 이용해서 registry 에 의해 수집된 스타일을 Root Layout 내 head 태그에 추가한다.
'use client'
import React, { useState } from 'react'
import { useServerInsertedHTML } from 'next/navigation'
import { ServerStyleSheet, StyleSheetManager } from 'styled-components'
export default function StyledComponentsRegistry({
children,
}: {
children: React.ReactNode
}) {
// Only create stylesheet once with lazy initial state
// x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet())
useServerInsertedHTML(() => {
const styles = styledComponentsStyleSheet.getStyleElement()
styledComponentsStyleSheet.instance.clearTag()
return <>{styles}</>
})
if (typeof window !== 'undefined') return <>{children}</>
return (
<StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
{children}
</StyleSheetManager>
)
}
import StyledComponentsRegistry from './lib/registry'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<StyledComponentsRegistry>{children}</StyledComponentsRegistry>
</body>
</html>
)
}
Next.js 는 기본적으로 next start
를 통해 자체 서버를 가질 수 있다. 만약 자체 백엔드 서버가 있다면 이를 이용해 새로운 실행방식을 쓸 수 있다. 대체적으로 필요하진 않지만 필요하다면 쓸 수도 있다.
아래는 custom server 의 예시다. (Express 와 비슷?)
import { createServer } from 'http'
import { parse } from 'url'
import next from 'next'
const port = parseInt(process.env.PORT || '3000', 10)
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
app.prepare().then(() => {
createServer((req, res) => {
const parsedUrl = parse(req.url!, true)
handle(req, res, parsedUrl)
}).listen(port)
console.log(
`> Server listening at http://localhost:${port} as ${
dev ? 'development' : process.env.NODE_ENV
}`
)
})
그리고 package.json
을 다음과 같이 수정한다.
{
"scripts": {
"dev": "node server.js",
"build": "next build",
"start": "NODE_ENV=production node server.js"
}
}
Skip
Skip
다른 프레임워크와 다르게 Next.js 는 Build-in 된 .env 활용방식이 있다.
Node.js 내장객체를 사용하는 방법이다. 먼저 아래와 같은 .env 를 가정한다.
DB_HOST=localhost
DB_USER=myuser
DB_PASS=mypassword
# you can write with line breaks
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
...
Kh9NV...
...
-----END DSA PRIVATE KEY-----"
# or with `\n` inside double quotes
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nKh9NV...\n-----END DSA PRIVATE KEY-----\n"
src 폴더를 사용한다면, .env 파일이 꼭 src 폴더를 나타내지 않는다는 사실에 주의하라. src 폴더가 있더라도 .env 파일은 root directory 에 존재해야 한다.
그럼 아래와 같이 사용할 수 있다.
export async function GET() {
const db = await myDB.connect({
host: process.env.DB_HOST,
username: process.env.DB_USER,
password: process.env.DB_PASS,
})
}
Next.js 런타임 외에 ORM 혹은 root config 파일 혹은 test runner 를 위해 환경변수를 불러오려면 @next/env
를 사용하는 것을 고려해보자.
npm install @next/env
// envConfig.ts
import { loadEnvConfig } from '@next/env'
const projectDir = process.cwd()
loadEnvConfig(projectDir)
// orm.config.ts
import './envConfig.ts'
export default defineConfig({
dbCredentials: {
connectionString: process.env.DATABASE_URL!,
},
})
$ 기호를 이용해서 환경변수 파일 내에서 한 변수가 다른 변수를 참조할 수 있다.
TWITTER_USER=nextjs
TWITTER_URL=https://x.com/$TWITTER_USER
Node 환경이 아닌 경우는 빌드할 때 터미널에 직접 입력할 수 있다.
NEXT_PUBLIC_ANALYTICS_ID=abcdefghijk
이건 코드 내에 process.env.NEXT_PUBLIC_ANALYTICS_ID
가 참조할 값을 설정한다. 여기서 말한 코드는 next build
로 생성된 코드를 말한다. 빌드가 된 코드는 더 이상의 변경이 되지 않으므로 위의 명령어는 빌드 전에 실행해야 한다.
import setupAnalyticsService from '../lib/my-analytics-service'
// setupAnalyticsService('abcdefghijk') 와 같다.
setupAnalyticsService(process.env.NEXT_PUBLIC_ANALYTICS_ID)
export default function HomePage() {
return <h1>Hello World</h1>
}
아래와 같이 동적으로 참조하는 해당 안되니 참고해야 한다.
// 변수 형태로 쓰면 안된다 1.
const varName = 'NEXT_PUBLIC_ANALYTICS_ID'
setupAnalyticsService(process.env[varName])
// 변수 형태로 쓰면 안된다 2.
const env = process.env
setupAnalyticsService(env.NEXT_PUBLIC_ANALYTICS_ID)
Next.js 는 빌드 혹은 런타임 시 환경변수 모두 지원한다. 기본적으로 환경변수는 서버(빌드 타임을 말하는 듯)에서만 사용 가능하다. 서버에서 사용할 때는 NEXT_PUBLIC_
접두사가 필요하다. 이 접두사가 들어간 환경변수는 빌드 시 Javascript 번들링이 되어야 한다.
서버를 통해 환경변수를 읽으려면 이 방법을 쓰면 된다.
import { connection } from 'next/server'
export default async function Component() {
await connection()
// cookies, headers, and other Dynamic APIs
// will also opt into dynamic rendering, meaning
// this env variable is evaluated at runtime
const value = process.env.MY_VALUE
// ...
}
만약 환경변수가 도커 이미지로 배포되고 있다면 이 방법이 좋다.
.env.test
파일을 이용하면 testing 환경의 환경변수도 대응 가능하다. 테스트용 값은 NODE_ENV
가 test 에 설정되어 있어야 한다. test 환경에서는 .env.local
을 불러오지 않음을 주의하자.
유닛 테스트에서 환경변수를 불러오는 방법이다.
// The below can be used in a Jest global setup file or similar for your testing set-up
import { loadEnvConfig } from '@next/env'
export default async () => {
const projectDir = process.cwd()
loadEnvConfig(projectDir)
}
환경변수는 아래 순서대로 값을 검색한다.
예를 들어 NODE_ENV 는 development 라면 환경변수는 .env.development.local 과 .env 그리고 .env.development.local 가 쓰일 것이다.
instrumentation 은 성능 및 이슈 검사를 위한 기능이다.
instrumentation.ts
파일을 루트 디렉토리에 만든다(src 폴더를 쓴다면 src 폴더 안에 넣는다). 그리고 register 함수를 export 한다. 이 함수는 앱에서 한 번만 실행될 것이다. 아래는 OpenTelemetry 를 사용한 것이다.
import { registerOTel } from '@vercel/otel'
export function register() {
registerOTel('next-app')
}
읽어보긴 했는데 무슨 말인지 잘 모르겠음
JSON-LD 는 검색 엔진이나 AI 에서 사용되어 순수한 형태의 데이터를 구조화 하는 것이다. 추천하는 JSON-LD 는 layout.ts 혹은 page.ts 컴포넌트에 script 태그 형태로 렌더링 되는 것이다.
export default async function Page({ params }) {
const { id } = await params
const product = await getProduct(id)
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
image: product.image,
description: product.description,
}
return (
<section>
{/* Add JSON-LD to your page */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
{/* ... */}
</section>
)
}