NextJs의 getInitialProps

?·2021년 7월 1일
3

웹 페이지는 각 페이지마다 사전에 불러와야할 데이터들이 있다. Data Fectching이라고도 하는 이 로직은 CSR(Client Side Rendering)에서는 react 로직에 따라 componentDidMount or useEffect로 컴포넌트가 마운트 되고 나서 하는 경우가 많다.

완성된 view를 만들기 위해서는 redux나 mobx, 또는 cotnext API 등을 사용한 store에서 해당 state를 받아와야 하고, 해당 state를 업데이트하기 위해 rendering 이후 componentDidMount 또는 useEffect를 통한 re-rendering 작업 이후 초기 값을 setting해 줬다. 

하지만 SSR은 re-rendering이 아니라, 서버에서 이 과정을 미리 처리하여 완성된 html을 한 번 rendering하는 것이 목적이다. 이 것을 가능하게 해주는 것이 getIntitialProps이다.
(사실 Data Fetching에만 getInitialProps를 사용할 수 있는 것은 아니다.)

Next 9.3 버전에서는 getInitialProps 대신에 getStaticProps, getStaticPaths, getServerSideProps 를 사용하게 된다고 한다.

getInitialProps의 LifeCycle

getInitialProps는 서버(Nodejs환경)와 클라이언트(Browser)에서 모두 호출되지만, 한 번에 호출되지는 않는다. 다시말해, route로 접근할 때 새로고침이나 직접 URL을 입력하는 방식의 Server Side Rendering으로 접근하면 서버에서, Next.js에서 제공하는 Link 컴포넌트를 통해 접근한다면 클라이언트에서 호출된다.

이러한 특징 때문에 getInitialProps는 page directory 안에 선언한 컴포넌트에서 호출해야한다. 해당 컴포넌트들의 자식 컴포넌트에서는 사용할 수 없다.

getInitialProps, 목적에 따른 사용법

또한 목적에 따라서 사용법이 다르다. 해당 페이지에만 미리 데이터를 불러오는 로직을 넣을 것인지, 혹은 전체 페이지에 대해 동일한 Data Fetching을 할 것인지를 정해야 한다. 이는 기획에 따라 달라지는 부분이다. 공통된 Data Fetching이 필요하다면 _app.js에 getInitialProps를 붙이면 된다. 페이지마다 다른 Data가 필요하다면 페이지마다 getInitialProps를 붙이면 된다.

사용시 주의할 점

  • getInitialProps 내부 로직은 서버에서 실행된다. 따라서 Client에서만 가능한 로직은 피해야 한다. (Window, document 등)
  • 한 페이지를 로드할 때, 하나의 getInitialProps 로직만 실행된다. 즉, _app.js에 getInitialProps를 달아서 사용한다면 그 하부 페이지의 getInitialProps는 실행되지 않는다. 다만, 커스터마이징을 통해, 최종 결과를 pageProps에 담을 수 있는데 이 방법은 밑에서 자세히 설명하도록 한다.

각 페이지에 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;

_app.js의 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 };
    // 여기서 리턴한 값은 getInitialProps를 호출한 컴포넌트의 props로 들어가게 된다. 
	}

	render() {
	  const { Component, pageProps, router } = this.props;
    
	 return (
	    <div>
		<Component {...pageProps} />
	    </div>
	   );
	}
};

함수형 예제

// src/_app.tsx
import "./globals.css";

function MyApp({ Component, pageProps }) {
  return <Component ponent {...pageProps} />;
}

MyApp.getInitialProps = async ({ Component, ctx }) => {
  let pageProps = {};
  // 하위 컴포넌트에 getInitialProps가 있다면 추가 (각 개별 컴포넌트에서 사용할 값 추가)
  if (Component.getInitialProps) {
    pageProps = await Component.getInitialProps(ctx);
  }

  // _app에서 props 추가 (모든 컴포넌트에서 공통적으로 사용할 값 추가)
  pageProps = { ...pageProps, posttt: { title: 11111, content: 3333 } };

  return { pageProps };
  // 여기서 리턴한 값은 getInitialProps를 호출한 컴포넌트의 props로 들어가게 된다. 
};

export default MyApp;

세부 컴포넌트의 getInitialProps

세부 컴포넌트에는 getInitialProps를 통해 _app.js에서 넘겨준 ctx를 사용하면 된다. 보통 redux로 dispatch를 해서 정보를 가져오는 등의 처리를 한다. 다시 강조하자면 getInitialProps가 return한 값은 해당 컴포넌트의 props로 들어가게 된다.

// id props는 getInitialProps를 통해 받은 props입니다.
const User = ({id}) => {
  const { mainPosts } = useSelector(state => state.post);
  const { userInfo } = useSelector(state => state.user);

  return (
      ...생략
  );
};


// context는 _app.js를 통해 Component.getInitialProps(ctx);로 받은 값입니다.
User.getInitialProps = async (context) => {

  // context에는 req, res, query, pathname, asPath, store 등이 있습니다.
  // store는 우리가 넣어준 redux store를 의미합니다.

  // _app.js에서 넘겨준 ctx가 context로 들어온다.
  // 여기서 프론트 서버로 부터 넘겨 받은 값을 사용하면 된다.
  const id = parseInt(context.query.id, 10);
  
  // 여기서는 redux를 이용해서 자동적으로 action을 날렸다.
  context.store.dispatch({
    type: LOAD_USER_REQUEST,
    data: id,
  });
  context.store.dispatch({
    type: LOAD_USER_POSTS_REQUEST,
    data: id,
  });
  
  // return한 값은 해당 컴포넌트의 props로 들어가게 됩니다.
  return { id };
};



ServerSide Cycle

우선 getInitialProps가 Server side 환경에서 어떻게 동작하는지 알아보자

  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 형태로 출력한다.
  8. 위의 과정으로 server logic이 실행이 된다.

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

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




추가설명

_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를 활용하자.

profile
?

0개의 댓글