[Next.js] Next.js에서 Data Fetching하기

Jane·2023년 8월 23일
9

Next.js

목록 보기
5/9
post-thumbnail

🥏 다시 돌아온 SSR!

  • 이전에 Next.js를 사용하게 된 이유에서도 정리했지만,
  • React, Angular, Vue와 같은 SPA 프레임워크들이 사용하던 CSR 방식에 문제점이 발견되기 시작했다.

CSR의 문제점

  1. SEO에 적합하지 못하다.
  2. 성능 문제
  3. 사용자가 첫 화면을 보기까지의 로딩이 오래 걸린다.

SSR의 재등장

  • SSR에서는, 웹 사이트 접속 시 서버에서 필요한 데이터를 모두 가져와 HTML 파일과 이를 동적으로 제어할 수 있는 소스 코드를 생성하여 클라이언트에게 보낸다.
  • 클라이언트는 잘 만들어진 HTML 문서를 사용자에게 바로 보여주게 된다.
    • 페이지 로딩이 빨라진다.
    • CSR과 달리 모든 컨텐츠가 HTML에 담겨 있어 효율적인 SEO가 가능해졌다.

그러나 SSR의 문제점!

  1. Blinking Issue
  2. 서버 과부하
  3. 웹이 제대로 반응하지 않을 수 있다.

🗣️ 웹이 제대로 반응하지 않는다는 것은 무슨 의미인가요?

위의 질문에 대해 제대로 대답하기 위해서는 TTV, TTI라는 개념을 먼저 파악해야 한다.

TTV와 TTI

🖥️ TTV

  • Time To View
  • 사용자가 웹 사이트를 볼 수 있는 시간

🖱️ TTI

  • Time To Interact
  • 사용자가 웹 사이트와 상호작용(클릭 등)을 할 수 있는 시간

🌟 CSR의 경우

  • 사용자가 사이트 접속 시, 서버는 비어있는 HTML 문서를 넘겨준다.
  • 클라이언트는 HTML에 링크된 JS 파일을 서버에 요청하고, 서버는 해당 JS 파일을 클라이언트에게 전송한다.
  • 클라이언트가 JS 파일을 받은 후에야 브라우저 화면에 웹 페이지가 뜨므로, 사용자가 웹 페이지와 바로 상호작용할 수 있다.
  • CSR에서는 TTV === TTI

🚨 SSR의 경우

  • 사용자가 사이트 접속 시, 서버는 이미 만들어진 HTML 문서를 클라이언트에게 넘겨준다.

  • 이 때문에 사용자는 화면을 바로 볼 수 있다.

  • 하지만 아직 JS 파일은 받지 못한 상태이므로, 바로 상호작용이 불가능하다.

  • SSR에서는 TTV !== TTI (시간차가 존재한다.)

  • 문제점

    • CSR의 경우 처음 로딩 속도를 줄이기 위해 JS 파일을 분할하여 필요한 정보만 보낼 수 있는 방법이 필요하다.
    • SSR의 경우 TTV와 TTI의 시간차를 줄이기 위한 방법이 필요하다.
📢 이를 해결하기 위해 등장한 SSG!

SSG

🤔 렌더링이 매 요청마다 필요한가요?

  • 여기서 렌더링이란, 서버가 다른 데이터 소스로부터 데이터를 받아와 데이터 파싱을 마친 HTML 파일을 내려주는 Next.js의 렌더링을 의미한다.
👩‍🏫 react-query를 생각해봅시다.
  • 우리는 불필요한 데이터 요청을 반복하지 않기 위해 API 결과값을 캐싱해서 사용한다.
  • 서버에서 렌더링을 할 때, 클라이언트에 리소스(HTML, CSS, JS, ...)를 제공하는 것은 결국 서버의 메모리를 소모하는 과정이다.
    • 예를 들어, 블로그 게시글에 수정된 내용이 없다는 가정 하에, 100명의 사용자가 해당 글을 열람한다고 했을 때 해당 글은 항상 같은 상태일 것이다.
    • 이 게시글의 렌더링을 서버 측에서 진행한다고 했을 때, 서버는 똑같은 렌더링 과정을 100번의 요청에 대해 반복해야 한다.
    • 그리고 이는 곧 서버 자원의 낭비로 직결될 것이다.
  • 따라서 이러한 복잡한 과정을 API 결과값을 캐싱하듯 캐싱해서 매 요청마다 반복하는 것을 줄일 수 있다면 좋지 않을까?

🧐 그렇다면 어떻게 해결할 수 있을까요?

  • 서버 자원의 낭비를 해결하기 위해, 사용자의 요청과 상관 없이 미리 렌더링을 해두었다가, 렌더링 결과값을 계속 전달해주는 방식을 사용할 수 있다.
  • 렌더링의 시점을 서버가 클라이언트의 요청을 받았을 때 ➡️ 빌드할 때 로 변경하는 것이다.
  • 빌드 타이밍에 정적인 페이지를 미리 만들어두기 때문에 정적 페이지 제공 방식이라고 표현한다.

🤠 그래서 SSG가 등장했습니다!

  • 미리 서버에 HTML 파일을 만들어두고, 이를 사용자에게 보여준다.
  • 사용자와의 상호작용 시 데이터가 변하지 않을 페이지는 미리 서버에서 HTML 파일을 만들어두고, 클라이언트의 요청이 있을 때마다 이 파일을 재사용하는 방식
    • next build 명령어 입력 시 HTML 파일이 생성되고, CDN을 통해 캐싱된다.
  • SSR처럼 pre-rendering 된 페이지를 보여주지만 변경사항을 업데이트할 수 없다.
  • CSR, SSR보다 렌더링 속도가 훨씬 빠르다.

CSR 🤼 SSR 🤼 SSG

분류Meaning방식
CSRClient-Side-Rendering화면을 클라이언트단에서 바꾸는 것
SSRServer-Side-Rendering화면을 서버단에서 전송해주는 것
SSGServer-Side-Generation화면을 서버에서 미리 만들어 전송해주는 것

🧐 CSR, 언제 써야 할까?

  • 네트워크가 빠를 때
  • 서버의 성능이 좋지 않을 때
  • 사용자에게 보여줘야 하는 데이터의 양이 많을 때 (로딩 화면을 보여줄 수 있음)
  • 메인 Script 로직이 가벼울 때
  • SEO가 중요하지 않을 때
  • 웹 애플리케이션에 사용자와 상호작용하는 부분이 많을 때 (UX가 중요한 경우)

🧐 SSR, 언제 써야 할까?

  • 네트워크가 느릴 때
  • 표시된 데이터가 항상 최신 데이터일 때
  • 자주 변경될 가능성이 있는 사용자별 데이터 or 동적 데이터일 때
  • SEO가 중요할 때
  • 최초 로딩이 빠른 페이지가 필요할 때
  • 메인 Script 로직이 무겁고 로딩이 오래 걸릴 때
  • 웹 사이트의 상호작용이 적을 때

🧐 SSG, 언제 써야 할까?

  • 정적인 정보를 항상 보여주는 페이지일 때
  • 상호작용을 위한 데이터가 존재하지 않을 때
  • 사용자의 요청보다 앞선 렌더링이 가능한 페이지일 때
  • 예: 블로그, 도움말, 마케팅용 페이지
 Next.js에서는 성능 상의 차이로 인해 SSG 방식을 권장한다.
 하지만 하나만 사용해야 하는 것은 아니고, 페이지마다 선택이 가능하다.

Next.js 12에서의 SSR

getServerSideProps( )

function Page({ data }) {
  // 데이터를 렌더링하는 코드~
}

// 모든 요청에서 호출된다.
export async function getServerSideProps() {
  // 외부 API에서 전송되는 데이터를 fetch
  const res = await fetch(`https://.../data`);
  const data = await res.json();

  // Props를 사용해 페이지로 데이터를 전달한다.
  return { props: { data } };
}

export default Page;
  • 매 요청마다 fetch가 실행되고, HTML이 서버 측에서 새로 생성된다.
  • 자주 변경되는 최신 데이터를 페이지에 업데이트 시킬 때 사용된다.
  • 서버 측에서만 실행되며 클라이언트 측에서는 import 되지 않는다.
  • next/link, next/router를 통해 페이지 요청 시 Next.js가 서버에 API 요청을 보내고, 서버가 이 함수를 실행시킨다.
  • 페이지에서만 동작하는 함수로, 페이지가 아닌 파일에서는 동작하지 않는다.
    • 예: app.js, document.js

⚠️ 이것은 주의하자!

  • 각각의 렌더링 유형은 props를 페이지 컴포넌트로 전달한다.
  • 초기 HTML 파일이 전송되면, 클라이언트 측에서 전달된 props 데이터를 확인하고 적절한 Hydration을 진행할 수 있다.
  • 이때 클라이언트에서 사용하면 안 되는 민감한 정보(secret key)를 props로 전달하지 않도록 하자!

Next.js 12에서의 SSG

외부 데이터가 없는 경우

function About() {
  return <div>About Me :D</div>;
}

export default About;
  • 다른 외부 데이터가 없는 경우 자동으로 HTML 파일이 생성된다.

외부 데이터가 존재하는 경우: getStaticProps( )

  • Next.js에서 SSG를 구현할 수 있게 만드는 기능
  • 소스 빌드 타임에 리소스들을 미리 렌더링해두고, 이후 요청에 대해서는 캐싱된 렌더링 결과들이 제공된다.
  • 매 요청마다 특정 API Call을 실행하여 데이터를 가져오는 것이 아니라 빌드 시 한 번만 호출이 이루어진다.
    • 따라서 실시간으로 변경되는 데이터에 따라 페이지가 변경되어야 하는 사이트에서는 적합하지 않은 방식!

🧐 조금 더 구체적인 상황을 가정해보자.

function Blog({ posts }) {
  // 게시글을 작성하는 코드~
}

// 이 함수는 빌드 시점에 호출된다.
export async function getStaticProps() {
  // 게시글을 가져오기 위한 외부 API 엔드 포인트를 호출한다.
  const res = await fetch("https://.../posts");
  const posts = await res.json();

  // 외부 API를 통해 가져온 데이터를 { props: { posts } }로 반환함으로써,
  // 블로그 컴포넌트는 빌드 시점에 prop으로 'posts'를 받게 된다.
  return {
    props: {
      posts,
    },
  };
}

// getStaticProps를 통해 구현된 컴포넌트는 빌드 시 실행되어 HTML화 된다.
export default Blog;
  • getServerSideProps( )와 사용 방법은 매우 유사하나 렌더링 시점에 차이가 있다.
    • getServerSideProps( ): 매 요청마다 HTML 생성
    • getStaticProps( ): 매 빌드 시점마다 pre-rendering
  • 만약 블로그에 글을 포스팅하는 request를 보내고, DB에 해당 요청이 반영되었다면

    일반적으로는 포스팅과 동시에 페이지를 새로고침하고, 페이지가 다시 렌더링되며 data-fetching이 된다.
    • 이를 통해 DB에서 새로 생긴 내용을 포함한 데이터를 받아오고, 사용자는 새로운 데이터가 추가된 블로그를 볼 수 있게 된다.
  • 하지만 getStaticProps( )로 블로그 글 리스트를 가져오면 처음 페이지가 빌드되었을 때의 props를 그대로 유지하므로 이처럼 새로운 데이터가 반영되지 않는다.
  • getStaticProps( )는 항상 서버에서 실행되고, 클라이언트 측에서는 절대 동작하지 않는다.
    • pre-rendering 된 HTML 파일은 클라이언트로 전송되기 전 서버 상에서 이미 처리된다.
    • 그러므로 API Call을 통해 데이터를 fetching하는 부분은 서버에서 이미 끝난 상태로 보내진다.
    • getStaticProps( )에 정의된 코드는 클라이언트에서 받았을 때 이미 번들링을 통해 삭제되어 있으므로 API를 fetching하는 URL 등의 주요 정보가 노출될 걱정을 하지 않아도 된다.

외부 데이터 요청을 함수 내부에서 호출하면 성능이 저하된다.

// lib/load-posts.js 로 분리
export async function loadPosts() {
  const res = await fetch("https://url");
  const data = await res.json();
  return data;
}

// pages/blog.js
import { loadPosts } from "../lib/load-posts";

export async function getStaticProps() {
  const posts = await loadPosts();

  return { props: { posts } };
}
  • 따라서 lib/이라는 별도의 폴더에 파일을 생성하여 API 경로를 분리시켜주어야 한다.

🚨 getStaticProps( )의 문제점

  • 위의 설명에서도 유추할 수 있듯, getStaticProps( )는 빌드 시에만 렌더링할 데이터에 변화를 줄 수 있다.
  • 이 때문에 데이터의 변화가 필요한 경우에도 이전 데이터를 담고 있는 페이지를 노출한다거나(변화된 내용을 제공하지 못한다.), 빌드를 너무 자주해야 하는 문제점이 발생할 수 있다.
📢 이를 해결하기 위해 등장한 ISR!

ISR (Incremental Static Regeneration)

getStaticProps( ), revalidate

  • 간단하게 말하자면, 정적 생성 방식으로 미리 만들어놓은 사이트도 필요한 경우 업데이트가 가능해졌다.

이미지 출처

1. 사용자 1 이 해당 페이지 진입 시 설정된 시간 동안은 어느 사용자가 들어오더라도 미리 생성해두었던 페이지를 제공한다.
2. 이후 설정 시간이 지나면 Next.js 백그라운드에서 해당 페이지를 업데이트한다.
3. 업데이트가 완료된 후 접속한 사용자에게는 새롭게 만들어진 페이지가 제공된다.
  • Next.js에서 제공하는 코드 예시를 살펴보자.
// 이 함수는 Server-Side에서 빌드하는 시점에 호출된다.
// 그리고 revalidation이 활성화되어 있고, 새로운 요청이 들어온 경우 다시 호출된다.
// 즉, 첫 요청 후 10초 뒤 "새로운 요청이 있으면" 정적 생성을 진행한다.
export async function getStaticProps() {
  const Next.js 12에서의 ISRres = await fetch("https://url");
  const posts = await res.json();

  return {
    props: {
      posts,
    },
    // 이럴 때 Next.js는 페이지를 새로 만든다!
    // : 새로운 요청이 들어왔을 때 + revalidate 시간으로 설정된 10초마다
    revalidate: 10,
  };
}
  • getStaticProps( ) 함수를 사용함과 동시에 return 객체에 revalidate 옵션을 주고 있다.
  • getStaticProps( ) 방식을 사용하여 빌드 타임에 렌더링을 미리 실행하되, 이후에 요청이 들어온다면 적어도 10초마다 리렌더링을 하게 된다.
  • 이를 통해 빌드 타임 렌더링 방식의 장점과 주기적인 페이지의 변화 라는 장점을 동시에 가질 수 있다.
  • 매 요청마다 렌더링을 할 필요는 없지만 주기적인 정보 갱신이 필요한 무거운 페이지에 적합한 방식이다.
    • 예: 정보 제공 페이지

🐎 On-demand Revalidation

  • Next.js 12.2.0부터 지원하는 기능으로, revalidate 과정을 필요에 따라 진행할 수 있게 해준다.
  • 기존 ISR 방식에서는 미리 렌더링되어 캐싱된 HTML 파일만이 내려오기 때문에 지정한 시간이 지날때까지는 항상 같은 화면이 노출된다.
  • 하지만 On-demand Revalidation 방식을 사용하면 지정된 시간 간격 외의 revalidate도 가능하다.
    • 지정한 특정 상황에 렌더링을 재실행할 수 있게 해주는 것!
// 임의의 사용자가 revalidate를 강제할 수 없도록 토큰을 생성해 사용한다.

export default async function handler(req, res) {
  // 유효한 요청인지 확인한다.
  if (req.query.secret !== process.env.MY_SECRET_TOKEN) {
    return res.status(401).json({ message: "Invalid token" });
  }

  try {
    // API 핸들러 내부에서 res.revalidate()를 사용해 원하는 상황에서 revalidate를 실행시킨다.
    // 재작성된 경로가 아닌 실제 경로여야 한다.
    // 예: "/blog/[slug]"의 경우 "/blog/post-1"로 작성해야 한다.
    await res.revalidate("/path-to-revalidate");
    return res.json({ revalidated: true });
  } catch (err) {
    // 에러가 발생하면 Next.js는 이전에 성공적으로 생성된 페이지를 계속 보여주게 된다.
    return res.status(500).send("Error revalidating");
  }
}
  • 또한 API 라우터를 열어둠으로써 API 요청을 통한 원격 revalidate도 가능하다.
https://<your-site.com>/api/revalidate?secret=<token>
  • 이를 통해 특정 조건이 충족되면 해당 API를 호출하여 페이지를 revalidate 시킬 수 있다.
    • 예: 데이터 배치 작업이 정상적으로 종료된 경우 위의 API 호출 과정을 통해 데이터에 의존성을 갖는 페이지를 새로 revalidate 해줄 수 있다.

Next.js 13에서는 어떻게 바뀌었을까?

👩‍🏫 Next.js 13의 Data Fetching 방식에 영향을 미친 React 18 버전의 새로운 개념을 먼저 알아봅시다.

🤠 Client Component에서 Server Component가 분리되었습니다!

  • React 18 이전 버전에서는 Client Component로 모든 것을 처리했다.
  • 하지만 React 18 버전부터는 Client Component와 Server Component를 분리하기 시작했다.
    • 정확히 말하자면, React 개발팀이 React 18 공개 전 새로 발표한 개념이고, 실제 초기 배포 시에는 포함되지 않았다.
    • 하지만 Next.js는 이러한 개념을 사용하여 React Component들이 서버에서 부분적으로 렌더링되는 페이지가 되게 하였고, 이를 통해 클라이언트 측에서 모든 것을 부담하지 않도록 개선하였다.
  • Server Component
    • 서버 관련 로직을 내포하고 있다.
    • Data Fetching 시 사용된다.
    • 백엔드 리소스에 접근할 때, 중요한 정보를 서버에 보관할 때(API Key, Access Token) 사용한다.
  • Client Component
    • 이름과 달리 기존에 사용하던 SSR + Hydration 방식의 컴포넌트라고 보면 된다.
    • 서버 측에서 생성된 HTML 파일에 클라이언트 측에서 Hydration 작업을 진행해주어야 한다.
    • Data Fetching에 SWR, React Query 등의 라이브러리를 적용하는 경우 사용된다.
    • onClick, onChange 등의 이벤트 리스너 추가 시 사용된다.
    • useState, useEffect 등 상태와 LifeCycle 부수효과가 필요할 때 사용된다.
    • Browser Web Api(LocalStorage, SessionStorage, Cookie)가 필요할 때 사용된다.
    • Custom Hook이나 React Class Component가 필요할 때 사용된다.

🧐 어떤 점이 좋은가요?

  1. 클라이언트 측에서 돌아가지 않는 DB 및 API 등의 백엔드 서비스에 접근할 수 있다.
  2. 보안이 중요한 Key 값들이 클라이언트 측에 드러나지 않도록 할 수 있다.
  3. Data Fetching과 렌더링을 동일한 환경에서 수행할 수 있다.
  • 클라이언트 로직이 필요한 곳은 Client Component, 서버 관련 로직이 필요한 곳에서는 Server Component를 사용하여 보다 효율적인 렌더링이 가능해졌다.
  1. 서버에 렌더링된 값을 캐싱할 수 있다.
  • 이를 통해 서버 관련 로직을 컴포넌트에 바로 적용시킬 수 있게 되어 클라이언트에 전송되어야 하는 데이터의 양이 현저히 줄어들었다.
  1. 번들링할 JS 양을 줄일 수 있다.
👩‍🏫 그러면 Next.js 13은 이를 사용하여 어떻게 Data Fetching 방식을 바꿨을까요?

async/await과 함께 사용되는 fetch() API 사용

  • Next.js에서 새롭게 추가된 App 폴더의 모든 컴포넌트는 기본 값이 Server Component로 설정되어 있다.
  • 따라서 서버에서 데이터를 불러오는 작업을 별도의 추가 작업 없이 바로 할 수 있다.
  • 앞에서 본 getStaticProps, getServerSideProps 메서드는 해당 폴더에서 사용하지 못한다.
  • 대신 Web 기본 API인 fetch()를 확장하여 cache, next 옵션을 적용할 수 있게 하였다.

Next.js 13에서의 SSR

// app/posts/page.js
const fetchPosts = async () => {
  const response = await fetch("https://url", {
    cache: "no-store",
  });
  return response.json();
};

const Posts = async () => {
  const data = await fetchPosts();
  return data.map((item, index) => <div key={item.id}>{item?.title}</div>);
};

export default Posts;
  • 사용자가 원하는 직관적인 함수명(예: fetchPosts)을 직접 정의하고 해당 함수로 데이터를 불러올 수 있다.
  • cache: "no-store"
    • 해당 쿼리에 대해서는 자동 캐싱 작업을 하지 않고, 새로운 요청이 있을 때마다 매번 데이터를 새로 불러오게 설정해준다.

Next.js 13에서의 SSG

// app/posts/page.js

const fetchPosts = async () => {
  const response = await fetch("https://url");
  // {cache: "force-cache"} > default 값이어서 생략 가능
  return response.json();
};

const Posts = async () => {
  const data = await fetchPosts();
  return data.map((item, index) => <div key={item.id}>{item?.title}</div>);
};

export default Posts;
  • SSR에서 SSG로 렌더링 방식을 변경하기 위해서는 아예 다른 메서드를 사용해야 했던 이전 버전과는 달리, Next.js 13 버전 이후부터는 cache 옵션만 변경해주면 렌더링 방식을 바꿀 수 있다.
  • cache: "force-cache"
    • default value
    • 캐싱을 강제하여 정적 사이트로 만들어준다.
    • 처음에는 Server-Side에서 작동하고, 두 번째부터는 캐싱된 값을 불러온다.
    • 빌드 시점에 fetch 된 request는 직접 무효화하기 전까지 캐싱된다.

Next.js 13에서의 ISR

const fetchPosts = async () => {
  const response = await fetch("https://url", {
    next: {
      revalidate: 10,
    },
  });
  return response.json();
};
  • SSG 방식을 사용하되 주기적으로 데이터를 새로 불러오기 위해서는 {next: {revalidate: minute}} 옵션에 데이터를 새로 fetch 할 시간 간격을 설정해주면 된다.
  • 위의 코드에서는 10초마다 API에 데이터 요청을 하게 되므로, 그 사이에 데이터에 변경 사항이 있었다면 별도의 빌드 과정이 없더라도 해당 데이터를 볼 수 있다.

🔎 References

참고 자료 모음
profile
An investment in knowledge pays the best interest🙃

2개의 댓글

comment-user-thumbnail
2023년 11월 15일

정리를 정말 잘하신 것 같아요. :)

1개의 답글