사전에
HTML
을 미리 만드는 방식
Next.js
는 사전에 HTML
을 미리 렌더링하는 프리렌더링(Pre-Rendering)
기능을 제공한다.
프리렌더링은 사전에 HTML
을 미리 생성한 뒤 최소한의 JavaScript
만을 연결한다.
이 후 Browser
에서 요청이 들어오면 미리 생성한 HTML
을 로드하여 나머지 JavaScript
를 연결해 화면에 렌더링한다.
그래서 프리렌더링은 성능, 검색엔진 최적화(Search Engine Optimization, SEO)
...등의 기능을 향상시킨다.
Next.js
는 다음과 같은 프리렌더링 방법을 제공하며 각각의 page에서 getServerSideProps
,getStaticProps
, getStaticPaths
...등의 메서드를 사용하여 구현한다.
Server-Side Rendering:
SSR
Static Site Rendering:SSG
매 요청시
HTML
이 생성
getServerSideProps
는 요청할때마다 HTML
이 생성되기 때문에 데이터가 계속 업데이트 된다.
즉, 빌드와 상관없이, 매 요청마다 데이터를 서버로부터 가져온다.
그래서 초기 로딩속도가 빠르며, 컨텐츠를 빠르게 확인할 수 있는 효과를 준다.
현재 예제는 다음과 같이 CSR
방식을 사용하여, 화면이 로딩된 후 useEffect
를 통해 게시글 데이터를 받아온다.
그래서 잠깐의 데이터 공백이 발생하는데, 이 때문에 사용자는 화면에서 이질감을 느낀다.
const Home = () => {
useEffect(() => { // CSR방식을 통해 게시글의 정보를 받아온다.
dispatch({
type: LOAD_TOP_POSTS_REQUEST
});
dispatch({
type: LOAD_RECENT_POSTS_REQUEST
});
}, []);
return (
<AppLayout>
:
:
:
</AppLayout>
)
};
export default Home;
이 때 getServerSideProps
를 사용하면 첫 화면 랜더링 전에 데이터를 미리 가져와 위에서 언급한 CSR
방식의 문제를 해결할 수 있다.
import wrapper from '../store/configureStore'; // next-redux-wrapper 불러오기
import { END } from 'redux-saga'; // END액션 불러오기
const Home = () => {
// useEffect(() => {
// dispatch({
// type: LOAD_TOP_POSTS_REQUEST
// });
// dispatch({
// type: LOAD_RECENT_POSTS_REQUEST
// });
// }, []);
return (
<AppLayout>
:
:
:
</AppLayout>
)
};
// 기존의 Home컴포넌트보다 먼저 실행
export const getServerSideProps = wrapper.getServerSideProps((context) => {
console.log(context); // context는 요청/응답, SSR에 관련된 정보가 들어있는 객체
// context.store는 임의로 context 객체에 넣은 redux store
context.store.dispatch({
type: LOAD_TOP_POSTS_REQUEST,
});
context.store.dispatch({
type: LOAD_RECENT_POSTS_REQUEST,
});
// END액션은 비동기 action이 REQUEST가 SUCCESS되기까지 대기
context.store.dispatch(END);
await context.store.sagaTask.toPromise();
});
export default Home;
getServerSideProps
는 페이지 호출시마다 요청되기 때문에, context
에는 request
파라미터가 포함되어 있다.
context
는 request
이외에도 아래와 같은 값들을 사용할 수 있다.
Parameter | 역활 |
---|---|
params | 다이나믹 페이지의 파라미터 |
req | HTTP Request객체 |
res | HTTP Response객체 |
query | 요청 주소의 쿼리 스트링 |
preview | page preview모드 |
previewData | setPreviewData메서드에 의해 설정되는 값 |
getServerSideProps
에서 dispatch
를 하면 store
에 변화가 생긴다.
그 변화의 결과는 Reducer
의 HYDRATE
로 전달된다.
// reducers/index.js
import { HYDRATE } from 'next-redux-wrapper';
import { combineReducers } from 'redux';
import user from './user'
import post from './post'
const rootReducer = combineReducers({
index: (state = {}, action) => {
switch (action.type) {
case HYDRATE: // 해당 코드가 실행
return {
...state,
...action.payload
};
default:
return state;
}
},
user,
post,
});
export default rootReducer;
그래서 데이터를 정상적으로 전달받기 위해서는 rootReducer
의 구조를 변경하여 모든 상태를 덮어씌울 수 있도록 다음과 같이 수정해야 한다.
import { HYDRATE } from 'next-redux-wrapper';
import { combineReducers } from 'redux';
import user from './user'
import post from './post'
const rootReducer = (state, action) => {
switch (action.type) {
case HYDRATE:
console.log('HYDRATE', action);
return action.payload;
default: {
// combineReducers는 user, post Reducer를 병합
const combineReducer = combineReducers({
user,
post,
});
// 병합된 combineReducers를 index와 병합
return combineReducer(state, action);
}
}
}
Reducer
를 수정한 뒤 getServerSideProps
를 실행하면 다음과 같이 요청로그에 Cookie
가 포함되지 않는것을 확인할 수 있다.
현재 예제에는 다음과 같이 Browser
에서 Backend Server
로 요청을 보낼 때 Cookie
를 포함하도록 설정했다.
// back/app.js
app.use(cors({
origin: 'http://localhost:3060',
credentials: true,
}));
그럼에도 쿠키가 담겨져있지 않다는 것은 getServerSideProps
는 Browser
가 아닌 온전히 Front Server
의 작업이기 때문이다.
그래서 다음과 같이 getServerSideProps
메서드를 통해 axios
와 같은 서버 요청을할 때는 직접 Cookie
를 포함하여 실행해야 한다.
import axios from 'axios'; // axios 불러오기
export default getServerSideProps = wrapper.getServerSideProps(async (context) => {
const cookie = context.req ? context.req.headers.cookie : '';
// 쿠키를 사용하지 않을 때는 서버에서 공유하고 있는 쿠키를 제거
axios.defaults.headers.Cookie = '';
if (context.req && cookie) { // 서버 또는 쿠키가 존재할 때
axios.defaults.headers.Cookie = cookie; // 쿠키를 Backend Server로 전달
}
context.store.dispatch({
type: LOAD_TOP_POSTS_REQUEST,
});
context.store.dispatch({
type: LOAD_RECENT_POSTS_REQUEST,
});
context.store.dispatch(END);
await context.store.sagaTask.toPromise();
});
위의 과정을 거쳐 getServerSideProps
를 실행하면 HYDRATE
Action
이 실행되고, 그 결과는 다음과 같이 Redux Devtools
에서 확인할 수 있다.
빌드타임에
HTML
을 미리 생성
getStaticProps
는 HTML
이 빌드타임에 생성된다.
빌드시에 데이터를 가져와 HTML
을 미리 생성한 뒤 사용자의 요청이 들어오면 빌드된 HTML
을 재사용한다.
그래서 getStaticProps
는 데이터가 변하지않는 정적 페이지에 사용하기 적합하다.
getStaticProps
는 getServerSideProps
와 사용방법이 유사하다.
다만 차이점이 있다면 getServerSideProps
에서는 Browser
에서 Cookie
를 포함하여 Next.js
서버로 오기 때문에 Cookie
를 가지고 올 수 있다.
하지만 getStaticProps
는 빌드 시에 실행되기 때문에 Cookie
를 가지고 올 수 없다.
import { END } from 'redux-saga';
import axios from 'axios';
export default getStaticProps = wrapper.getStaticProps(async (context) => {
context.store.dispatch({
type: LOAD_POST_REQUEST,
data: 1,
});
context.store.dispatch(END);
await context.store.sagaTask.toPromise();
});
페이지 요청에 따라 동적으로 데이터가 계속 업데이트된다면 getServerSideProps
를 사용하는것이 좋다.
반대로 페이지 요청을 해도 데이터가 변화없이 정적으로 렌더링된다면 gettStaticProps
를 사용하는것을 권장한다.
데이터에 따라
Pre-Rendering
할 페이지의 동적 경로를 지정
다이나믹 라우팅 페이지에서 getStaticProps
를 단독으로 사용하면 다음과 같은 에러가 발생한다.
해당 에러는 렌더링하기 위해 필요한 경로를 설정하지 않아 발생하는 에러다.
그래서 위와 같은 에러를 해결하기 위해 getStaticPaths
를 사용한다.
getStaticPaths
는 paths
, fallback
을 return
하며 각각의 요소는 다음과 같은 역활을 담당한다.
paths
: 빌드 시 생성되어야 하는 경로 지정
fallback
: 특정 페이지가 Next.js Cache에 존재하지 않는 경우 수행할 작업
// pages/user/[id].js
import { END } from 'redux-saga';
import axios from 'axios';
if (router.isFallback) { // fallback 실행시 대기화면
return <div>로딩중...</div>;
}
export async function getStaticPaths() {
return {
paths: [
{ params: { id: '1' } }, // 1번 게시글이 미리 빌드
{ params: { id: '2' } },
{ params: { id: '3' } },
],
fallback: true, // paths에 없는 경로 서버 요청여부
}
}
export default getStaticProps = wrapper.getStaticProps(async (context) => {
const cookie = context.req ? context.req.headers.cookie : '';
console.log(context);
axios.defaults.headers.Cookie = '';
if (context.req && cookie) {
axios.defaults.headers.Cookie = cookie;
}
context.store.dispatch({
type: LOAD_MY_INFO_REQUEST,
});
context.store.dispatch({
type: LOAD_POSTS_REQUEST,
data: context.params.id, // getStaticPaths에서 작성한 게시글 id
});
context.store.dispatch(END);
await context.store.sagaTask.toPromise();
});