Next 데이터패칭

최성훈·2024년 4월 13일

Next

목록 보기
2/3
post-thumbnail

데이터 캐싱

데이터 캐싱이란?

캐싱은 데이터를 저장하므로 요청이 있을때마다 데이터를 다시 받아올 필요가 없습니다.

Next는 기본적으로 반환된 값을 자동으로 데이터 캐시에 캐시합니다.

-> 그 말은 빌드 또는 요청 시 캐시 값을 재사용 할 수 있음을 의미합니다.

예외

다음과 같은 경우는 fetch 요청이 캐시되지 않습니다.

  • server action 내에서 사용될 때
  • Router handler 안에서 Post 메소드로 사용될 때

데이터 재검증

데이터 재검증이란?

데이터 캐시를 제거하고 최신 데이터를 다시 받아오는 프로세스 입니다.

이는 데이터가 변경되어 최신 정보를 표시하려는 경우에 유용합니다.

재검증 방법

  1. 시간 기반 재검증
    일정 시간마다 재검증 합니다. 이는 자주 변경되지 않고 최신값 변경이 중요하지 않은 데이터에 유용합니다.

  2. 온디맨드 재검증
    이벤트 기반으로 재검증합니다. 이는 태그기반 또는 경로기반 접근방식을 이용하여 재검증 할 수 있습니다. 가능한 빨리 최신값을 표시하려는 경우에 유용합니다.

시간 기반 재검증

시간 간격을 두고 데이터를 재검증하려면 fetch의 next.revalidate 옵션을 사용하여 리소스의 캐시 시간을 설정할 수 있습니다.

fetch('https://...', { next: { revalidate: 3600 } })

또는 라우트 세그먼트의 모든 response를 재검증하기 위해 세그먼트 구성 옵션을 사용할 수 있습니다.

export const revalidate = 3600 // revalidate at most every hour

정적으로 렌더링된 경로에 여러 fetch response가 있고 각각의 재검증 빈도가 다른 경우, 모든 요청에 가장 낮은 시간이 사용됩니다. 동적으로 랜더링되는 라우트의 경우 각 fetch 요청은 독립적으로 재검증됩니다.

온디맨드 재검증

라우트 핸들러 또는 서버 액션 내부의 라우트 또는 캐시 태그로 데이터를 온디맨드 방식으로 재검증할 수 있습니다.

Next.js에는 경로 전반에 걸쳐 fetch요청을 무효화하기 위한 캐시 태깅 시스템이 있습니다.

  1. fetch 를 사용할 때 하나 이상의 태그로 캐시 항목에 태그를 지정할 수 있는 옵션이 있습니다.
  2. 그런 다음 revalidateTag를 호출하여 해당 태그와 관련된 모든 항목의 유효성을 다시 검사할 수 있습니다.

예를 들어 다음 fetch 요청은 collection 캐시 태그를 추가합니다.

export default async function Page() {
  const res = await fetch('https://...', { next: { tags: ['collection'] } })
  const data = await res.json()
  // ...
}

그런 다음 서버 작업에서 revalidateTag를 호출하여 collection 태그가 지정된 이 fetch 호출을 다시 검증할 수 있습니다.

'use server'
 
import { revalidateTag } from 'next/cache'
 
export default async function action() {
  revalidateTag('collection')
}

오류 밑 재검증

데이터 재검증을 시도하는 동안 오류가 발생하면 마지막으로 성공적으로 생성된 데이터가 캐시에서 계속 제공됩니다. 다음 후속 요청에서 Next.js는 데이터 재검증을 다시 시도합니다.

데이터 캐싱 선택 해제

다음과 같은 경우 fetch 요청이 캐시되지 않습니다.

  • cache: 'no-store'이 fetch 요청에 추가 됩니다.
  • 옵션은 revalidate: 0개별 fetch 요청에 추가됩니다.
  • fetch 요청은 해당 메소드를 사용하는 라우터 핸들러 POST 안에 있습니다.
  • fetch 요청은 headers 또는 cookies 를 사용한 후에 발생합니다.
  • 경로 const dynamic = 'force-dynamic'세그먼트 옵션이 사용됩니다.
  • fetchCache의 경로 세그먼트 옵션은 기본적으로 캐시를 건너뛰도록 구성됩니다.
  • fetch 요청이 Authorization 또는 Cookie 헤더를 사용 하고 구성 요소 트리의 그 위에 캐시되지 않은 요청이 있습니다.

개별 fetch 요청

개별 fetch 요청에 대한 캐싱을 선택 해제하려면 cache에서 'no-store' 옵션을 설정하면 됩니다. 그러면 fetch 요청이 있을 때마다 데이터를 동적으로 가져옵니다.

fetch('https://...', { cache: 'no-store' })

다중 fetch 요청

경로 세그먼트(예: 레이아웃 또는 페이지)에 여러 fetch 요청이 있는 경우 세그먼트 구성 옵션을 사용하여 세그먼트에 있는 모든 데이터 요청의 캐싱 동작을 구성할 수 있습니다.

그러나 각 fetch 요청의 캐싱 동작을 개별적으로 구성하는 것이 좋습니다. 이를 통해 캐싱 동작을 보다 세부적으로 제어할 수 있습니다.

서드파티 라이브러리를 이용하여 서버에서 데이터 받아오기

지원하지 않거나 대중화되지 않은 다른 라이브러리(예: 데이터베이스, CMS 또는 ORM 클라이언트)를 사용하는 경우 Route 세그먼트 구성 옵션을 사용하여 해당 fetch 요청의 캐싱 및 재검증 동작을 구성할 수 있습니다.

데이터가 캐시되는지의 여부는 경로 세그먼트가 정적으로 렌더링되는지 아니면 동적으로 렌더링되는지에 따라 달라집니다. 세그먼트가 정적(기본값)인 경우 요청 출력은 경로 세그먼트의 일부로 캐시되고 재검증됩니다. 세그먼트가 동적인 경우 요청 출력은 캐시되지 않으며 세그먼트가 렌더링될 때 모든 요청에서 다시 가져옵니다.

unstable_cache 실험용 API를 사용할 수도 있습니다.

Route Handler를 이용하여 클라이언트에서 데이터 가져오기

클라이언트 구성 요소에서 데이터를 가져와야 하는 경우 클라이언트에서 Route Handler를 호출할 수 있습니다. 경로 처리기는 서버에서 실행되고 데이터를 클라이언트에 반환합니다. 이는 API 토큰과 같은 민감한 정보를 클라이언트에 노출하고 싶지 않을 때 유용합니다.

서드파티 라이브러리를 사용하여 클라이언트에서 데이터 가져오기

SWR이나 TanStack Query 같은 서드파티 라이브러리를 사용하여 클라이언트에서 데이터를 가져올 수도 있습니다. 이러한 라이브러리는 요청 메모, 캐싱, 재검증 및 데이터 변형을 위한 자체 API를 제공합니다.

서버액션 및 뮤테이션

서버액션은 서버에서 실행되는 비동기 함수 입니다. 서버 및 클라이언트 구성 요소에서 Next.js 애플리케이션의 제출 폼 및 데이터 뮤테이션을 처리하는 데 사용할 수 있습니다.

컨벤션

서버 액션은 React로 정의될 수 있습니다. 함수의 상단에 async를 배치하여 해당 기능을 서버 작업으로 표시하거나 별도의 파일 상단에 배치하여 해당 파일의 모든 내보내기를 서버 작업으로 표시할 수 있습니다.

서버 컴포넌트

서버 컴포넌트는 인라인 함수 또는 모듈에 "use server"를 사용할 수 있습니다.

서버 작업을 인라인하려면 "use server"함수 본문 상단에 다음과 같은 코드를 추가하면 됩니다.

// Server Component
export default function Page() {
  // Server Action
  async function create() {
    'use server'
 
    // ...
  }
 
  return (
    // ...
  )
}

클라이언트 컴포넌트

클라이언트 컴포넌트는 "use server"을 사용하는 모듈만 불러올 수 있습니다.
클라이언트 컴포넌트에서 서버작업을 호출하려면 새 파일을 만들고 코드 상단에 "use server"를 추가하면 됩니다. 그러면 파일 내의 모든 기능은 클라이언트 및 서버 컴포넌트에서 재사용할 수 있는 서버 작업으로 표시됩니다.

'use server'
 
export async function create() {
  // ...
}

서버액션을 prop으로 클라이언트 컴포넌트에 전달할 수 있습니다.

'use client'
 
export default function ClientComponent({ updateItem }) {
  return <form action={updateItem}>{/* ... */}</form>
}

동작

  • 서버 작업은 form의 action속성을 사용하여 호출할 수 있습니다.
    - 서버 컴포넌트는 기본적으로 점진적인 향상을 지원합니다. 즉, JavaScript가 아직 로드되지 않았거나 비활성화된 경우에도 양식이 제출됩니다.
    - 클라이언트 컴포넌트에서 서버 액션을 호출하는 양식은 JavaScript가 아직 로드되지 않은 경우 제출을 대기열에 넣어 클라이언트 하이드레이션의 우선 순위를 지정합니다.
    - 하이드레이션 후 양식 제출 시 브라우저가 새로 고쳐지지 않습니다.
  • 서버 액션은 이벤트 핸들러,useEffect, 서드파티 라이브러리 및 기타 양식 요소(예: button tag) 에 제한되지 않고 호출될 수 있습니다.
  • 서버 액션은 Next.js 캐싱 및 재검증 아키텍처와 통합됩니다. 작업이 호출되면 Next.js는 단일 서버 왕복으로 업데이트된 UI와 새 데이터를 모두 반환할 수 있습니다.
  • 뒤에서는 액션이 POST메서드를 사용하며 이 HTTP 메서드만 호출할 수 있습니다.
  • 서버 액션의 인수와 반환 값은 React에서 직렬화 가능해야 합니다. 직렬화 가능한 인수 및 값 목록은 React 문서를 참조하세요
  • 서버 액션은 함수입니다. 즉, 애플리케이션의 어느 곳에서나 재사용할 수 있습니다.
  • 서버 액션은 사용되는 페이지나 레이아웃에서 런타임을 상속합니다.
  • 서버 액션이나 maxDuration 같은 필드를 포함하여 사용되는 페이지나 레이아웃에서 경로 세그먼트 구성을 상속합니다.

예시

form

React는 HTML form prop을 사용하여 서버 작업을 호출할 수 있도록 하는 요소를 확장합니다.
When invoked in a form, the action automatically receives the FormData object.
만약 form을 호출한다면 동작은 자동으로 FormData 객체를 반환합니다. 필드를 관리하기 위해 useState를 쓸 필요는 없으며 대신에 기본 메서드인 FormData를 사용하여 데이터를 추출할 수 있습니다.

export default function Page() {
  async function createInvoice(formData: FormData) {
    'use server'
 
    const rawFormData = {
      customerId: formData.get('customerId'),
      amount: formData.get('amount'),
      status: formData.get('status'),
    }
 
    // mutate data
    // revalidate cache
  }
 
  return <form action={createInvoice}>...</form>
}

추가 인수 전달

JavaScript bind 메소드를 사용하여 서버 작업에 추가 인수를 전달할 수 있습니다

'use client'
 
import { updateUser } from './actions'
 
export function UserProfile({ userId }: { userId: string }) {
  const updateUserWithId = updateUser.bind(null, userId)
 
  return (
    <form action={updateUserWithId}>
      <input type="text" name="name" />
      <button type="submit">Update User Name</button>
    </form>
  )
}

userId 서버액션은 FormData 외에도 인수를 받습니다.

'use server'
 
export async function updateUser(userId, formData) {
  // ...
}

보류 상태

React useFormStatusf 를 사용할 수 있습니다. 이것은 Form이 제출되는 동안 보류상태를 표시하는 hook 입니다.

  • useFormStatus 는 form 의 특성에 대한 상태를 반환하므로 form 요소의 하위로 정의되어야 합니다 .
  • useFormStatusReact React hook 이므로 클라이언트 컴포넌트에서 사용해야 합니다.
'use client'
 
import { useFormStatus } from 'react-dom'
 
export function SubmitButton() {
  const { pending } = useFormStatus()
 
  return (
    <button type="submit" disabled={pending}>
      Add
    </button>
  )
}

그러고 SubmitButton은 어떤 형태로 중첩될 수 있습니다.

import { SubmitButton } from '@/app/submit-button'
import { createItem } from '@/app/actions'
 
// Server Component
export default async function Home() {
  return (
    <form action={createItem}>
      <input type="text" name="field-name" />
      <SubmitButton />
    </form>
  )
}

서버측 유효성 검사 및 오류 처리

기본 클라이언트 측 양식 유효성 검사 type="email",required와 같은 HTML 유효성 검사를 사용하는 것이 좋습니다.

낙관적인 업데이트

React useOptimistic 를 사용할 수 있습니다. 응답을 기다리지 않고 서버 작업이 완료되기 전에 UI를 낙관적으로 업데이트하려면 hook를 사용하세요.

중첩된 요소

button, input type="submit" 및 input type="image" 와 같은 요소는 form 내부에 중첩된 요소에서 서버 작업을 호출할 수 있습니다. 이러한 요소는 formAction prop 또는 이벤트 핸들러를 허용합니다.

이는 양식 내에서 여러 서버 작업을 호출하려는 경우에 유용합니다. 예를 들어 게시물 초안을 게시하는 것 외에도 저장하기 위한 특정 button 요소를 만들 수 있습니다.

프로그래밍 방식의 양식 제출

requestSubmit() 메소드를 사용하여 양식 제출을 트리거할 수 있습니다. 예를 들어 사용자가 ⌘ + Enter를 누르면 onKeyDown 이벤트를 수신할 수 있습니다.

'use client'
 
export function Entry() {
  const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
    if (
      (e.ctrlKey || e.metaKey) &&
      (e.key === 'Enter' || e.key === 'NumpadEnter')
    ) {
      e.preventDefault()
      e.currentTarget.form?.requestSubmit()
    }
  }
 
  return (
    <div>
      <textarea name="entry" rows={20} required onKeyDown={handleKeyDown} />
    </div>
  )
}

그러면 가장 가까운 form 상위 항목의 제출이 트리거되어 서버 작업이 호출됩니다.

비형식 요소

form 요소 내에서 서버 작업을 사용하는 것이 일반적이지만 이벤트 핸들러 및 useEffect와 같은 코드의 다른 부분에서도 호출할 수 있습니다.

이벤트 핸들러

onClick과 같은 이벤트 핸들러에서 서버 작업을 호출할 수 있습니다.

UX를 개선하려면 useOptimistic 및 useTransition과 같은 다른 React API를 사용하여 서버 작업이 서버에서 실행을 마치기 전에 UI를 업데이트하거나 보류 중인 상태를 표시하는 것이 좋습니다.

예를 들어 form 필드를 저장하기 위해 form 요소에 이벤트 핸들러를 추가할 수도 있습니다.

'use client'
 
import { publishPost, saveDraft } from './actions'
 
export default function EditPost() {
  return (
    <form action={publishPost}>
      <textarea
        name="content"
        onChange={async (e) => {
          await saveDraft(e.target.value)
        }}
      />
      <button type="submit">Publish</button>
    </form>
  )
}

이와 같이 여러 이벤트가 연속적으로 빠르게 실행될 수 있는 경우 불필요한 서버 작업 호출을 방지하기 위해 디바운싱을 권장합니다.


useEffect

React useEffect hook을 사용하면 구성 요소가 마운트되거나 종속성이 변경될 때 서버 작업을 호출할 수 있습니다. 이는 전역 이벤트에 의존하거나 자동으로 트리거되어야 하는 변형에 유용합니다. 예를 들어 앱 바로가기를 위한 onKeyDown, 무한 스크롤을 위한 교차 관찰자 후크 또는 보기 수를 업데이트하기 위해 구성요소가 마운트되는 경우:

'use client'
 
import { incrementViews } from './actions'
import { useState, useEffect } from 'react'
 
export default function ViewCount({ initialViews }: { initialViews: number }) {
  const [views, setViews] = useState(initialViews)
 
  useEffect(() => {
    const updateViews = async () => {
      const updatedViews = await incrementViews()
      setViews(updatedViews)
    }
 
    updateViews()
  }, [])
 
  return <p>Total Views: {views}</p>
}

useEffect의 동작과 주의사항을 고려해야 합니다.


오류처리

오류가 발생하면 클라이언트에서 가장 가까운 error.js 또는 Suspense 경계에 포착됩니다. UI에서 처리할 오류를 반환하려면 try/catch를 사용하는 것이 좋습니다.

예를 들어, 서버 작업은 다음 메시지를 반환하여 새 항목을 생성할 때 발생하는 오류를 처리할 수 있습니다.

'use server'
 
export async function createTodo(prevState: any, formData: FormData) {
  try {
    // Mutate data
  } catch (e) {
    throw new Error('생성에 실패하였습니다')
  }
}

데이터 재검증

revalidatePath API를 사용하여 서버 작업 내에서 Next.js 캐시의 유효성을 다시 검사할 수 있습니다.

'use server'
 
import { revalidatePath } from 'next/cache'
 
export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }
 
  revalidatePath('/posts')
}

또는 revalidateTag를 사용하여 캐시 태그로 특정 데이터 가져오기를 무효화합니다.

'use server'
 
import { revalidateTag } from 'next/cache'
 
export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }
 
  revalidateTag('posts')
}

리다이렉팅

서버 작업 완료 후 사용자를 다른 경로로 리디렉션하려면 리디렉션 API를 사용할 수 있습니다. 리디렉션은 try/catch 블록 외부에서 호출되어야 합니다.

'use server'
 
import { redirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'
 
export async function createPost(id: string) {
  try {
    // ...
  } catch (error) {
    // ...
  }
 
  revalidateTag('posts') // Update cached posts
  redirect(`/post/${id}`) // Navigate to the new post page
}

쿠키

cookies API를 사용하여 서버 작업 내부에서 쿠키를 가져오고, 설정하고, 삭제할 수 있습니다.

'use server'
 
import { cookies } from 'next/headers'
 
export async function exampleAction() {
  // Get cookie
  const value = cookies().get('name')?.value
 
  // Set cookie
  cookies().set('name', 'Delba')
 
  // Delete cookie
  cookies().delete('name')
}

보안

인증 및 승인

공개 API 엔드포인트와 마찬가지로 서버 작업을 처리해야 하며 사용자에게 작업을 수행할 권한이 있는지 확인해야 합니다.

'use server'
 
import { auth } from './lib'
 
export function addItem() {
  const { user } = auth()
  if (!user) {
    throw new Error('You must be signed in to perform this action')
  }
 
  // ...
}

폐쇄 및 암호화

구성 요소 내부에 서버 작업을 정의하면 해당 작업이 외부 함수 범위에 액세스할 수 있는 클로저가 생성됩니다. 예를 들어 게시 작업은 publishVersion 변수에 액세스할 수 있습니다.

'use client'
export default function Page() {
  const publishVersion = await getLatestVersion();
 
  async function publish(formData: FormData) {
    "use server";
    if (publishVersion !== await getLatestVersion()) {
      throw new Error('The version has changed since pressing publish');
    }
    ...
  }
 
  return <button onClick={publish}>Publish</button>;
}

클로저는 렌더링 시점의 데이터를 캡처하여 나중에 사용할 수 있게 해주는데, 이를 보안상의 이유로 Next.js가 자동으로 암호화합니다. 새로운 개인 키가 각 액션마다 생성되며, 이는 특정 빌드에서만 액션을 호출할 수 있도록 합니다.

암호화 키 덮어쓰기(고급)

여러 서버에 걸쳐 Next.js 애플리케이션을 자체 호스팅하는 경우 각 서버 인스턴스는 서로 다른 암호화 키로 종료되어 잠재적인 불일치가 발생할 수 있습니다.

이를 완화하려면 process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY 환경 변수를 사용하여 암호화 키를 덮어쓸 수 있습니다. 이 변수를 지정하면 암호화 키가 빌드 전반에 걸쳐 지속되고 모든 서버 인스턴스가 동일한 키를 사용하게 됩니다.

이는 여러 배포에서 일관된 암호화 동작이 애플리케이션에 중요한 고급 사용 사례입니다. 키 순환 및 서명과 같은 표준 보안 관행을 고려해야 합니다.

허용되는 출처(고급)

서버 작업은 CSRF 공격에 노출될 수 있으므로 POST 메서드만 허용됩니다. 이는 SameSite 쿠키와 함께 사용되어 대부분의 CSRF 취약성을 방지합니다. Next.js의 서버 작업은 Origin 헤더를 Host 헤더와 비교하여 요청을 거부합니다. 역방향 프록시를 사용하는 애플리케이션의 경우 serverActions.allowedOrigins 옵션을 사용하여 안전한 원본을 명시적으로 지정할 수 있습니다.

/** @type {import('next').NextConfig} */
module.exports = {
  experimental: {
    serverActions: {
      allowedOrigins: ['my-proxy.com', '*.my-proxy.com'],
    },
  },
}

패턴 및 모범 사례

React 및 Next.js에서 데이터를 가져오기 위한 몇 가지 권장 패턴과 모범 사례가 있습니다. 아래에서는 이에 대한 내용을 다루어보겠습니다.


서버에서 데이터 가져오기

가능하다면 서버 구성요소를 사용하여 서버에서 데이터를 가져오는 것이 좋습니다. 이를 통해 다음을 수행할 수 있습니다.

  • 백엔드 데이터 리소스(예: 데이터베이스)에 직접 액세스할 수 있습니다.

  • 액세스 토큰, API 키 등 민감한 정보가 클라이언트에 노출되는 것을 방지하여 애플리케이션을 더욱 안전하게 유지할 할 수 있습니다.

  • 동일한 환경에서 데이터를 가져오고 렌더링합니다. 이렇게 하면 클라이언트와 서버 간의 앞뒤 통신은 물론 클라이언트의 기본 스레드 작업도 줄어듭니다.

  • 클라이언트에서 여러 개별 요청을 수행하는 대신 단일 왕복으로 여러 데이터 가져오기를 수행합니다.

  • 클라이언트-서버 Waterfall을 줄입니다.

    여기서 Waterfall 이란 순차적으로 물흐르듯 네트워크 상의 흐름이 발생하는 것을 뜻합니다.

  • 지역에 따라 데이터 가져오기가 데이터 소스에 더 가까운 곳에서 발생하여 대기 시간이 줄어들고 성능이 향상될 수도 있습니다.

그런 다음 서버 작업을 사용하여 데이터를 변경하거나 업데이트할 수 있습니다.


필요한 곳에서 데이터 가져오기

트리의 여러 구성 요소에서 동일한 데이터(예: current user)를 사용해야 하는 경우 전역적으로 데이터를 가져오거나 구성 요소 간에 소품을 전달할 필요가 없습니다. 대신 동일한 데이터에 대해 여러 번 요청하는 경우 성능에 미치는 영향을 걱정하지 않고 데이터가 필요한 구성 요소에서 fetch 또는 React 캐시를 사용할 수 있습니다.

이는 가져오기 요청이 자동으로 메모되기 때문에 가능합니다.


스트리밍

스트리밍 및 서스펜스는 클라이언트에 UI의 렌더링된 단위를 점진적으로 렌더링하고 증분적으로 스트리밍할 수 있게 해주는 React 기능입니다.

서버 구성 요소 및 중첩 레이아웃을 사용하면 특별히 데이터가 필요하지 않은 페이지 부분을 즉시 렌더링하고 데이터를 가져오는 페이지 부분에 대한 로딩 상태를 표시할 수 있습니다. 즉, 사용자는 전체 페이지가 로드되어 상호 작용을 시작할 때까지 기다릴 필요가 없습니다.


병렬 및 순차 데이터 가져오기

React 구성 요소 내에서 데이터를 가져올 때 병렬 및 순차라는 두 가지 데이터 가져오기 패턴을 알아야 합니다.

  • 순차적 데이터 가져오기를 사용하면 경로의 요청이 서로 종속되므로 waterfall이 생성됩니다. 한 가져오기가 다른 가져오기의 결과에 따라 달라지기 때문에 이 패턴을 원하거나 리소스를 절약하기 위해 다음 가져오기 전에 조건이 충족되기를 원하는 경우가 있을 수 있습니다. 그러나 이 동작은 의도하지 않은 것일 수도 있으며 로딩 시간이 길어질 수도 있습니다.
  • 병렬 데이터 가져오기를 사용하면 경로의 요청이 즉시 시작되고 동시에 데이터가 로드됩니다. 이렇게 하면 클라이언트-서버 waterfall와 데이터를 로드하는 데 걸리는 총 시간이 줄어듭니다.

순차적 데이터 가져오기

중첩된 구성 요소가 있고 각 구성 요소가 자체 데이터를 가져오는 경우 해당 데이터 요청이 다르면 데이터 가져오기가 순차적으로 발생합니다(자동으로 메모되므로 동일한 데이터에 대한 요청에는 적용되지 않습니다).

예를 들어, Playlists 구성 요소는 Artist 구성 요소가 데이터 가져오기를 완료한 후에만 데이터 가져오기를 시작합니다. 왜냐하면 재생 목록은 ArtistID prop에 의존하기 때문입니다.

// ...
 
async function Playlists({ artistID }: { artistID: string }) {
  // Wait for the playlists
  const playlists = await getArtistPlaylists(artistID)
 
  return (
    <ul>
      {playlists.map((playlist) => (
        <li key={playlist.id}>{playlist.name}</li>
      ))}
    </ul>
  )
}
 
export default async function Page({
  params: { username },
}: {
  params: { username: string }
}) {
  // Wait for the artist
  const artist = await getArtist(username)
 
  return (
    <>
      <h1>{artist.name}</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <Playlists artistID={artist.id} />
      </Suspense>
    </>
  )
}

이와 같은 경우에는 loading.js(route 세그먼트의 경우) 또는 React Suspense (중첩된 구성 요소의 경우)를 사용하여 React가 결과를 스트리밍하는 동안 즉각적인 로딩 상태를 표시할 수 있습니다.

이렇게 하면 데이터 가져오기로 인해 전체 경로가 차단되는 것을 방지할 수 있으며 사용자는 차단되지 않은 페이지 부분과 상호 작용할 수 있습니다.

데이터 요청 차단:

Waterfall을 방지하는 또 다른 접근 방식은 애플리케이션의 루트에서 전역적으로 데이터를 가져오는 것입니다. 하지만 이렇게 하면 데이터 로드가 완료될 때까지 그 아래의 모든 경로 세그먼트에 대한 렌더링이 차단됩니다.
await이 포함된 가져오기 요청은 Suspense 경계에 래핑되거나 loading.js가 사용되지 않는 한 그 아래에 있는 전체 트리에 대한 렌더링 및 데이터 가져오기를 차단합니다. 또 다른 대안은 병렬 데이터 가져오기 또는 사전 로드 패턴을 사용하는 것입니다.

병렬 데이터 가져오기

데이터를 병렬로 가져오려면 데이터를 사용하는 구성 요소 외부에서 요청을 정의한 다음 구성 요소 내부에서 호출하여 요청을 적극적으로 시작할 수 있습니다. 이렇게 하면 두 요청을 병렬로 시작하여 시간이 절약되지만 두 약속이 모두 해결될 때까지 사용자는 렌더링된 결과를 볼 수 없습니다.

아래 예에서 getArtist 및 getArtistAlbums 함수는 페이지 구성 요소 외부에서 정의된 다음 구성 요소 내부에서 호출되며 두 약속이 모두 해결될 때까지 기다립니다.

import Albums from './albums'
 
async function getArtist(username: string) {
  const res = await fetch(`https://api.example.com/artist/${username}`)
  return res.json()
}
 
async function getArtistAlbums(username: string) {
  const res = await fetch(`https://api.example.com/artist/${username}/albums`)
  return res.json()
}
 
export default async function Page({
  params: { username },
}: {
  params: { username: string }
}) {
  // Initiate both requests in parallel
  const artistData = getArtist(username)
  const albumsData = getArtistAlbums(username)
 
  // Wait for the promises to resolve
  const [artist, albums] = await Promise.all([artistData, albumsData])
 
  return (
    <>
      <h1>{artist.name}</h1>
      <Albums list={albums}></Albums>
    </>
  )
}

UX을 향상시키기 위해 Suspense Boundary를 추가하여 렌더링 작업을 분할하고 결과의 일부를 최대한 빨리 표시할 수 있습니다.


데이터 사전 로드

Waterfall 방지를 위한 다른 방법으로는 preload 패턴을 사용하는 것이 있습니다. preload 함수를 만들어 병렬 데이터 가져오기를 더욱 최적화할 수 있습니다. 이 패턴을 사용하면 프로미스를 props로 전달할 필요가 없습니다. preload 함수의 이름은 API가 아니라 패턴이기 때문에 자유롭게 지정할 수 있습니다.

import { getItem } from '@/utils/get-item'
 
export const preload = (id: string) => {
  // void evaluates the given expression and returns undefined
  // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void
  void getItem(id)
}
export default async function Item({ id }: { id: string }) {
  const result = await getItem(id)
  // ...
}
import Item, { preload, checkIsAvailable } from '@/components/Item'
 
export default async function Page({
  params: { id },
}: {
  params: { id: string }
}) {
  // starting loading item data
  preload(id)
  // perform another asynchronous task
  const isAvailable = await checkIsAvailable()
 
  return isAvailable ? <Item id={id} /> : null
}

React 캐시, server-only 및 Preload 패턴 사용

캐시 기능, Preload 패턴 및 server-only 패키지를 결합하여 앱 전체에서 사용할 수 있는 데이터 가져오기 유틸리티를 만들 수 있습니다.

import { cache } from 'react'
import 'server-only'
 
export const preload = (id: string) => {
  void getItem(id)
}
 
export const getItem = cache(async (id: string) => {
  // ...
})

이 접근 방식을 사용하면 데이터를 적극적으로 가져오고 응답을 캐싱하며 이 데이터 가져오기가 서버에서만 발생하도록 보장할 수 있습니다.

utils/get-item 내보내기는 레이아웃, 페이지 또는 기타 구성 요소에서 항목의 데이터를 가져오는 때를 제어하는 데 사용할 수 있습니다.

tip!
서버 데이터 가져오기 기능이 클라이언트에서 사용되지 않도록 하려면 서버 전용 패키지를 사용하는 것이 좋습니다.


민감한 데이터가 클라이언트에 노출되는 것을 방지

전체 객체 인스턴스나 민감한 값이 클라이언트에 전달되는 것을 방지하려면 React의 taint API인 taintObjectReference 및 taintUniqueValue를 사용하는 것이 좋습니다.

애플리케이션에서 오염을 활성화하려면 Next.js Config Experiment.taint 옵션을 true로 설정하세요.

module.exports = {
  experimental: {
    taint: true,
  },
}

그런 다음 오염시키려는 객체 또는 값을 Experiment_taintObjectReference 또는 Experiment_taintUniqueValue 함수에 전달합니다.

//util.ts
import { queryDataFromDB } from './api'
import {
  experimental_taintObjectReference,
  experimental_taintUniqueValue,
} from 'react'
 
export async function getUserData() {
  const data = await queryDataFromDB()
  experimental_taintObjectReference(
    'Do not pass the whole user object to the client',
    data
  )
  experimental_taintUniqueValue(
    "Do not pass the user's address to the client",
    data,
    data.address
  )
  return data
}
// page.tsx
import { getUserData } from './data'
 
export async function Page() {
  const userData = getUserData()
  return (
    <ClientComponent
      user={userData} // this will cause an error because of taintObjectReference
      address={userData.address} // this will cause an error because of taintUniqueValue
    />
  )
}
profile
프론트엔드를 공부하는 최성훈입니다👋

0개의 댓글