📢 어렵고 정석적인 개념 설명보다는 저같은 초보자도 이해하기 쉽게 정리하는 것을 원칙으로 포스팅하고 있습니다. 😄
CSR, SSR, SSG는 뭐가 다를까?
우리가 리액트를 공부할 때, CSR(클라이언트 사이드 렌더링)을 배우면서 SSR도 같이 들어본 적이 있을 것이다.
CSR은 서버에서 비어있는 html 파일을 클라이언트에게 주면 클라이언트가 자바스크립트로 페이지를 그리는 방식이라면, SSR은 서버에서 렌더링 된 html 파일을 클라이언트에게 주면 클라이언트가 자바스크립트로 동적인 부분만 연결(Hydration)해 주는 방식이다.
그렇다면 SSG는 뭘까? SSR에서는 페이지에 접속할 때마다 html 파일을 렌더링 했지만, SSG는 이미 빌드하는 순간에 모든 페이지를 렌더링 한다. 즉, 클라이언트가 서버에게 페이지를 요청할 때 SSR은 "잠깐만! 렌더링 하고 금방 줄게!"라면, SSG는 "응~ 그럴 줄 알고 미리 만들어놨어~" 인 것이다. 따라서, SSG는 응답 속도가 굉장히 빠르다는 장점이 있다. 하지만, 미리 만들어 놓으면 최신 데이터를 반영하지 못하기 때문에 항상 똑같은 페이지만 반환한다는 단점도 있다. (이것은 ISR 방식으로 해결 가능하다.)
만약, 만들려는 페이지가 동적으로 받아오는 데이터가 많다면 SSR 방식이 좋을 것이고, 받아오는 데이터가 적고 반응 속도가 빨라야 하는 페이지라면 SSG 방식으로 만드는 게 좋다. 즉, 페이지의 역할에 맞게 취사 선택하면 된다.
SSR 방식 사용하기 : getServerSideProps()
export default Home({ name }) {
console.log("Mount Home"); // 서버와 브라우저 둘 다 출력
useEffect(() => {
console.log("Mount Only Browser"); // 브라우저에서만 출력
}, [])
return <div>{name}</div>
}
export const getServerSideProps = async (context) => {
console.log("getServerSideProps Called"); // 서버에서 출력
console.log(context); // URL 파라미터나 쿼리 스트링과 같은 요청들 출력
return {
props: {
name: 'Shawn'
}
}
}
기존의 CSR 방식에서 SSR 방식으로 바꾸고 싶다면, SSR 방식으로 구동시킬 페이지 컴포넌트 하단에 getServerSideProps()를 선언해 주면 된다. 이 함수는 서버에서 작동되는 메서드로 서버에서는 getServerSideProps()와 Home() 컴포넌트가 각각 한 번씩 호출된다. (서버에서 호출되는 코드에는 window.location과 같은 브라우저에서만 작동되는 윈도우 객체가 있으면 오류가 발생하니 주의하자!)
보통은 getServerSideProps()에서 API DB를 받아오고 이것을 Props로 Home 컴포넌트에 전달한다. 서버는 Home 페이지 컴포넌트를 호출해서 전달받은 Props가 사용되는 위치에 Props를 넣어주고 브라우저에게 생성된 home.html 파일을 전달한다.
getServerSideProps()는 반드시 객체를 return 해야 하며, 객체 안에는 props 프로퍼티가 존재해야 한다.
또한, 브라우저 단에서 URL 파라미터나 쿼리 스트링을 사용하고 싶을 때는 useRouter() Hook을 사용하면 됐지만, 서버에서는 useRouter() 대신 getServerSideProps()의 context 파라미터를 사용한다. context에는 쿼리 스트링을 비롯한 브라우저에게 요청한 값들을 모두 확인할 수 있다.
위의 예시 코드에서는 Home 컴포넌트와 getServerSideProps() 안에 각각 console.log가 찍혀있는 것을 볼 수 있다. 서버에서는 getServerSideProps()를 호출한 다음 Home 컴포넌트를 호출하기 때문에 두 곳의 console.log가 모두 서버에서 출력된다. 또한, Home 컴포넌트는 브라우저에서도 호출되기 때문에 브라우저 단에서도 같이 출력된다.
만약, Home 컴포넌트의 console.log가 브라우저 단에서만 찍히게 하고 싶다면 useEffect()를 이용해서 브라우저에 Mount 될 때만 출력될 수 있도록 처리하면 된다.
SSG 방식 사용하기 : getStaticProps(), getStaticPaths()
export default Home({ name }) {
const router = useRouter();
if (router.isFallback) {
return <div>로딩 중입니다...</div>
}
return <div>{name}</div>
}
export const getStaticProps = async () => {
return {
props: {
name: "Shawn"
}
}
}
// 동적 경로를 갖는 페이지를 SSG로 작동하고 싶을 때 : 미리 경로를 설정해줘야 함
export const getStaticPaths = async () => {
return {
path: [
{params: {code: "KOR"}},
{params: {code: "NOR"}}
],
fallback: false
}
}
SSG 사용 방식은 앞서 소개한 SSR 방식과 별반 다를 게 없다. 이름만 getStaticProps()로 바뀐 것뿐이지 똑같이 Props를 서버에서 전달받고 렌더링 한 html 파일을 브라우저 단에 전달한다. 다만, SSG 방식은 동적 라우팅을 대응하는 방식이 다르다. SSG는 빌드할 때 딱 한 번 모든 페이지를 만들어서 브라우저에 전달한다고 했었다. 이러한 이유로 동적 라우팅을 사용한다면 미리 어떤 페이지를 만들어 놔야 할지 경로를 설정해줘야 한다. (그래야 그 페이지를 미리 만들어 놓을 수 있기 때문에)
getStaticPaths()는 SSG 방식에서 미리 설정한 경로들의 페이지를 생성하는 역할을 해준다. return되는 객체 안에 path 배열을 만들고 그 안에 params 프로퍼티를 가진 객체를 선언해 준다. 참고로, params의 객체 key값이 code인 이유는 동적 페이지가 [code].js로 되어 있기 때문이다.
만약, [code]/[id].js처럼 동적 라우팅이 중첩으로 되어 있다면, {params: {code: "KOR", id: 1}} 이렇게 선언해 주면 된다.
그런데 getStaticPaths()로 설정되지 않은 페이지에 들어가면 SSG 방식에서는 어떻게 대응할까? 이와 관련된 속성이 바로 fallback이다. fallback 속성이 false인 경우, 'blocking'인 경우, true인 경우 이렇게 3가지 값에 따라 설정되지 않은 페이지로 들어갔을 때 서버가 대응하는 방식이 각각 다르다.
1) fallback: false
false 값인 경우 그냥 404 오류 페이지가 뜬다. 그냥 페이지가 없다는 오류로 당연히 만들어 놓지 않았으니까 보여줄 게 없다는 뜻이다.
2) fallback: 'blocking'
'blocking'으로 설정하면 SSR 방식처럼 실시간으로 페이지를 생성한다. 빌드 타임에 생성되지 않은 페이지들을 이렇게 실시간으로 생성한다는 장점이 있지만, 페이지가 생성되는 동안 로딩 시간이 발생한다는 단점도 있다. (이런 방식으로 한 번 생성된 페이지들은 다시 재접속했을 땐 미리 만들어진 페이지이기 때문에 로딩 시간이 발생하지 않는다.)
3) fallback: true
true 값을 사용하면 'blocking' 모드에서 페이지가 생성되는 로딩 시간을 조금 더 유연하게 다룰 수 있다. 'blocking' 모드에서는 페이지를 생성하는 동안 로딩 시간이 소요되는데 이것은 페이지를 생성한 다음 데이터를 페이지에 주입한 후 페이지를 렌더링 하는 시간이 걸리기 때문이다. 하지만, true로 설정하면 먼저 데이터가 없는 fallback 상태의 순수 html 파일을 사용자에게 보여주고, 그다음 데이터를 주입하여 페이지를 다시 렌더링 하게 된다.
브라우저 관점에서는 Props가 없는 버전의 html은 로딩할 게 거의 없기 때문에 페이지를 빠르게 렌더링 할 수 있다. 하지만, 데이터를 주입하는 과정에서 시간이 소요될 수 있으므로 페이지가 로딩 중임을 사용자에게 알리는 별도의 로딩 텍스트를 표시해야 할 수도 있다.
해당 페이지 컴포넌트 안에 if문과 useRouter()를 사용해서 if(router.isFallback) {return <div>로딩 중 입니다...</div>} 처리를 해주면 데이터가 입혀지는 동안에 로딩 중이라는 텍스트를 띄울 수 있다.
ISR 방식 사용하기 (SSG + SSR)
export const getStaticPaths = async () => {
return {
path: [
{params: {code: 'KOR'}}
],
fallback: false,
revalidate: 60 // 60초마다 페이지를 업데이트한다.
}
}
SSG의 단점은 페이지를 빌드 타임에 딱 한 번만 렌더링 하기 때문에 최신 데이터를 반영할 수 없다는 점이다. 그렇다고 SSR을 사용하기에는 SSG의 빠른 속도를 포기할 수 없다. 각각의 장점을 모두 더하면 얼마나 좋을까? ... 해서 나온 게 ISR(증분 정적 재생성)이다.
방법은 간단하다. getStaticPaths()에 revalidate 속성을 추가하면 된다. 이 속성의 값에는 '초'를 적어주면 되는데, 이것은 해당 값의 초마다 페이지를 업데이트해 준다는 의미이다. 만약 현재 페이지가 V1이라면 60초 후에 V1에 접속했을 때, V1을 페이지에 출력함과 동시에 V2 페이지를 생성한다. 그리고 다시 V1에 재접속했을 때부터 V2 페이지를 출력한다.
ISR은 이미 만들어져 있는 페이지를 반환하여 매우 빠른 속도로 렌더링 한다는 SSG의 장점과 주기적으로 업데이트하여 최신 데이터를 반영할 수 있다는 SSR의 장점을 합친 방식이다. (그냥 업데이트가 가능한 SSG 방식이라고 봐도 무방하다.)
// api/revalidate.ts (On-Demand ISR)
export default async function handler(req, res) {
try {
await res.revalidate("/board"); // board 페이지를 갱신한다.
return res.json({ revalidated: true });
} catch(err) {
return res.status(500).send('Error revalidating');
}
}
ISR 방식에서 특정 주기로 페이지를 갱신하도록 설정할 수 있다는 것에는 몇 가지 고려해야 할 점이 있다. 예를 들어, 게시판 페이지의 갱신 주기를 60초로 설정했을 경우, 만약 60초 이전에 게시글이 수정된다면 사용자는 수정된 내용을 바로 확인할 수가 없다. 60초가 지나야 페이지가 갱신되므로, 그 이후에야 수정된 게시글을 볼 수 있게 된다.
반대로, 갱신 주기를 60초로 설정했지만, 24시간 동안 게시글에 아무런 변화가 없을 경우에도 Next.js는 매 60초마다 페이지를 재생성하게 된다. 이것은 불필요한 리소스 사용으로 이어질 수 있다.
이러한 상황에서는 특정 주기가 아닌, 특정 이벤트가 발생할 때마다 페이지를 갱신하는 게 더 효율적이다. 이때 사용할 수 있는 방법이 바로 On-Demand ISR이다. On-Demand ISR은 정해진 주기가 아니라 필요한 순간에만 페이지를 재생성할 수 있도록 해준다.
위의 코드는 API 라우트를 사용하여 특정 페이지를 갱신하는 방법의 예시 코드이다. API 요청이므로, req와 res 객체를 사용하며 Next.js에서 제공하는 내장 메서드인 res.revalidate(갱신할 페이지의 경로)를 호출하여 페이지를 갱신한다. (게시판 페이지라고 한다면, 이 API는 게시글 수정이나 삭제 등 게시글의 변화가 있는 이벤트가 발생했을 때 호출하면 된다.) 이 방식으로 특정 페이지를 갱신하는 API를 만들어, 필요한 페이지에서 fetch를 이용해 이 API를 호출하면, 요청이 있을 때마다 페이지가 갱신된다.
🤔 Next.js에서 데이터 페칭 메서드(getServerSideProps, getStaticProps...)가 없으면 무슨 방식일까?
데이터 페칭 메서드가 없으면 기본적으로 SSG 방식으로 렌더링된다. 그러나 컴포넌트 내부에서 useEffect 등을 사용해
클라이언트 사이드에서 데이터를 가져오는 로직이 있다면, CSR 방식으로 동작한다.
1. SSR : 요청이 올 때마다 서버에서 페이지를 생성한 다음 브라우저에게 생성된 html 파일을 전달하는 것.
2. SSR 사용하기 : getServerSideProps(){...} 선언해 주기.
3. 서버에서 getServerSideProps 실행 -> 서버에서 해당 페이지 컴포넌트 호출 및 렌더링 -> 브라우저에게 전달
4. SSG : 빌드 타임에 서버에서 페이지를 모두 생성한 다음 브라우저에게 생성된 html 파일을 전달하는 것.
5. SSG 사용하기 : getStaticProps() / 동적 라우팅에는 getStaticPaths(){...} 선언해 주기.
6. SSG 동적 라우팅에서 없는 페이지에 들어갈 때 대응 방식에는 fallback 속성에 false, 'blocking', true 중 택1 하기.
7. ISR : SSG 방식에 주기적으로 페이지를 업데이트하는 방식을 더한 것. (SSR + SSG)
'Next.js' 카테고리의 다른 글
[Next.js] 에러 페이지 만들기 (404, 500, notFound, error) (0) | 2024.10.04 |
---|---|
[Next.js] 캐시 사용으로 성능 최적화하기 (데이터 캐시, 라우트 캐시 등) (1) | 2024.09.27 |
[Next.js] 이미지 넣는 방법 : Image 컴포넌트 활용하여 최적화하기 (0) | 2023.09.28 |
[Next.js] 레이아웃 구성하기 : 공통 요소들은 한 곳에서 관리하자! (0) | 2023.09.14 |
[Next.js] 페이지 라우팅하는 방법 (동적 라우팅, useRouter()) (0) | 2023.09.11 |