학습 과정을 처음부터 끝까지 쓰기엔 너무 길고 무리가 있을것같다. 그래서 next의 핵심인 정적페이지 생성방법에 대해 써보려고한다.
next에서 지원하는 페이지 생성방법은 csr을 포함한 총 4가지가 있다.
각각의 방법에따라 차이가 있으니 자신에게 맞는걸 쓰면된다. 이 블로그같은 경우 ISR 방식을 선택했다.
export async function getServerSideProps() {
return {
props: {}, // 여기서 반환한 데이터를 page 컴포넌트에서 사용
}
}
ssr을 적용할 page 컴포넌트에서 getServerSideProps 라는 이름의 함수를 export 시켜줘야한다. 해당 함수에서 반환하는 데이터를 기반으로 next에서 html을 생성해서 클라이언트에게 전달해준다. 이 함수는 클라이언트가 페이지를 요청할때마다 매번 next 서버에서 실행되며 진짜 ssr 이라고 부를만한 방식이다. 근데 솔직히 써본적이 없어서 자세히 모름.
export async function getStaticProps() {
return {
props: {}, // 여기서 반환한 데이터를 page 컴포넌트에서 사용
}
}
getServerSideProps 와 마찬가지로 적용할 page 컴포넌트에서 export 해줘야하며 여기서 반환된 데이터로 정적
인 페이지를 생성한다.
설명하자면 이 getStaticProps 라는 함수는 next build
명령어를 실행했을때 딱 한번만 실행되며(next dev 같은 개발 환경에서는 매번 실행됨)
이때 생성된 페이지를 가지고 있다가 같은 url 요청이면 이미 만들어진 페이지를 내보낸다.
간단한 예를 들어보자
//getStaticProps를 이용한 유저 정보 페이지
const userInfo=(props)=>{
return(
<div>
<p>이름:{props.name}</p> //아래의 props를 받아 실제 html에는 '이름' 과 23 이라는 숫자가 렌더링된다.
<p>나이:{props.age}</p>
</div>
}
export async function getStaticProps() {
//여기서는 하드코딩한 데이터를 리턴했지만 실제로는 fetch나 axios 같은 패키지로 api 요청을하여 페이지에 필요한 데이터를 리턴한다
return{
props:{
name:'이름',
age:23
}
}
}
이 /user
라는 경로의 userInfo 라는 페이지는 next build 실행시 getStaticProps를 단 한번만 실행하고 만들어진 html 파일을 저장하고 있다가
다시한번 /user
요청이 들어오면 빌드할때 만들어진 페이지를 반환하는 것이다. 그래서 동적으로 변하는 내용이 페이지에 있다면 적합하지 않다.
이 방법을 next는 SSG(Static Site Generation) 이라고 부르며 next에서 권장하는 방식이다.
그래서 아래에 ISR 이라는 방법이 있다. 근데 그전에 getStaticPath 라는거 먼저 알아야됨
위의 getStaticProps와 함께 쓰이는 함수다. getStaticProps를 실행할 페이지가 동적인 url의 경우 정적으로 생성할 목록을 여기서 리턴해줘야한다.
/post/[pid]
와같은 경로가 있다면 /post/1
, /post/2
같이 경로의 수가 여럿일수도 있다.
그래서 이 [pid]
의 목록을 getStaticPaths 에서 리턴하면 그 목록대로 빌드할때 getStaticProps를 호출하여 정적 페이지를 생성한다.
// pages/posts/[id].js
// paths에서 1,2 배열을 리턴하면 `/posts/1` 과 `/posts/2` 가 생성된다
// 여기서 주의할점은 경로가 `/posts/[id].js 라면 `params` 안에 반드시 id 라는 값이 있어야한다.
export async function getStaticPaths() {
return {
paths: [{ params: { id: '1' } }, { params: { id: '2' } }],
fallback: false, // can also be true or 'blocking'
}
}
// paths 에서 리턴한 params를 받는다
export async function getStaticProps({params}) {
const post=await getPost(params.id) //params로 받은 id로 실제 포스트를 가져온다
return {
props: { post: {...post} },
}
}
getStaticPaths의 옵션중 fallback 이라는 값이 있다. 이 값으로 빌드할때 생성하지 않는 페이지에 대해 어떻게 처리할지를 정한다
앞서 얘기했듯이 /post/[pid]
경로가 있을때 paths로 [1,2] 를 반환했다. 그러면 /post/1
과 /post/2
는 생성이 될껀데 나중에 /post/3
요청이 들어왔을때의 얘기다.
false - 빌드할때 생성되지 않는 모든 요청에 대해 404를 리턴한다.
true -
/post/3
페이지를 생성한다.blocking - true와 비슷하나 페이지가 완성될때까지 응답이 없고 페이지가 완성되면 응답하기때문에 사용자는 기다렸다가 바로 완성된 페이지를 보게된다.
빌드할때만 페이지를 생성하는거면
/posts/[id]
와 같이 계속해서 새로운 경로가 생기는 경우는 어떻게 처리하나요?
이 부분이 사실 fallback에서 설명하는 부분이다. getStaticPaths에 [1,2] 를 전달했다면 빌드시 getStaticProps는 params에 id를 /1 과 /2 전달받아 페이지를 생성하고 이후 /3 의 주소로 요청을 한다면 자동으로 getStaticProps의 id 값은 3이 전달 되고 /3 페이지를 생성하게 된다. 이때 fallback 설정에 따라 페이지가 완성되고 응답할지 fallback 페이지를 보여줄지 결정한다. 참고로 fallback이 false 라면 동적인 페이지 생성없이 빌드할때 없던 페이지는 전부 404를 반환한다.
이름은 거창한데 별거 없다. 그냥 getStaticProps의 리턴값에 revalidate 를 추가하면 끝이다.
export async function getStaticProps() {
return {
props: {...},
revalidate:60 //초 단위로 동작한다
}
}
이렇게 설정해두면 1분마다 새로운 정적페이지를 생성하여 최신상태를 유지한다. 페이지를 생성하고 60초가 지나기전에 같은 요청이 온다면 만들어뒀던 페이지를 전달하고 60초가 지난상태에서 요청이 들어온다면 새로운 페이지를 만들어서 전달한뒤 최신페이지를 캐시로 남겨둔다.
이 얼마나 효율적인 방식인가, 블로그같이 페이지 내용이 수정될 일이 적은 사이트의 경우 이 방법이 제격이다.
로컬에서 ISR을 적용한 페이지를 테스트 하던중 revalidate가 적용되지 않는 문제가 생겼다.
분명 빌드할때에도 ISR 10초 라고 콘솔에 떴지만 새로운 내용으로 갱신이 안돼더라. 스택오버플로우에 나와 같은 문제에 대한 질문이 있었다. 내용을 보니 참 어이가 없는 경우였는데.
next의 문제가 아니라 apollo client 서버의 캐시 설정때문에 이런 문제가 발생하는거였다고 자문자답이 달려있었다. 나도 똑같이 캐시 비활성화 하니 동작은 하더라... 그런데 의문이 생겼다. 대체 graphql 에서 캐시를 설정한게 왜 이런 문제가 생기고 어떻게 해결하는지는 알수가없었다. 하지만 당장 블로그를 완성하는게 급해서 일단은 이렇게 처리를해두고 넘어가서 이 문제는 아직까지 미스터리로 남아있다. 나중에 시간이 있다면 까먹지 말고 꼭 알아봐야할 문제인거 같다.
한번은 포스팅을 쓰기위한 텍스트 에디터를 적용하려다가 오류가 생겨 애를 먹은적이 있다.
이 블로그는 tui-editor를 사용중이고 next에서 그대로 임포트 하여 사용하려니
window is not defined
이라는 에러가 발생하는것이었다.
이유를 알아보니 tui editor가 실행될때 브라우저의 window 객체가 필요한데 next 서버 환경에서 먼저 페이지를 렌더링하는 도중
window 객체가 없어서 이러한 문제가 발생하는것이었다.
이 문제를 해결하기 위해 tui editor를 바로 import 하는것이 아니라 클라이언트 환경에서 동적으로 임포트를 하는 방법이다.
next/dynamic
이라는 모듈로 동적인 임포트를 구현할수있다. 공식문서에 나온대로 ssr 옵션을 false로 주면 클라이언트에서만 임포트를 한다.
import dynamic from 'next/dynamic'
const DynamicHeader = dynamic(() => import('../components/header'), {
ssr: false,
})