티스토리 뷰
✅ (복습) React App에서의 data fetching
1. 불러온 데이터를 보관할 state 생성
2. 데이터 페칭 함수 생성
3. useEffect를 이용해 컴포넌트 마운트 시점에 데이터 페칭 함수 호출
4. 데이터가 로딩 중일때의 예외처리
export default function Page() {
// 1. 데이터를 보관할 state 생성
const [state, setState] = useState();
// 2. 데이터 페칭 함수 생성
const fetchData = async () => {
const response = await fetch("...");
const data = await response.json();
setState(data);
};
// 3. 컴포넌트 마운트 시점에서 데이터 페칭 함수 호출
useEffect(() => {
fetchData();
}, []);
// 4. 데이터가 로딩 중일 때 예외처리
if(!state) return "Loading...";
return <div>...</div>;
}
위와 같은 데이터 페칭 방식의 단점은 초기 접속 요청부터 데이터 로딩까지 오랜 시간이 걸린다는 것이다.
왜냐하면 백엔드 서버에게 보내는 데이터 요청이 컴포넌트 마운트된 이후에 발생하기 때문이다.
CSR 방식인 React App은 FCP도 느린데 데이터 요청은 컨텐츠가 렌더링 된 이후에 발생한다.
데이터 요청 시점 자체가 늦으니 사용자가 페이지에 접속하고 데이터가 불러와져서 정상적으로 볼 수 있기까지 시간이 오래 걸린다.
사용자는 초기 접속 시 컨텐츠 렌더링 시간 + 이후 데이터 요청해서 로딩하기까지의 시간 동안 기다려야하는 것이다.
✅ Next.js에서의 data fetching : 사전 렌더링
사전 렌더링에 대한 자세한 내용은 이전 글에서 참고하자.
2024.08.30 - [개발공부/⬛ Next.js] - [Next.js] 사전 렌더링 이해하기
Next.js는 사전 렌더링을 통해 서버에서 렌더링된 HTML을 브라우저에 전달해주는데 이 과정에서
백엔드 서버로부터 현재 페이지에 필요한 데이터를 미리 불러오도록 설정할 수 있다.
이 방식은 React App에서의 data fetching보다 빠른 타이밍에 데이터를 요청하고 불러온다.
서버가 브라우저에게 전달하는 렌더링된 HTML에는 이미 백엔드 서버로부터 불러온 데이터들이 다 포함되어 있기 때문에
사용자에게 데이터 페칭이 완료된 페이지를 추가적인 로딩없이 바로 보여줄 수 있다.
정리해서 React와 Next.js의 데이터 페칭을 비교해보자면,
React App의 데이터 페칭
- 컴포넌트 마운트 이후에 발생.
- 데이터 요청 시점이 느려지게 되는 단점.
Next App의 데이터 페칭
- 사전 렌더링 중 발생. (당연히 컴포넌트 마운트 이후에도 발생 가능)
- 데이터 요청 시점이 매우 빠르다는 장점.
그런데 만약에 Next.js에서 데이터 페칭이 진행될 때 백엔드 서버 상태가 매우 좋지 않다거나
불러와야할 데이터의 용량이 엄청 크다거나 하는 등의 이슈로 사전렌더링 중 데이터 요청이 너무 오래걸리게 된다면,
사용자는 아무런 화면도 볼 수 없는게 아니냐는 의문이 들 수도 있다.
Next.js는 이러한 문제점을 해결하기 위해 사전렌더링이 오래걸릴 것으로 예상되는 페이지는
빌드 타임(build time)에 미리 사전렌더링을 진행하는 등의 다양한 사전 렌더링 방식을 제공한다.
✅ Next.js의 다양한 사전 렌더링 : SSR, SSG, ISR
1. 서버사이드 렌더링 (SSR) : Server Side Rendering
- 가장 기본적인 사전 렌더링 방식이다.
- 요청이 들어올 때마다 사전 렌더링을 진행한다. (요청이 들어올 때마다 페이지를 새롭게 생성)
- getServerSideProps() 라는 함수를 통해 구현할 수 있다.
장점 : 페이지 내부의 데이터를 항상 최신으로 유지할 수 있다.
단점 : 데이터 응답 속도가 느려지면 모든 것이 늦어지게 된다.
페이지 역할을 하는 파일 안에 getServerSideProps()라는 Next.js에서 제공하는 함수를 작성하고 export하면 이 페이지는 SSR로 동작하도록 설정된다.
// SSR을 적용하고 싶은 페이지
export const getServerSideProps = () => {
const data = "백엔드에서 불러온 데이터";
return {
props: {
data,
},
};
};
export default function Page({ data }) {
console.log(data);
// ...
}
getServerSideProps()는 해당 페이지에 접근했을 때 페이지 컴포넌트보다 먼저 실행되어
해당 페이지에 필요한 데이터를 불러오는 역할을 한다. 데이터를 불러온 다음 페이지 컴포넌트가 실행된다.
- getServerSideProps()에서 props라는 속성을 가진 객체를 반환해주면 Page 컴포넌트로 해당 props를 전달해준다.
- getServerSideProps()의 return값은 반드시 props라는 객체 프로퍼티를 포함하는 단 하나의 객체여야 한다.
- getServerSideProps()는 사전 렌더링 과정에서 딱 한번만 실행되는 함수이므로 오직 서버 측에서만 실행되는 함수이므로
해당 함수 내에서 window 등 브라우저에서 사용하는 객체는 사용하지 못한다.
export const getServerSideProps = () => {
window.alert(); // 오류 발생
// ...
};
여기서 한가지 더 주의할 점은,
페이지 역할을 하는 Page 컴포넌트는 사전 렌더링 과정에서 서버에서 먼저 한번 실행된 후 브라우저에서 js번들 형태로 전달될 때
즉, 하이드레이션 과정이 진행될 때 한번 더 실행된다.
위 코드에서 Page 컴포넌트 내부에 작성된 console.log(data)는 서버에서 한번, 브라우저에서 한번 찍히는 것을 볼 수 있다.
따라서, Page 컴포넌트 안에서 어떠한 조건도 없이 window.location() 같은 코드를 작성하면 오류가 발생한다.
export const getServerSideProps = () => {
// ...
};
export default function Page({ data }) {
window.location; // 오류 발생
// ...
}
브라우저 측에서만 실행되는 코드를 작성하고 싶다면 여러 방법이 있지만 가장 간단한 방법은 useEffect()를 사용하는 것이다.
export const getServerSideProps = () => {
// ...
};
export default function Page({ data }) {
useEffect(() => {
console.log(window);
}, [])
// ...
}
마지막으로, getServerSideProps()를 통해 전달받은 props의 타입은 어떻게 지정해야 되는지 알아보자.
Next.js가 제공하는 내장타입 중에 InferGetServerSidePropsType라는 타입은
getServerSideProps()의 반환값 타입을 자동으로 추론해주는 기능을 하는 타입이다.
제네릭으로 <typeof getServerSideProps>을 넣어주면 자동으로 getServerSideProps() 반환값 타입이 추론이 되어서
Page 컴포넌트의 매개변수의 타입을 정의해준다.
export const getServerSideProps = () => {
const data = "백엔드에서 불러온 데이터";
return {
props: {
data,
},
};
};
export default function Page({ data }: InferGetServerSidePropsType<typeof getServerSideProps>) {
console.log(data);
// ...
}
2. 정적 사이트 생성 (SSG) : Static Site Generation
- 빌드 타임에 미리 사전 렌더링을 진행한다.
- SSR의 단점인 데이터 응답 속도가 느릴 경우를 보완하기 위한 사전 렌더링 방식이다.
- 1데이터가 자주 업데이트 되지 않아도 되는 정적인 페이지에 적합한 방식이다.
- getStaticProps() 라는 함수를 통해 구현할 수 있다.
장점 : 사전 렌더링에 많은 시간이 소요되는 페이지더라도 사용자의 요청에는 매우 빠른 속도로 응답이 가능하다.
단점 : 매번 똑같은 페이지만 응답하기 때문에 최신 데이터를 반영하기 어렵다.
getServerSideProps()와 마찬가지로 props라는 속성을 가진 객체를 반환해주면 Page 컴포넌트로 해당 props를 전달해준다.
props 타입 또한 동일한 방법인데 대신 InferGetStaticPropsType를 사용해주면 된다.
export const getStaticProps = async () => {
console.log("인덱스 페이지")
const [allBooks, recoBooks] = await Promise.all([fetchBooks(), fetchRandomBooks()]);
return {
props: {
allBooks,
recoBooks,
},
};
};
export default function Home({ allBooks, recoBooks }: InferGetStaticPropsType<typeof getStaticProps>) {
// ...
}
위 코드에서 getStaticProps 안에 입력한 console.log는 개발 모드에서는 매 요청 때마다 서버 콘솔에 찍히지만
프로덕션 모드에서는 빌드 시 페이지가 생성될 때 한 번만 찍히고 브라우저 요청에는 찍히지 않는다.
그리고 Route 좌측에 속이 찬 동그라미, 속이 빈 동그라미 등이 가호 표시가 있는데
빌드 결과 최하단을 보면 기호별로 사전 렌더링이 어떤 방식으로 진행되는지 알 수 있다.
/book, /search 페이지는 getServerSideProps를 이용해 SSR로 동작하게 코드를 작성한 페이지들이다.
이 페이지들에는 f로 표시하는 Dynamic 기호가 붙어있는데 이는 브라우저로 요청을 받을 때마다
다이나믹하게 사전 렌더링이 될 거라는 뜻, 즉 SSR로 동작한다는 뜻이다.
/api/hello, /api/time 같은 api 라우트에 다이나믹 기호가 붙어있는 이유는
Next.js가 기본적으로 모든 api 라우트들을 SSR로 작동하게 설정하기 때문이다.
/404, /test 같은 Static 기호가 붙은 페이지는 SSG와 동일한 정적인 페이지인데 getStaticProps를 사용하지 않은 페이지이다.
이 페이지들은 SSR도, SSG도 적용하지 않은 페이지인데 Next.js에서는 이렇게 아무것도 적용하지 않은 페이지들은
기본값으로 정적인 페이지로 설정하여 빌드 타임에 미리 사전렌더링하도록 한다.
즉, Next.js는 getServerSideProps나 getStaticProps로 SSR나 SSG로 설정해주지 않는다면
기본적으로 SSG 페이지로 설정이된다는 것을 알 수 있다.
(= Next.js의 기본 사전 렌더링 방식은 SSG이다.)
SSG에서 동적 경로를 사용하려면 어떤 경로들이 존재할 수 있는지 설정하는 과정이 필요하다.
그 설정은 getStaticPaths() 함수를 통해 설정할 수 있다.
paths 속성에 존재할 수 있는 모든 경로를 작성해주고, fallback 속성에는 paths에 작성한 이외의 경로로 접근했을 때 어떻게 처리할지 설정해줄 수 있다.
export const getStaticPaths = () => {
return {
paths: [
{ params: { id: "1" } }, // params의 값은 반드시 문자열
{ params: { id: "2" } },
{ params: { id: "3" } },
],
fallback: false, // 존재하지 않는 페이지는 not found 처리
};
};
export const getStaticProps = async (context: GetStaticPropsContext) => {
// ...
};
export default function Page({ book }: InferGetStaticPropsType<typeof getStaticProps>) {
// ...
}
위 코드는 getStaticPaths()를 통해 /book/[id]라는 동적 경로가 /book/1, /book/2, /book/3 이라는 경로만 가지고 있다고
미리 알려주고 그 이외의 id값을 받을 경우는 not found 처리를 하라고 작성한 것이다.
빌드를 시켜보면 /book/1, /book/2, /book/3 페이지 정적으로 미리 생성해둔 것을 확인할 수 있다.
(.next/server/pages/book 디렉터리에서도 사전 렌더링이 되어있는 것을 확인해 볼 수 있다.)
이렇게 미리 만들어진 정적 페이지에 접근해보면 매우 빠르게 접속되는 것을 확인할 수 있다.
getStaticPaths()의 fallback 속성에는 3가지 옵션이 있다.
false
: 없는 경로로 요청시 404 Not Found 반환
"blocking"
: 없는 경로로 요청시 즉시 생성 (Like SSR)
→ 빌드 타임에 모든 동적 경로를 불러오기 어려운 상황일 경우, 새로운 데이터가 계속 추가되어야 하는 경우 등에 사용 가능.
→ 첫번째 요청 때는 SSR 방식으로 페이지를 새롭게 생성해서 신규 데이터를 반영, 그 이후에는 SSG 방식으로 빠른 접근.
true
: 즉시 생성 + 페이지만 미리 반환
bloking 옵션을 사용할 때 존재하지 않았던 페이지를 새롭게 생성하면서 페이지 생성 시간이 길어지면
브라우저에게 아무 응답도 주지 않기 때문에 로딩이 발생하게 된다. 이런 문제를 해결하기 위해 사용하는 옵션이 fallback: true이다.
true 옵션을 사용하면 props가 없는 페이지, 즉 getStaticProps로부터 받은 데이터가 없는 페이지를 먼저 반환해서 사용자에게 보여준다.
이 때, 페이지 컴포넌트가 아직 서버로부터 데이터를 전달받지 못한 상태를 "fallback 상태"라고 한다.
fallback 상태일 때 로딩 등의 처리를 해주고 싶다면 useRouter()의 isFallback 속성을 통해 fallback 상태인지 확인하여
원하는 동작을 처리해줄 수 있다.
export const getStaticPaths = () => {
return {
paths: [...],
fallback: true,
};
};
export const getStaticProps = async (context: GetStaticPropsContext) => {
// ...
};
export default function Page({ book }: InferGetStaticPropsType<typeof getStaticProps>) {
const router = useRouter();
if (router.isFallback) return "로딩 중입니다..."; // fallback 상태일 때
if (!book) return "문제가 발생했습니다. 다시 시도하세요."; // 뭔가 데이터에 문제가 있어서 데이터를 받지 못했을 때
// ...
};
3. 증분 정적 재생성 (ISR) : Incremental Static Regeneration
- SSG 방식으로 생성된 정적 페이지를 일정 시간을 주기로 다시 생성하는 방식
장점 : 매우 빠른 속도로 응답이 가능(SSG의 장점)하며, 주기적으로 페이지를 업데이트하여 최신 데이터 반영이 가능(SSR의 장점)하다.
설정해준 시간마다 정확히 딱딱 맞춰서 업데이트를 진행하는게 아니라 설정한 시간 이후에 접속 요청이 발생하면
원래 가지고 있었던 페이지를 반환하고 서버에서 새로운 페이지를 재생성한다.
그 이후 발생하는 요청에는 새로 업데이트된 페이지를 반환한다.
ISR을 적용하는 방법은 SSG에서 사용했던 getStaticProps()의 반환값에 revalidate라는 속성을 추가해주고
몇 초 간격으로 재생성을 진행할 것인지 설정해주면 된다.
export const getStaticProps = async () => {
// ...
return {
props: {
// ...
},
revalidate: 3,
};
};
3-1. On-Demand ISR (주문형 재검증)
- 요청을 받을 때 마다 페이지를 다시 생성하는 요청 기반의 ISR
ISR은 SSR과 SSG의 장점을 합쳐놓은 방식인 만큼 매우 강력한 사전 렌더링 전략이다.
하지만, 시간과 관계없이 사용자의 행동에 따라 데이터가 업데이트 되는 페이지와 같이
시간 기반의 ISR 방식을 적용하기 어려운 페이지도 존재한다.
예를 들어, 커뮤니티 사이트의 게시글 페이지는 사용자가 게시글을 수정한 시점에서 해당 내용으로 업데이트가 되어야 하는데
ISR 방식을 사용하면 재생성 시점이 지나야 업데이트가 적용되므로 최신 데이터를 즉각적으로 반영하기 어렵다.
아니면 아래와 같이 불필요한 페이지의 재생성이 발생할 수 도 있다.
이런 경우 ISR말고 SSR을 사용하면 되지 않나? 라고 생각할 수 있지만, SSR은 브라우저가 요청할 때마다
매번 새롭게 사전렌더링을 진행하므로 응답시간이 느려지고 동시 접속자가 많이 몰리게 되면 서버의 부하가 올 수 있다.
그래서 Nex.js에서는 시간을 기반으로 페이지를 업데이트 시키는 기존의 ISR 방식과 더불어
요청을 기반으로 페이지를 업데이트 시키는 On-Demand ISR 방식 또한 제공한다.
On-Demand ISR 방식을 사용하면 거의 대분의 페이지를 최신 데이터 반영이 가능한 정적 페이지로 처리할 수 있다.
On-Demand ISR을 적용하는 방법은 기본 ISR을 적용할 때 getStaticProps()의 반환값에 추가했던 revalidate라는 속성은 다시 지워주고
export const getStaticProps = async () => {
// ...
return {
props: {
// ...
},
};
};
export default function Home({ allBooks, recoBooks }: InferGetStaticPropsType<typeof getStaticProps>) {
// ...
}
revalidate 요청을 처리할 API 라우트를 하나 만들어 준다.
// pages/api/revalidate.ts
import { NextApiRequest, NextApiResponse } from "next";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await res.revalidate("/"); // 인수는 revalidate할 페이지 경로
return res.json({ revalidate: true });
} catch (err) {
res.status(500).send("Revalidation Failed");
}
}
이제 /api/revalidate라는 주소로 접속 요청을 하게 되면 위 handler()가 실행되며 인수로 전달한 "/"경로를 revalidate할 것이다.
프로젝트를 빌드하고 start하면 "/" 페이지를 계속 새로고침해도 SSG방식이라 데이터가 변화하지 않지만
브라우저에서 새탭을 열고 ".../api/revalidate"로 한번 접속한 뒤 "/" 페이지를 새로고침하면 데이터가 업데이트되는 것을 확인할 수 있다.
정리하자면, 사용자의 행동에 따라 혹은 특정 조건에 따라 데이터가 업데이트되어야하는 페이지를
정적 페이지로 유지하고 싶다면 On-Demand ISR 방식을 사용할 수 있다.
On-Demand ISR은 대부분의 케이스를 커버할 수 있는 강력한 사전 렌더링 방식이라
많은 Next.js로 구축된 웹 서비스가 이 방식을 사용하고 있다고 한다.
참고자료
🔗 [이정환] 한입 크기로 잘라먹는 Next.js(15+)
🔗 [Next.js 공식문서] SSR (Using Pages Router)
🔗 [Next.js 공식문서] SSG (Using Pages Router)
🔗 [Next.js 공식문서] 자동 정적 최적화 (Using Pages Router)
🔗 [Next.js 공식문서] ISR (Using Rages Router)
'개발공부 > ⬛ Next.js' 카테고리의 다른 글
[Next.js] Pages Router의 장단점 (3) | 2024.09.17 |
---|---|
[Next.js] SEO 적용하기[Next.js] SEO 적용하기 (Using Pages Router) (4) | 2024.09.16 |
[Next.js] Global Layout과 페이지별 레이아웃 적용하기 (Using Pages Router) (0) | 2024.09.10 |
[Next.js] 스타일링 (2) | 2024.09.07 |
[Next.js] 프리페칭 (Pre-fetching) (0) | 2024.09.04 |
프론트엔드 개발자 삐롱히의 개발 & 공부 기록 블로그