Next js 구동방식 과 getInitialProps

박한준·2020년 3월 15일
65

Front-End-dev

목록 보기
6/8
post-thumbnail

Next js가 React Project의 SSR을 가능하게 한다. 라고는 하는데, 어떤 방식으로 SSR을 가능하게 할까, SSR과 CSR의 구분은 어떻게 되어 있을까.

이 궁금증을 해결하기 위해, 먼저 알아야 할 것은 Next js의 구동방식 이라고 생각한다.

Server Side

최초에 Next 서버로 요청이 들어왔을 때, Next 서버에서는 요청이 들어온 페이지에 들어갈 데이터를 Fetch하고 Html을 구성하여 Client로 보내준다. 그 과정을 알아보자.

_app.js 와 _document.js

최초로 실행되는 녀석들이다. 사실 이 두 녀석은 없어도 된다. 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

getInitialProps

웹 페이지는 각 페이지마다 사전에 불러와야할 데이터들이 있다. 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;

사용시 주의할 점

  • getInitialProps 내부 로직은 서버에서 실행된다. 따라서 Client에서만 가능한 로직은 피해야 한다. (Window, document 등)
  • 한 페이지를 로드할 때, 하나의 getInitialProps 로직만 실행된다. 예를 들어 _app.js에 getInitialProps를 달아서 사용한다면 그 하부 페이지의 getInitialProps는 실행되지 않는다. 다만, 다음과 같이 커스터마이징을 하면, 최종 결과를 pageProps에 담을 수 있다.
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>

		);
	}
};

default Props

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

ServerSide Cycle

  1. Next Server가 GET 요청을 받는다.
  2. 요청에 맞는 Page를 찾는다.
  3. _app.js의 getInitialProps가 있다면 실행한다.
  4. Page Component의 getInitialProps가 있다면 실행한다. pageProps들을 받아온다.
  5. _document.js의 getInitialProps가 있다면 실행한다. pageProps들을 받아온다.
  6. 모든 props들을 구성하고, _app.js > page Component 순서로 rendering.
  7. 모든 Content를 구성하고 _document.js를 실행하여 html 형태로 출력한다.

위의 과정으로 server logic이 실행이 된다. 이 순서가 가끔 헷갈려서 서버 상에 로직이 생각과 다르게 진행되는 경우가 많다. 브라우저 console에도 안찍히는 로직이므로, 디버깅이 어렵다는 단점도 있다.

** 조사하면서 느낀 것들.

  • Next js docs가 생각보다 친절하게 되어있다. 몇 줄 안 적혀있는데 필요한 부분을 꼭 집어서 설명해주는 docs 인듯하다. 약간 츤데레 스타일
  • 현재 프로젝트가 next의 SSR을 이용하는데, 이해 못했던 SSR 로직들을 이해하는 계기가 되었다. 특히 _app.js에서 getInitialProps를 이용하여 컴포넌트들 마다 initial props들을 실행하도록 하는 로직은 먼소린가... 했었는데 저런 깊은 뜻이 있더라.. 역시 legacy code는 다 이유가 있다.

By Cyrano on MAR 15, 2020.

profile
개발자로 한걸음 한걸음 가고 있어요.

2개의 댓글

comment-user-thumbnail
2020년 11월 10일

한준이형 잘보고갑니다!

답글 달기