Archive

Next.JS의 서버사이드 렌더링 (feat.redux-saga)

|

[Next.JS] 서버사이드 렌더링 + redux-saga


1. Next 서버사이드 렌더링

getInitialProps (deprecated 예정)

getStaticProps : 언제 접속하든 같은 데이터를 받을 때 사용한다

import { END } from "redux-saga";
//...

export const getStaticProps = wrapper.getStaticProps(async (ctx) => {
  ctx.store.dispatch(loadPostsRequest());
  ctx.store.dispatch(END);
  await ctx.store.sagaTask.toPromise();
});

//...


getStaticPaths : Dynamic Routing일 때, getStaticProps쓰면 오류남, getStaticPaths 사용해야한다

import { useRouter } from 'next/router';

const Post = () => {
    const router = useRouter();

    if(router.isFallback) {
        return <div>로딩중...</div>
    }
    //...
};

export async function getStaticPaths() {
    return {
        paths: [
            { params: {id: '1' }},

        ],
        fallback: true, // false면 paths에 없는 값으로 진입하면 에러남
    };
};

export const getStaticProps = wrapper.getStaticProps(async (ctx) => {
    //...
});

Dynamic Routing에서 id값을 path에 지정해서 미리 HTML로 만들어놓고 갖고 있는다. fallback이 true일 때,

path에 없는 id로 진입하면 router의 isFallback으로 로딩 처리를 해줄 수 있고 그 동안 getStaticProps로 없는 id에

대한 데이터를 가져올 수 있다.


getServerSideProps : 접속한 상황에 따라 다른 데이터를 받을 때 사용한다

import { END } from "redux-saga";
// ...

export const getServerSideProps = wrapper.getServerSideProps(async (ctx) => {
  ctx.store.dispatch(loginRequestAction());
  //...
  ctx.store.dispatch(END);
  await ctx.store.sagaTask.toPromise();
});

//...

wrapper는 next-redux-wrapper 패키지의 createWrapper() 메소드로 만든 객체로, store가 들어있다.

getServerSideProps의 콜백의 파라미터인 ctx 안에는 store가 들어있어서 접근할 수 있다.

화면이 렌더링 되기 전에 위 로직이 먼저 실행되며 getServerSideProps 안에서 action이 dispatch되면 rootReducer의 switch문 중 HYDRATE case에 걸리게 된다.

import { combineReducers } from "react-redux";

const rootReducer = (state, action) => {
  switch (action.type) {
    case HYDRATE:
      return action.payload;
    default: {
      const combinedReducer = combineReducers({
        user,
        post,
      });
      return combineReducer(state, action);
    }
  }
};
export default rootReducer;


// ...

export const getServerSideProps = wrapper.getServerSideProps(async (ctx) => {
  const cookie = ctx.req ? ctx.req.headers.cookie : "";
  axios.defaults.headers.Cookie = "";
  if (ctx.req && cookie) {
    axios.defaults.headers.Cookie = cookie;
  }
});

//...

Next 서버가 실행되면 ctx.req가 존재하게 되는데 여기에 다양하 정보가 들어있다.

주의할 점은 getServerSideProps는 클라이언트가 아닌 프론트 서버에서 실행되는 로직이므로 잘못하면 사용자 정보를 공유해버릴 수도 있다는 점이다. 위에서도 쿠키를 헤더에 설정할 때, 사용자끼리 겹치지 않도록 분기를 해주었다.


2. Dynamic Routing (Next 9부터 지원)

pages 폴더 밑에 url 구조에 맞춰 폴더 설계를 할 수 있다.

ex) localhost:3000/post/3 => src/pages/post/[id].js

파일명의 [value] 안의 value 안의 값을 다음과 같이 받아올 수 있다.

서버사이드 props에서도 ctx.params.value, 혹은 ctx.query.value로 값을 받아올 수 있다.

import { useRouter } from 'next/router';

const Post = () => {
    const router = useRouter();
    const { id } = router.query;
    // ...
};

export const getServerSideProps = wrapper.getServerSideProps(async (ctx) => {
    ctx.store.dispatch({
        loadPostRequest(ctx.params.id);
    });
});


3. CSS의 SSR적용 + styled-components

next 자체적으로 babel과 webpack을 지원하고 있지만 이를 커스터마이징하는 것도 가능하다

.babelrc 파일을 root 디렉토리에 만든다 (webpack은 next.config.js)

{
    "presets": ["next/babel"],
    "plugins": [
        ["babel-plugin-styled-components", {
            "ssr": true,
            "displayName": true
        }]
    ]
}


pages 폴더 아래에 _document.js 파일 생성한다

(_document.js는 최초 SSR 시 한 번 실행되므로 다이나믹한 리소스들은 안쓰는 것이 좋다.

주로 meta 태그 설정, CDN 호출, 언어 설정 등을 정의한다.)

(이유는 모르나 class형 컴포넌트로 써야한다)

import React from 'react';
import Document, { Html, Head, body, Main, NextScript} from 'next/document';
import { ServerStyleSheet } from 'styled-components';

export default class MyDocument extends Document {
    static async getInitialProps(ctx) {
		const sheet = new ServerStyleSheet();
        const originalRenderPage = ctx.renderPage;

        try {
            ctx.renderPage = () => originalRenderPage({
                enhanceApp: (App) => (props) => sheet.collectStyles(<App {...props} />)
            });
        	const initialProps = await Document.getInitialProps(ctx);
            return {
            	...initialProps,
              	styles: (
                	<>
                    	{initialProps.styles}
                    	{sheet.getStyleElement()}
                    </>
                )
            }
        } catch(err) {
            // ...
        } finally {
            sheet.seal();
        }
    }

    render() {
        <Html>
            <Head />
            <body>
            	<Main />
                <NextScript />
            </body>
        </Html>
    }
}


4. global window

window 객체 접근 시, 서버사이드에서는 이를 알 수 없기에 TypeError가 난다.

따라서, useEffect 안에서 사용해야한다. useEffect는 클라이언트 사이드 전용 이벤트이다.



참고 자료


zerocho - react-nodebird