Next js가 React Project의 SSR을 가능하게 한다.
라고는 하는데, 어떤 방식으로 SSR을 가능하게 할까, SSR과 CSR의 구분은 어떻게 되어 있을까.
이 궁금증을 해결하기 위해, 먼저 알아야 할 것은 Next js의 구동방식 이라고 생각한다.
최초에 Next 서버로 요청이 들어왔을 때, Next 서버에서는 요청이 들어온 페이지에 들어갈 데이터를 Fetch하고 Html을 구성하여 Client로 보내준다. 그 과정을 알아보자.
최초로 실행되는 녀석들이다. 사실 이 두 녀석은 없어도 된다. Next 자체에서 제공하는 로직으로 실행되기 때문이다. 하지만 항상 프로젝트를 진행하다보면 자체 제공 로직은 언젠가 커스터마이징하기 마련이다. 커스터마이징을 하기 위해서는 pages
폴더에 각각 _app.js, _document.js 파일을 생성하고 코드를 작성하면 된다.
위의 두 파일은 server only file이다. Next server logic에 사용되는 파일이라는 뜻으로 client에서 사용하는 로직(ex. eventlistener 등의 window / DOM 로직)을 사용하면 안된다. window is not defined
라는 에러를 보았다면 해당 사항을 체크해보길 바란다.
최초로 실행되는 것은 _app.js 이다. _app.js는 client에서 띄우길 바라는 전체 컴포넌트의 레이아웃으로 이해하면 쉽다. 공통 레이아웃 이므로 최초에 실행되어 내부에 들어갈 컴포넌트들을 실행한다. 내부에 Content 들이 있다면 전부 실행하고 Html의 Body로 구성한다.
다음 샘플 코드로 설명하자면,
function MyApp({ Component, pageProps }) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
);
}
export default MyApp;
여기서 props로 받은 Component는 요청한 페이지이다. GET /
요청을 보냈다면, Component 에는 /pages/index.js
파일이 props로 내려오게 된다. pageProps는 페이지 getInitialProps를 통해 내려 받은 props들을 말하는데, 이는 getInitialProps 파트에서 자세히 설명하겠다.
그 다음 _documents.js가 실행된다. _document.js는 static html를 구성하기 위한 _app.js에서 구성한 Html body가 어떤 형태로 들어갈지 구성하는 곳이다. Content들을 브라우저가 html로 이해하도록 구조화 시켜주는 곳이라고 이해하면 쉽다.
다음 샘플 코드로 설명하자면,
import Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDocument extends Document {
render() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument
_app.js가 실행되면서 갖추어진 content들은 Main Component 아래에 생성된다.
기억하자!
_documents.js
에 어플리케이션 로직을 넣지 말자. 브라우저는 Main을 제외한 다른 component들을 initialize하지 않는다. 공통된 어플리케이션 로직이 필요하다면, _app.js를 활용하자.
// next js docs 발췌
React components outside of <Main /> will not be initialized by the browser. Do not add application logic here. If you need shared components in all your pages (like a menu or a toolbar), take a look at the App component instead
웹 페이지는 각 페이지마다 사전에 불러와야할 데이터들이 있다. Data Fectching이라고도 하는 로직은 CSR(Client Side Rendering)에서는 react 로직에 따라 componentDidMount or useEffect
로 컴포넌트가 마운트 되고 나서 하는 경우가 많다. 이 과정을 서버에서 미리 처리하도록 도와주는 것이 바로 getInitialProps이다. (사실 Data Fetching에만 getInitialProps를 사용할 수 있는 것은 아니다.)
Next 9.3 버전에서는 getInitialProps
를 대신에 getStaticProps
, getStaticPaths
, getServerSideProps
를 사용하게 된다고 한다. 각각의 용법은 다르지만, 서버에서 페이지의 연산을 미리 한다는 점은 동일하므로 따로 언급하지는 않겠다. 각각의 로직이 어떤 역할을 하는지는 따로 포스팅할 기회가 있을지도...
Data Fetching을 서버에서 하게 되면 첫째, 속도가 빨라진다. 브라우저에서의 연산을 서버와 함께 하면서 미리 데이터를 받아오고 브라우저는 렌더링만 할 수 있기 때문이다. 둘째, 코드 상의 처리가 깔끔해진다. 데이터가 꼭 필요한 페이지의 경우 브라우저가 데이터를 가져올 때까지 화면 렌더링을 잠시 null 처리하는 경우가 있다. 이 과정이 없어지고, Initial한 데이터가 들어오는 과정을 전제로 코드를 작성할 수 있다.
목적에 따라서 사용법이 다르다. 해당 페이지에만 미리 데이터를 불러오는 로직을 넣을 것인지, 혹은 전체 페이지에 대해 동일한 Data Fetching을 할 것인지를 정해야 한다. 이는 기획에 따라 달라지는 부분이다. 공통된 Data Fetching이 필요하다면 _app.js
에 getInitialProps를 붙이면 된다. 페이지마다 다른 Data가 필요하다면 페이지마다 getInitialProps를 붙이면 된다. 먼저 각 페이지마다 getInitialProps를 붙이는 방법은 다음과 같다.
import axios from 'axios';
const Page = ({ stars }) => {
return <div>Next stars: {stars}</div>;
};
Page.getInitialProps = async ctx => {
const { data } = await axios.get('...url');
return { stars: data };
}
export default Page;
export default class MyApp extends App {
static async getInitialProps({ Component, ctx }) {
let pageProps = {};
// 실행하고자 하는 component에 getInitialprops가 있으면 실행하여 props를 받아올 수 있다.
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
return {
pageProps
};
}
render() {
const { Component, pageProps, router } = this.props;
return (
<div>
<Component {...pageProps} />
</div>
);
}
};
getInitialProps들은 기본적으로 받는 props가 있다. 이를 context(ctx)라고 한다.
ctx Object의 기본 구성은 다음과 같다.
pathname
- 현재 pathname /user?type=normal
page 접속 시에는 /user
query
- 현재 query를 object형태로 출력 /user?type=normal
page 접속 시에는 {type: 'normal'}
asPath
- 전체 path /user?type=normal
page 접속 시에는 /user?type=normal
req
- HTTP request object (server only)res
- HTTP response object (server only)err
- Error object if any error is encountered during the rendering위의 과정으로 server logic이 실행이 된다. 이 순서가 가끔 헷갈려서 서버 상에 로직이 생각과 다르게 진행되는 경우가 많다. 브라우저 console에도 안찍히는 로직이므로, 디버깅이 어렵다는 단점도 있다.
** 조사하면서 느낀 것들.
getInitialProps
를 이용하여 컴포넌트들 마다 initial props들을 실행하도록 하는 로직은 먼소린가... 했었는데 저런 깊은 뜻이 있더라.. 역시 legacy code는 다 이유가 있다.By Cyrano on MAR 15, 2020.
한준이형 잘보고갑니다!