[Docs] Next.js from Analytics to JSON-LD

백상휘·2025년 5월 19일
0

FE

목록 보기
4/5

Analytics

How to add analytics to your Next.js application

성능 측정 기능은 useReportWebVitals 훅을 사용하거나 Observability 를 사용하면 자동으로 수집된다.

Client Instrumentation

만약 더 고도화 된 분석이 필요하다면 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)
})

Build Your Own

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>
  )
}

Web Vitals

사용자의 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
      }
      // ...
    }
  })
}

Sending results to external systems

외부 시스템에 수집한 데이터를 보내는 것은 해당 시스템이 요구하는 사양을 따르도록 하자.

CI Build Caching

How to configure Continuous Integration (CI) build caching

.next/cache 는 빌드 시간을 줄이기 위해 Next.js 가 관리하는 디렉토리이다. 아래는 그 예시의 일부이며, .next/cache 관련 설정이 없다면 No Cache Detected 에러가 발생한다.

Vercel

기본 구성됨. Turborepo 를 사용한다면 링크를 참고하세요.

CircleCI

.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)

Content Security Policy

How to set a Content Security Policy (CSP) for your Next.js application

cross-site scripting (XSS), clickjacking 등의 보안위협을 막기 위해 Content Security Policy (CSP) 를 사용한다. content sources, scripts, stylesheets, images, fonts, objects, media (audio/video), iframes 등이 실행되는 시작점을 필터링한다.

Nonces

nonce 는 한 번 사용되는 유니크한 문자열로, CSP 와 같이 사용되어 특정 스크립트나 스타일들을 실행하거나 넘긴다. Next.js 버전 13.4.2 이상을 권고한다.

Why use a nonce?

CSP 가 악성 스크립트를 방지하더라도, 그 스크립트를 실행해야 하는 경우가 있습니다. 만약 적절한 nonce 를 사용한다면 이런 상황을 허용시킨다.

Adding a nonce with Middleware

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' },
      ],
    },
  ],
};

Reading the nonce

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}
    />
  )
}

Without Nonces

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, ''),
          },
        ],
      },
    ]
  },
}

CSS-in-JS

How to use CSS-in-JS libraries

app 디렉토리에 Client Component 에 적용할 수 있는 CSS 라이브러리들은 다음과 같다.

  • ant-design
  • chakra-ui
  • @fluentui/react-components
  • kuma-ui
  • @mui/material
  • @mui/joy
  • pandaces
  • styled-jsx
  • styled-components
  • stylex
  • tamagui
  • tss-react
  • vanilla-extract

Configuring CSS-in-JS in app

CSS-in-JS 를 효율적으로 적용하기 위해서는

  1. CSS 규칙을 렌더링할 때 수집할 수 있는 style-registry
  2. useServerInsertedHTML 훅을 이용해서 컨텐츠 사용 전에 규칙을 삽입한다.
  3. 초기 서버 측 렌더링 중에 style-registry 로 앱을 래핑하는 클라이언트 구성 요소

styled-jsx

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 Components

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>
  )
}

Custom Server

How to set up a custom server in Next.js

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"
  }
}

Debugging

Skip

Draft Mode

Skip

Environment Variables

How to use environment variables in Next.js

다른 프레임워크와 다르게 Next.js 는 Build-in 된 .env 활용방식이 있다.

Loading Environment Variables

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,
  })
}

Loading Environment Variables with @next/env

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!,
  },
})

Referencing Other Variables

$ 기호를 이용해서 환경변수 파일 내에서 한 변수가 다른 변수를 참조할 수 있다.

TWITTER_USER=nextjs
TWITTER_URL=https://x.com/$TWITTER_USER

Bundling Environment Variables for the Browser

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)

Runtime Environment Variables

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
  // ...
}

만약 환경변수가 도커 이미지로 배포되고 있다면 이 방법이 좋다.

Test Environment Variables

.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)
}

Environment Variable Load Order

환경변수는 아래 순서대로 값을 검색한다.

  1. process.env
  2. .env.$(NODE_ENV).local
  3. .env.local (test 에선 안함)
  4. .env.$(NODE_ENV)
  5. .env

예를 들어 NODE_ENV 는 development 라면 환경변수는 .env.development.local 과 .env 그리고 .env.development.local 가 쓰일 것이다.

Instrumentation

instrumentation 은 성능 및 이슈 검사를 위한 기능이다.

Convention

instrumentation.ts 파일을 루트 디렉토리에 만든다(src 폴더를 쓴다면 src 폴더 안에 넣는다). 그리고 register 함수를 export 한다. 이 함수는 앱에서 한 번만 실행될 것이다. 아래는 OpenTelemetry 를 사용한 것이다.

import { registerOTel } from '@vercel/otel'
 
export function register() {
  registerOTel('next-app')
}

Examples

읽어보긴 했는데 무슨 말인지 잘 모르겠음

JSON-LD

How to implement JSON-LD in your Next.js application

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>
  )
}
profile
plug-compatible programming unit

0개의 댓글