Next.js14를 기준으로 App router와 Pages router를 각각 CSR/SSR/SSG/ISR을 구현해 봤다.
왜 굳이 둘 다 한 거죠...?라고 한다면
처음에는 App router로 구현하여 동작과정을 확인하려고 했다. 하지만 내 예상과 다르게 동작해 둘 다 구현했다.
실제로 둘 다 구현해 보니... 차이점이 있었다!
구현에 사용한 예시는 API에서 시간을 받아와 화면에 보여주는 페이지이다.
CSR (Client Side Rendering)
사용자가 페이지에 진입할 때, 서버로부터 빈 HTML을 받아오고 이후 브라우저에서 JS를 통해 필요한 데이터를 API로 받아와 화면을 그린다. 즉, 초기 로딩 시에는 빈 화면이 나타나고 JS가 실행되고 데이터가 로드된 후에 콘텐츠가 나타난다.
예시 코드
import { useState, useEffect } from "react";
import TimeZone from "@/components/TimeZone";
import styles from "../../styles/Home.module.css";
export default function CSRPage() {
const [dateTime, setDateTime] = useState<string | null>(null);
useEffect(() => {
fetch("https://worldtimeapi.org/api/ip")
.then((response) => response.json())
.then((data) => {
setDateTime(data.datetime);
})
.catch((error) => console.error(error));
}, []);
return (
<main className={styles.detail}>
<TimeZone title="CSR" datetime={dateTime} />
</main>
);
}
- 페이지 진입 시 API 호출을 통해 data을 받아와 보여준다.
- API 통신이 완료되기 전까지 LOADING이 나타난다.
- 페이지 요청이 있을 때마다 데이터를 가져오므로 새로고침할 때마다 API 통신을 통해 새로운 시간을 가져온다.
SSR (Server Side Rendering)
사용자가 페이지를 요청할 때마다 서버에서 API를 요청을 하고 HTML을 생성한다. 서버는 완성된 HTML을 클라이언트에 전달해 준다.
예시 코드
type SSRPageProps = {
datetime: string;
};
export async function getServerSideProps() {
const res = await fetch("https://worldtimeapi.org/api/ip");
const { datetime } = await res.json();
return {
props: {
datetime,
},
};
}
export default function SSRPage({ datetime }: SSRPageProps) {
return (
<main className={styles.detail}>
<TimeZone title="SSR" datetime={datetime} />
</main>
);
}
- 페이지가 렌더링 되기 전에 서버에서 API 호출해서 data를 받아오고 완전한 html을 그려준다.
- 서버에서 HTML을 만들기 전까지 약간의 지연이 있지만 로딩 없이 페이지가 나타난다.
- 페이지 요청이 있을 때마다 데이터를 가져오므로 새로고침할 때마다 API 통신을 통해 새로운 시간을 가져온다.
SSG (Static Site Generator)
빌드 시 HTML을 생성하고 매 요청마다 이 HTML을 재사용한다. 서버 부하를 줄이고 빠른 응답 속도를 제공한다.
반면, SSR은 매 요청마다 HTML을 생성해 응답 속도가 느리고 서버에 부담이 갈 수 있다.
getStaticPaths
동적 라우팅이 필요한 경우(/post/[id].tsx) getStaticPaths를 활용해 경로에 따라 다른 페이지를 보여줄 수 있다.
fallback
동적 페이지 빌드 시, 생성되지 않은 주소로 사용자가 요청을 보낼 때 fallback설정에 따라 대응하게 된다.
- true
빌드 시 생성되지 않은 정적 페이지를 요청하는 경우 일단 fallback 페이지를 제공한다.
이후 서버에서 정적 페이지를 생성하고 생성되면 사용자에게 해당 페이지를 제공한다. - false
빌드 시 생성되지 않은 정적 페이지를 요청하는 경우 404 페이지를 제공한다. - blocking
빌드 시 생성되지 않은 정적 페이지를 요청하는 경우 서버에서 SSR으로 페이지를 생성해 제공한다.
예시 코드
type SSGPageProps = {
datetime: string;
};
export async function getStaticProps() {
const res = await fetch("https://worldtimeapi.org/api/ip");
const { datetime } = await res.json();
return {
props: {
datetime,
},
};
}
export default function SSGPage({ datetime }: SSGPageProps) {
return (
<main className={styles.detail}>
<TimeZone title="SSG" datetime={datetime} />
</main>
);
}
- API 호출해서 받아온 data가 현재 시간과 차이가 있다. (빌드 시 API를 통해 받아온 데이터를 보여주기 때문에)
- 로딩 없이 바로 페이지가 나타나고 새로고침해도 아무런 변화가 없다.
ISR (Incremental-Static-Regeneration)
SSG는 빌드 시 모든 페이지를 생성해 데이터가 변경되면 전체 페이지를 다시 빌드해야 하지만,
ISR은 일정 시간마다 특정 페이지만 다시 빌드해 페이지를 업데이트할 수 있다.
예시 코드
type ISRPageProps = {
datetime: string;
};
export async function getStaticProps() {
const res = await fetch("https://worldtimeapi.org/api/ip");
const { datetime } = await res.json();
return {
props: {
datetime,
},
revalidate: 20,
};
}
export default function ISRPage({ datetime }: ISRPageProps) {
return (
<main className={styles.detail}>
<TimeZone title="ISR" datetime={datetime} />
</main>
);
}
- 처음에는 21:33:09이고 바로 새로고침하는 경우 data가 변경되지 않는다.
- 약 20초 뒤 새로고침 시 21:33:34로 변경된 걸 확인할 수 있다.
ISR은 재검증 시간(revalidate) 동안 데이터를 새로 받아오지 않는다. 이는 페이지가 Cooldown 상태이기 때문이다.
현재 revalidate가 20초로 설정되어 있어 20초 동안은 데이터가 갱신되지 않는다. 20초 이후 새로고침 하면 백그라운드에서 API통신을 통해 페이지를 새로 구축한다. 이때 사용자는 기존 페이지를 계속 보게 되며 페이지 재구축이 완료된 후 새로고침하면 변경된 페이지를 확인할 수 있다.
20초마다 자동으로 재구축되는 건가??
아니다!
Coldown이 해제되고 해당 페이지를 방문하는 사용자가 없다면 20초가 지나도 페이지는 재구축되지 않는다.
Coldown이 해제된 상태에서 누군가 방문한다면 백그라운드에서 재구축을 시작한다. 이 사용자는 변경된 페이지를 볼 수 없고 변경사항은 다음부터 확인할 수 있다.
빌드 결과에서 ISR 20초로 설정된 걸 확인할 수 있다!
언제 무엇을 사용하면 좋을까?
CSR
- SEO가 크게 중요하지 않은 경우
- 사용자와 상호작용이 많은 페이지(필터링, 실시간 업데이트 등)
SSR
- SEO가 중요하고, 최신의 데이터를 유지하기 위해 매 요청마다 서버 렌더링이 필요한 경우
- 초기 로딩 속도가 중요한 경우
- (메인, 제품 상세 페이지 등)
SSG
- 콘텐츠가 자주 변경되지 않는 정적인 페이지로 빠른 초기 로딩이 필요한 경우 (블로그 등)
ISR
- 자주 변경되지 않거나 데이터가 최신 상태가 아니어도 문제없는 경우
- 정적 사이트의 장점을 유지하면서 주기적으로 데이터 업데이트가 필요한 페이지
Next.js14 App router
getServerSideProps, getStaticProps 대신에 App router에서는 fetch()를 사용한다.
SSR (getServerSideProps)
async function getData(): Promise<TimeResponse> {
const res = await fetch("https://worldtimeapi.org/api/ip", {
cache: "no-store", // 캐시 되지 않도록
});
if (!res.ok) {
throw new Error("Failed to fetch data");
}
return res.json();
}
export default async function SSRPage() {
const { datetime } = await getData();
return (
<main className={styles.detail}>
<TimeZone title="SSR" datetime={datetime} />
</main>
);
}
SSG (getStaticProps)
async function getData(): Promise<TimeResponse> {
const res = await fetch("https://worldtimeapi.org/api/ip"); // default가 cache: 'force-cache'
if (!res.ok) {
throw new Error("Failed to fetch data");
}
return res.json();
}
export default async function SSGPage() {
const { datetime } = await getData();
return (
<main className={styles.detail}>
<TimeZone title="SSG" datetime={datetime} />
</main>
);
}
ISR
async function getData(): Promise<TimeResponse> {
const res = await fetch("https://worldtimeapi.org/api/ip", {
next: { revalidate: 20 },
});
if (!res.ok) {
throw new Error("Failed to fetch data");
}
return res.json();
}
export default async function ISRPage() {
const { datetime } = await getData();
return (
<main className={styles.detail}>
<TimeZone title="ISR" datetime={datetime} />
</main>
);
}
Pages Router와 App Router 차이
Pages Router의 경우 SSR과 ISR로 만들어진 페이지에 Link로 진입 시 data를 새로 받아와 보여줬다.
App Router에서 SSR과 ISR로 만들어진 페이지는 Link로 진입 시 이전에 데이터를 보여주고 새로고침 시에만 data를 새로 받아와 페이지를 보여줬다.
확실하지 않다... 캐싱이랑 관련 있을 거 같은데 정확한 이유는 찾지 못했다...
(App Router는 클라이언트 측 성능 최적화를 위해 캐싱을 적극적으로 활용한다. 처음 로드된 데이터를 캐시해 빠른 페이지 전환을 제공하는 반면, Pages Router는 각 페이지 전환 시마다 서버에서 데이터를 새로 가져오는 방식을 사용한다.)
참고자료
'IT > Next.js' 카테고리의 다른 글
[Next.js] rewrites로 CORS 해결하기 (2) | 2023.11.09 |
---|