[#1 - App Router Data Fetching]

Page Router에서 Data Fetching을 할 때에는 Pre-Rendering을 통해서 데이터를 받아왔었다. 그 중에서도 SSG,SSR,ISR 등 필요한 방식에 따라 getServerSideProps, getStaticProps, getStaticPath 등 사용 방식에 맞는 함수를 사용하여 데이터를 호출한 뒤 필요한 하위 컴포넌트에 Props 형태로 데이터를 전달해주는 로직을 구성했었다. 하지만 App Router에서는 Server Component가 생겨남과 동시에 새로운 Data Fetching 방식이 생겨났다.

함수형 컴포넌트를 사용하고 있음에 따라 컴포넌트에 async 키워드를 붙여줌으로써 해당 컴포넌트 내에서 Data Fetching을 진행하여 await 키워드로 응답 받아 필요한 데이터를 사용하면 되는 방식으로 굉장히 편리하게 Data Fetching이 가능해졌다. Async/await에 대한 사용법과 내용에 대해서는 알고 있으니 코드로 사용 방식을 살펴보자.

export default async function Home(){
  const response = await fetch(URL);
  //Error Handling
  if(!response.ok){
    return <div>Error!</div>
  }
  const data = await response.json();
  return (
    <div>
      <p>{data.id}</p>
    </div>
  )
}

위의 코드를 보면 컴포넌트 자체에 async 키워드를 붙여주고 컴포넌트 내부에서 await 키워드와 함께 fetch 함수를 사용하여 데이터를 호출하여 응답 받은 데이터를 바로 JSX에 사용하였다. 추가적으로 예외 처리도 쉽게 할 수 있다.

 

.env(환경 변수: environment value)

환경 변수란 프로젝트 내에 .env 파일을 생성하여 해당 파일 내부에 입력해둔 변수로 해당 프로젝트 내에서만 접근이 가능하여 프로젝트 전체적으로 사용하는 변수를 저장해두거나 보안이 필요한 경우에 사용하기도 하며 .env 파일은 프로젝트 전체적으로 사용되는 변수들을 모아두어 쉽게 사용하도록 만들어놓은 파일이다.

.env 파일 내부에서 환경 변수는 NEXT_PUBLIC_API_SERVER_URL = http://localhost:12345와 같이 저장하여 사용한다. 이 때 변수명 앞의 NEXT_PUBLIC과 같은 형태로 접두사를 함께 입력해주기도 한다. 그 이유는 만약 앞의 접두사를 작성해놓지 않는다면 Next는 자동으로 해당 변수를 서버 측에서만 활용할 수 있도록 private로 설정을 해버리기 때문에 Client Component에서 접근이 불가능해지게 되기 때문이다.

추가적인 정보로 NextJS에서는 환경 변수를 사용할 때 process.env.NEXT_PUBLIC_API_SERVER_URL과 같은 형태로 호출하여 사용하게 되는데 만약 Next가 아닌 Vite 환경에서 React로 프로젝트를 진행 시 환경 변수를 사용할 때에는 process가 아닌 import.meta.env 형식으로 환경 변수를 호출하여 사용해주어야 한다. Vite 환경에서 프로젝트를 하다가 Next로 넘어와서 환경 변수를 사용할 때 import.meta.env 형식으로 사용하는 것이 익숙할텐데 Next는 Webpack이나 Babel을 사용하기 때문에 Vite와 환경 변수를 다루는 방식이 다르기 때문에 process.env 형식으로 접근해야 한다.

 

import.meta.env vs process.env

import.meta.env.API_KEYprocess.env.API_KEY환경과 플랫폼에 따라 다르다. 이 두 접근 방식은 모두 환경 변수를 사용하는 방법이지만, 그 동작 방식과 목적이 다르다.

 

import.meta.env.API_KEY

Vite와 같은 빌드 도구에서 주로 사용되는 방식이다. Vite는 Next.js와는 다른 빠른 빌드 도구로, 환경 변수를 관리하는 방법도 다르게 처리한다.

  • 빌드 타임 변수: import.meta.env는 빌드 시점에 환경 변수를 처리한다. 즉, Vite는 빌드 중에 해당 변수를 읽고 정적으로 코드에 주입합니다.
  • 클라이언트 사이드에서도 사용 가능: import.meta.env는 클라이언트 사이드에서도 안전하게 사용할 수 있도록 설계되었다. 하지만 노출을 원하는 변수만 주입됩니다.
  • 보안 통제 가능: 클라이언트에 노출되지 않도록 하기 위해, VITE_ 접두사가 붙은 환경 변수만 클라이언트 사이드에서 사용 가능하다. 예를 들어, VITE_API_KEY처럼 명시적으로 작성해야 클라이언트에 노출된다.
  • Next.js에서 사용 불가: Next.js는 Webpack이나 Babel을 주로 사용하기 때문에, Next.js 프로젝트에서는 기본적으로 지원되지 않는다.

process.env.API_KEY

Node.js의 전역 객체인 process에서 환경 변수를 읽는 방식이며, Next.js와 같은 Node 기반의 런타임에서 널리 사용된다.

  • 서버 사이드 환경: process.env는 주로 SSR에서 환경 변수를 읽기 위해 사용된다. CSR에서도 사용할 수 있지만, 클라이언트에 노출하려면 변수를 NEXT_PUBLIC_ 접두사로 정의해야 한다.
  • 런타임 변수: process.env는 런타임에 변수를 읽기 때문에 서버가 시작될 때 설정된 환경 변수를 그대로 반영한다. 따라서 클라이언트에서 환경 변수를 사용하려면 빌드 시점에 미리 결정된 값을 클라이언트 코드로 전달해야 한다.
  • 보안 및 유연성: 클라이언트 사이드에 노출되지 말아야 할 중요한 변수들은 서버에서만 접근 가능하도록 NEXT_PUBLIC_ 접두사를 붙이지 않으면 된다.

Next.js에서는 process.env를 사용하는 것이 표준적이며, Vite에서 사용하는 import.meta.env는 Next.js에서 기본적으로 지원되지 않는다.

[#2 - Data Cache]

Data Cache

데이터 캐시란 Fetch Method를 통해 불러온 데이터를 Next 서버에 보관하는 기능이다. 영구적으로 데이터를 보관하거나 특정 주기로 갱신 시키는 것도 가능하다. 따라서 불필요한 데이터 요청 수를 줄여서 웹 서비스의 성능을 크게 개선할 수 있는 기능이다. 데이터 캐시를 사용하기 위해서는 반드시 Fetch Method를 사용해서 데이터 호출을 해야한다. React의 Axios 같이 별도의 데이터 호출 라이브러리를 사용하면 사용이 불가능하다. 그 이유는 Next에서 제공하는 Fetch Method는 우리가 JS에서 일반적으로 사용하던 Fetch Method와 달리 cache와 같이 일반적인 기능들을 추가해놓은 확장판 Method이기 때문이다. 사용 방법은 Fetch Method의 첫번째 인자로 데이터 호출을 요청할 API를 넣고 두번째 인자로 객체를 부여하는 것인데 객체의 종류에는 4가지가 있다. 모두 알아보자.

 

{ cache: "no-store" }

const response = await fetch(API, {cache: "no-store"};

본 속성은 Data Fetching의 결과를 저장하지 않는 옵션이다. 데이터 캐싱을 아예 하지 않도록 설정하는 옵션이다. Fetch Method를 사용하는 로직에 본 속성을 부여해서 Data Fetching을 진행하면 console 창에 데이터의 캐싱 여부를 확인할 수 있다. 하지만 확인 전에 Next App의 설정 파일인 next.config.mjs 파일에서 설정해주어야 하는 속성이 있다.

//next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  logging: {
    fetches: {
      fullUrl: true
  	}
  }
};
 export default nextConfig;

위와 같이 Next 설정 파일의 속성을 변경해주고 본 속성을 Fetch Method에 넣어서 호출하면 콘솔창에 다음과 같은 결과가 출력된다.

"cache skip"이라는 문구가 함께 출력되며 서버로부터 데이터를 호출 받은 후 캐싱을 스킵했다는 정보를 알려준다.

 

{ cache: "force-cache" }

const response = await fetch(API, {cache: "force-cache"};

본 속성은 데이터 요청의 결과를 무조건 캐싱하도록 하는 속성이다. 한 번 호출 된 이후에는 다시 호출되지 않는 특징을 갖고 있다. 마찬가지로 위의 코드 형태로 Data Fetching을 하면 console 창에 다음과 같은 결과가 출력된다.

"cache hit"이라는 문구가 함께 출력되며 서버로부터 데이터를 다시 호출하지 않고 캐싱 되어있던 데이터를 반환해준다. 앞서 살펴본 "no-store" 속성을 적용하였을 때에는 "cache skip"이라는 문구가 출력되면서 캐싱을 스킵하고 넘어간 반면에 이번에는 "cache hit"이라는 문구로 알 수 있듯이 캐싱 된 데이터를 Hit 하여 찾아낼 수 있게 되는 것이다.

이 때 캐싱된 데이터는 JSON 형태로 Next 서버 내부에 저장이 되는데 로컬에서 찾아보기 위해서는 프로젝트 내의 next > cache > fetch-cache 폴더 경로를 타고 들어가면 JSON 형태로 작성된 파일에서 확인이 가능하다.

 

{ next: { revalidate: 3 } }

본 속성은 Page Router의 ISR 방식과 유사한 형태로 특정 시간을 주기로 캐시를 업데이트 하도록 설정하는 옵션이다. "revalidate: 3"이라는 속성을 부여하면 3초 주기로 데이터를 재호출하여 캐시를 업데이트하는 방식이라고 보면 된다.

 

위에서 살펴본 { cache:  "force-cache" } 속성은 한 번 호출한 데이터를 캐싱한 후 영구적으로 저장하는 방식이지만 본 방식은 데이터를 일정 주기로 교체만 해준다는 점만 다르다.

 

{ next: { tags: ['a'] } }

본 속성은 Page Router의 On-Demand Revalidate와 유사한 형태로 요청이 들어왔을 때 데이터를 최신화하는 옵션이다. 이 속성에 대해서는 추후에 더 자세히 다루어보도록 하자.

 

[#3 - Request Memoization]

Request Memoization

동일한 페이지에서 중복된 API 요청이 발생하게 되는 경우 중복된 API 요청을 Pre-Rendering 시에 캐싱을 통해 Memoization 처리를 해두어 하나의 API 요청만으로도 모든 컴포넌트에서 응답 받은 데이터를 활용할 수 있도록 하는 기능이다. Request Memoization은 NextJS가 자동으로 실행해주는 기능이다. 코드에서의 예시를 살펴보자.

//App.tsx

async function AllData(){
  const response = await fetch(API);
  if(!response.ok){return <div>Error!</div>}
  const data = await response.json();
  return(
    <div>
      {data}
    </div>
  )
}

export default function Home(){
  return (
    <div>
      <AllData />
    </div>
  )
};

//Page.tsx

async function Footer(){
  const response = await fetch(API);
  if(!response.ok){return <div>This is Footer</div>}
  const data = await response.json();
  const dataCount = data.length;
  return (
    <footer>
      <div>This is Footer</div>
      <div>There are {dataCount} data on this page</div>
    </footer>
  )
}

export default function Page(){
  return (
    <div>
      <Footer />
    </div>
  )
}

위의 코드는 동일한 페이지에 있는 Home 컴포넌트와 Page 컴포넌트에서 동일한 API로 요청을 보내고 동일한 데이터를 사용하는 간단한 예시 코드이다. 위와 같은 코드가 실행되면 NextJS에서는 자체적으로 동일한 API 요청이 발생한 것을 보고 Request Memoization을 실행하여 페이지 Pre-Rendering 과정에서 API 요청에 대한 응답 데이터를 캐싱해두어 해당 API 요청을 한 컴포넌트들에게 서버에게로 Data Fetching 없이도 Memoization 해 둔 데이터를 사용할 수 있도록 해준다. 위의 코드와 같은 형태의 로직을 렌더링 해보면 콘솔창에 호출한 API와 그 횟수가 출력되는데 다음과 같이 출력된다.

두 개의 API 요청 중에 아래에 있는 "http://localhost:12345/book" API에게 두 컴포넌트에서 요청을 보냈음에도 불구하고 API 요청 자체는 한 번만 일어난 것을 볼 수 있다. 따라서 별도의 설정 없이도 NextJS 자체적으로 Request Memoization을 실행하여 불필요한 API 요청의 중복을 막아주었다는 것을 확인할 수 있다.

 

여기서 추가적으로 뒤에 노란 문구를 보면 "cache skip"이라는 문구를 확인할 수 있다. Request Memoization 역시 데이터 캐싱 과정을 통해서 불필요한 API 요청의 중복을 방지한 것인데 캐싱 되지 않았다는 문구가 출력되는 이유는 무엇일까. 결론부터 말하자면 Request Memoization은 데이터의 캐싱 과정보다 전 단계인 Pre-Rendering 과정에 실행되기 때문이다. 우리가 앞서 살펴 본 Data Cache는 페이지가 Pre-Rendering 되면서 서버로부터 최초로 받아온 데이터를 Fetch Method의 두번째 인자로 설정해준 객체 값에 따라서 캐싱해두는 방법이 달랐었다. 하지만 Request Memoization은 페이지의 Pre-Rendering 과정, 데이터의 캐싱 과정(캐싱 데이터 확인 하는 영역), 서버로부터 데이터를 받아오는 과정 중에서 Pre-Rendering 과정에서 실행되기 때문에 페이지가 렌더링이 되고 나면 캐싱 되었던 데이터는 모두 소멸된다. 따라서 데이터 캐싱이 페이지 렌더링이 완료되면 모두 소멸되기 때문에 콘솔 창에 "cache skip"이라는 문구가 출력되는 것이다. 그에 따라 웹 페이지를 새로 고침한 경우 Request Memoization 되었던 API 요청이 실행되는 것을 보면 Pre-Rendering이 완료되면 Request Memoization은 종료되고 캐싱된 데이터는 소멸되는 것을 확인해 볼 수 있다.

 

정리하자면, Request MemoizationData Cache는 모두 백엔드 서버에게 데이터 호출을 통해 받아온 데이터를 저장, 캐싱 해놓을 수 있도록하는 기능이다.

Request Memoization은 하나의 페이지를 렌더링하는 동안 동일한 페이지 내에서 중복된 API 요청을 캐싱하여 불필요하게 API 호출을 방지하고 캐싱해둔 데이터를 사용할 수 있도록 해주는 방식이다. 추가적으로 Request Memoization은 NextJS의 Pre-Rendering 과정에서 실행되기 때문에 초기 렌더링이 종료되면 모든 캐시가 소멸되므로 페이지가 종료되었다가 다시 접속 요청이 발생하면 다시 서버 측에 데이터를 요청하고 새로운 Request Memoization이 진행된다

Data Cahce는 백엔드 서버로부터 불러온 데이터를 거의 영구적으로 보관하기 위해 사용되는 기능이다. Request Memoization과 백엔드 서버로부터 불러온 데이터를 캐싱해놓고 캐싱된 데이터를 사용한다는 부분은 동일하지만, Data Cache는 서버가 가동중에는 영구적으로 데이터가 보관된다는 차이점이 존재한다. 이 때 말하는 서버는 백엔드 서버를 의미한다.

위의 차이점이 있는 이유를 잘 생각해보면 Data Cache는 개발자가 fetch method를 사용할 때 캐싱 옵션을 설정해줌에 따라 캐싱 여부와 데이터 호출에 대한 부분을 설정할 수 있지만 Request Memoization은 NextJS가 기본적으로 제공해주고 동작하는 기능이기 때문에 차이점이 존재할 수 밖에 없다는 점이 이해가 된다.

 

여기서 추가적인 궁금증이 발생했다. 어차피 두 기능 모두 Data Fetching을 최적화하는 기능이며 Data Cache 기능을 사용하면 Request Memoization이 필요없지 않을까라는 궁금증을 가졌다. 이 부분에 대해서 찾아본 결과 다음과 같은 이유가 있다.

- Request Memoization은 하나의 컴포넌트 내에서 같은 요청이 여러 번 발생하는 상황을 최적화한다. 따라서 중복된 네트워크 요청을 줄이고 불필요한 데이터 로드를 방지하는 효과가 있다. 결과적으로 일회성 요청 최적화에 집중이 되어 있다.

- Data Cache는 응답 데이터를 캐시에 저장해 반복적으로 사용할 수 있도록 한다. 따라서 네트워크 트래픽을 줄이고 로딩 속도를 향상시킨다. 결과적으로 보다 지속적인 데이터 재사용에 중점을 두고 있다.

위와 같은 구체적인 기능과 목적의 차이점이 있기에 두 기능이 공존하고 있으며 두 기능을 상호 보완적으로 사용하여 성능 최적화를 극대화 할 수 있다.

'NextJS' 카테고리의 다른 글

NextJS[NextJS v.13~] - (4) Streaming  (4) 2024.11.01
NextJS[NextJS v.13~] - (3) Page Caching  (3) 2024.10.11
[NextJS v.13~] - (1) App Router  (1) 2024.09.26
[NextJS] - (4) SEO, Deploy  (6) 2024.09.19
[NextJS] - (3) SSR,SSG,ISR  (1) 2024.09.19

+ Recent posts