programming language/react

[React] Next.js 알아보기 - 2탄 : SSR 적용하기

공대키메라 2022. 7. 10. 23:48

지난 시간에 Next.js에 대해 알아보았다.
(궁금하면 여기 클릭!)

지난 Next.js 알아보기 - 1탄에 이어서 이번 시간에도 Next.js에 대해서 알아볼 것이다.

키메라가 정리하는 내용은 전부 공식 사이트에 있다.

다만 필자는 정리를 하면서 스스로 적용하는 것이 더 이해도 잘되고 기억에 오래남기 때문에

번거롭더라도 번역을 하면서 정리하고 있다.(그래서 약간 어색함 ㅎ...)

키메라가 정리한 내용을 읽을 필요 없이 다음 사이트를 참고하면 된다.

출처:
https://nextjs.org/learn/basics/data-fetching/blog-data
https://cpro95.tistory.com/492
https://www.youtube.com/watch?v=WAMqFdCFotY&ab_channel=SonnySangha
https://tsh.io/blog/ssr-vs-ssg-in-nextjs/

1. Next.js기능 - pre-rendering

필자가 Next.js를 사용하려는 이유는 다름아닌 pre-rendering기능 때문이다.

그냥 쌩 React로 하려고 하면 엄청 번거롭고 귀찮기 때문이다.

이와 관련된 조금의 사전 지식을 알면 좋을 내용을 전에 정리한 적이 있다. (궁금하면 여기 클릭!)

Next.js에서 이 pre-rendering에 대해 다음과 같이 설명하고 있다.

Next.js는 기본적으로 모든 페이지를 pre-rendering한다. (미리 그려낸다.)

이것은 Next.js가 전부 Client Side Javascript에 의해 그려지는것 대신에, 미리 각각의 페이지를 위해 HTML를 생성한다는것을 말한다.

Pre-rendering은 더 나은 성능과 Search Engine Optimization을 돕는다.


이를 위해서 Next.js에서는 hydration이라는 기능을 제공한다.

각각의 생성된 HTML은 그 페이지에 필요한 최소한의 javascript 코드와 연관되어있다.

페이지가 브라우저에서 로드될 때, 그 javascript 코드가 동작하고 그 페이지를 완전히 상호작용하도록 만든다.
(이 과정을 hydration이라고 한다.)


여기에 정말로 pre-rendering이 일어나는지 확인하려면 어떻게 해야하는지 설명을 해주고 있다.

궁금하면 더 찾아서 읽어보도록 하자.

필자는 여기서 어떻게 적용하는지만 알아보도록 하겠다.

Pre-rendering의 두 형태

Next.js 는 두 형태의 pre-rendering을 가진다.

Static generation 과 server side rendering이다.

차이점은 그것이 페이지에서 html을 생성할 때에 있다.

 

https://nextjs.org/static/images/learn/data-fetching/static-generation.png

 

https://nextjs.org/static/images/learn/data-fetching/server-side-rendering.png


Static Generation은 build-time에 생성되고 각각의 요청에서 재사용된다.

Server-side rendering은 각각의 요청에 생성된다.

=> 그러니까, 단순하게 보자면 Static generation은 정말 화면을 하나 그냥 프로젝트 build시에 딱! 만들어주는 것이고,SSR은 이제 어느 화면에 들어갈 때, 데이터가 필요한것을 가져와서 화면을 만들어 주는 것이다.당연히 SSR이 조금 느릴 수밖에 없다.

Next.js는 우리가 이 둘중 하나를 선택할 수 있게 한다.


그러면 어느 상황에서 Static Generation을 사용하고, SSR를 사용해야 하는지 친절하게 설명해준다.

Static Generation vs Server-side Rendering을 사용해야할 때

Static generation을 가능하면 사용하길 추천하는데, 이유는 당신의 페이지가 한 번 buil되고 CDN에 의해 제공될 수 있는데, 이것은 모든 요천마다 페이지를 만드는 server rendering보다 훨씬 빠르다.

Marketing 페이지 블로그, E-commerce 상품 리스팅, 도움과 설명 등에 Static generation을 사용할 수 있다.

스스로에게 질문하자. 내가 사용자의 요청보다 먼저 이 페이지를 그릴 수 있나? 만약에 답변이 그렇다 라면, Static generation을 선택해야 한다.

반면에, static generation은 사용자 요청보다 먼저 페이지를 그릴 수 없다면 좋은 선택이 아니다.
아마도 페이지가 자주 수정된 데이터를 보여주고 페이지 내용이 요청마다 변한다?

이런 경우에 Server-side rendering을 사용할 수 있다.

이건 더 느리지만, pre-rendered한 페이지는 항상 최신상태이다.

혹은, pre-rendering을 건나뛰고 client side javascript를 자주 최신화되는 데이터를 채우는데 사용할 수 있다.


정리를 하자면, 데이터가 변하지 않고 정적인 정보만 보여준다면 Static generation을, 요청이 올 때 마다 화면이 업데이트된다면 SSR을 추천한다고 한다.

SSR을 무조건 해야하는 곳은 로그인 여부를 확인하는 것이 있다.

CSR로만 이것을 구현하게 되면, 새로고침시에 매번 로그인 창이 보였다가, 쿠키나 토큰을 확인한 후에야 로그인이 확인이되서 로그인이 된다. 그러면 굉장히 사이트가 허졉해보인다.

2. Next.js - Static generation with and without data

Static Generation은 데이터 있이, 혹은 없이 될 수 있다.

여태까지, 우리가 생성해온 모든 페이지들은 외부 데이터를 가져올 필요가 없었다.

이러한 페이지들은 자동적으로 그 앱이 production으로 build될 때 정적으로 생성된다.

 

https://nextjs.org/static/images/learn/data-fetching/static-generation-without-data.png



데이터가 없는 page는 build되면 데이터를 가져오는 것 없이 HTML를 생성한다.

하지만, 몇몇 페이지에서, 외부 데이터를 처음에 가져오는 것 없이 HTML을 그릴 수 없다.

아마 당신은 build time에 file system에 접근하거나,, 외부 API에 접근하거나 또는 데이터베이스에서 쿼리를 실행해야할지 모른다.

Next.js는 이 경우 기존의 틀을 깨고, 데이터가 있는채로 Static Generation을 지원한다.

 

https://nextjs.org/static/images/learn/data-fetching/static-generation-with-data.png


데이터가 있는 경우에는 getStaticProps를 이용하면 된다.

2. Next.js - 프로젝트 세팅하기

이를 위한 예시도 공식사이트에서 알려주고 있으나, 굉장히 무언가... 따라가면서 하기 번거로웠다. (아오 개짜증나 ㅎㅎ)

그래서 필자가 기존에 사용하던 예시 프로젝트 내용을 적용한 새로운 Next.js 연습 프로젝트를 다시 생성해서 적용해볼 것이다.

그러면 예시를 통해서 어떻게 사용하는지 확인해 보도록 하겠다.

기존에 redux-toolkit 실습 프로젝트에 시간 관련 api 기능을 추가할 것이다. 이를 위해 다음 블로그를 참고했다.
(참고한 블로그 여기 클릭! - 좋은 글 감사합니다...)


우선 프로젝트를 다시 생성해보겠다.

먼저 next.js 프로젝트를 받는다.

npx create-next-app next-js-ssr-practice

그리고 필요한 라이브러리를 다운받는다.

npm i @reduxjs/tookit react-redux redux-saga

 

npm i next-redux-wrapper


참고로 react-redux는 기존에 독립적으로 사용하는 redux를 react에서 편리하게 사용할 수 있도록 지원하는 라이브러리고, next-redux-wrapper는 말 그대로 next.js에서 redux를 돌아가게끔 지원해주는 라이브러리다.

우리가 사용할 프로젝트 디렉토리는 다음과 같다.

3. Next.js - 연습 내용 세팅하기

pages/index.js

import Head from "next/head";
import Image from "next/image";
import React, { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import LoginForm from "../src/Login/LoginForm";
import Profile from "../src/Login/Profile";
import styles from "../styles/Home.module.css";

export default function Home() {
  const { loginDone } = useSelector((state) => state.user);
  const [todos, setTodos] = useState([]);

  useEffect(() => {
    const fetchTodos = async () => {
      const res = await fetch("https://jsonplaceholder.typicode.com/todos");
      const data = await res.json();
      setTodos(data);
    };
    fetchTodos();
  }, []);

  return (
    <div className={styles.container}>
      <Head>
        <title>키메라 next.js 공부하기!</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <div className={styles.grid}>
          <>{loginDone ? <Profile /> : <LoginForm />}</>
        </div>
        <div className={styles.grid}>
          {todos.length === 0 ? (
            <div>Loading...</div>
          ) : (
            todos.map((todo) => (
              <div key={todo.id}>
                <p>
                  {todo.id} : {todo.title}
                </p>
              </div>
            ))
          )}
        </div>
      </main>

      <footer className={styles.footer}>
        <a
          href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          Powered by{"공대키메라"}
          <span className={styles.logo}>
            <Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
          </span>
        </a>
      </footer>
    </div>
  );
}


맨 처음 index 페이지에 접속시에 loading이 찰나의 순간에 보이고, 그 다음에 fake data를 jsonplaceholder를 통해서 받아온다.

필자 키메라의 경우에는 이것을 이런 위글링이 일어나지 않고 좀 더 걸리더라도 페이지를 완전한 상태로 받고 싶다.

이런 경우에 밑에서 소개할 next.js의 기능인 getStaticProps와 getServerSideProps를 이용하면 된다.

src/store/store.js

import { configureStore } from "@reduxjs/toolkit";
import { userReducer } from "../reducers/user";
import createSagaMiddleware from "redux-saga";
import rootSaga from "../saga/index";
import logger from "../Logger/MyLogger";
import { createWrapper } from "next-redux-wrapper";

/*
밑에 코드와 동일하게 작동한다. 필자는 combinreReducers를 굳이 안써도 된다는 생각이므로 그냥 합치도록 지원이 되기에
이렇게 작성하지 않음.
const combinedReducer = combineReducers({
    userReducer,
});

export const store = () => {
    configureStore({
        reducer: combineReducer,
        middleware: [logger, saga],
    })
};

*/

export const createStore = () => {
  const saga = createSagaMiddleware();
  const store = configureStore({
    reducer: {
      user: userReducer,
    },
    middleware: [logger, saga],
  });
  saga.run(rootSaga);
  return store;
};

export const wrapper = createWrapper(createStore);


next.js에서 redux-saga의 기능을 추가하려면 next-redux-wrapper를 이용하면 된다.

pages/_app.js

import "../styles/globals.css";
import { wrapper } from "../src/store/store";
//_app.js파일은 다른 페이지들이 생성될 때 이 파일을 바탕으로 초기화됨.
function MyApp({ Component, pageProps }) {
  return (
    <>
      <Component {...pageProps} />
    </>
  );
}

export default wrapper.withRedux(MyApp);


그 외의 내용들은 지난번에 사용했던 예시 내용과 동일하다. 디렉토리는 조금 다르지만 파일 이름에 따른 내부 코드는 동일하다. (찾아보려면 여기 클릭!)

우선 여기까지 코드를 작성하고 실행하면 다음과 같이 보인다.

이것을 캡쳐하려고 고생좀 했다... ㅠㅠ...

어쨌든...

맨 처음에는 Loading...하는 것이 보이고... 잠시 후


위 모습처럼 데이터가 나오는 것을 확인할 수 있다.

이것은 Client Side Rendering으로 현재 페이지가 구동하기 때문이다.

이것을 이제 SSR 혹은 SSG로 변경할 것이다!

4. getStaticProps & getServerSideProps으로 정적 화면 생성하기

이제 기존 내용에 Next.js의 기능을 이용한 server-side rendering을 적용할 것이다.

pages/serverside.js

import axios from "axios";
import React, { useEffect } from "react";
import styles from "../styles/Home.module.css";

export default function serverSidePage({ todos }) {
  return (
    <>
      <div className={styles.grid}>
        {todos?.length === 0 ? (
          <div>Loading...</div>
        ) : (
          todos?.map((todo) => (
            <div key={todo.id}>
              <p>
                {todo.id} : {todo.title}
              </p>
            </div>
          ))
        )}
      </div>
    </>
  );
}

export async function getServerSideProps() {
  const res = await fetch("https://jsonplaceholder.typicode.com/todos");
  const data = await res.json();
  return {
    props: { todos: data },
  };
}

pages/staticsitegeneration.js

import axios from "axios";
import React, { useEffect } from "react";
import styles from "../styles/Home.module.css";

export default function staticSiteRender({ todos }) {
  return (
    <>
      <div className={styles.grid}>
        {todos?.length === 0 ? (
          <div>Loading...</div>
        ) : (
          todos?.map((todo) => (
            <div key={todo.id}>
              <p>
                {todo.id} : {todo.title}
              </p>
            </div>
          ))
        )}
      </div>
    </>
  );
}

export async function getStaticProps() {
  const res = await fetch("https://jsonplaceholder.typicode.com/todos");
  const data = await res.json();
  return {
    props: { todos: data },
  };
}


둘 다 나오는 화면은 동일하다.

다음처럼 말이다.


그래! 이제는 아까 Loading 문구가 화면이 보이기 전에 데이터를 이미 가져오므로 더이상 보이지 않는다.

그러면 SSR(server-side rendering)과 SSG(static site generation)의 차이는 뭐라고 해야 할까?

5. SSR(server-side rendering) VS SSG(static site generation)! What's the difference?

이에 대한 정보를 다음 사이트에서 찾을 수 있었다. (궁금하면 여기 클릭!)

공식 사이트에도 동일하게 설명을 하고 있다. 위에서도 설명했지만, 좀 더 정리해서 설명하자면 다음과 같다.

SSR과 SSG의 주요한 차이점은 나중의 경우에, runtime동안 보다는 HTML이 build time에 생성되는 것이다.

그러한 웹사이트들은 html 컨텐츠가 요청을 보내기 전에 생성되서 극단적으로 빠르다.

반면에, 웹사이트는 다시 build되야 하고 전체적으로 변화가 있을 때 다시 로딩해야한다.

결론적으로, SSG 기반의 웹사이트들은 SSR에 기반한 사이트들보다 훨씬 덜 반응적이고 원형 상태이다.

그들은 크게 동적이지 않은 콘텐츠가 약간 있는 정적인 사이트들이다.

 

https://tsh.io/wp-content/uploads/2022/03/ssr-ssg-overview.png


이렇게 next.js를 이용해서 어떻게 SSR를 구현하고, SSG도 구현하는지 알아보았다.

생각보다 정리하는데 쉽지는 않았지만 많은 정보가 있었기에 생각보다 수월하게 할 수 있었다.