React 재활훈련- 11일차, Nextjs SSR, SSG

0

react

목록 보기
11/11

https://www.udemy.com/course/react-next-master/

Nextjs의 SSR(server side rendering)

SSR은 서버 측에서 page를 렌더링하는 것을 의미한다.

-------------------                         ---------
|Nextjs App server|                         |Browser|
-------------------                         ---------
        | <-----  mypage.html 요청  ----------  |
        | ------ mypage.html 반환   --------->  |
        |                                    ---------------
        |                                    |페이지를 화면에|
        |                                    | 렌더링       |
        |                                    ---------------
        |                                       |
        | ------ Javascript Bundle ------------>|
        |                                       |
        |                                    -------------------
        |                                    |interaction에 따른|
        |                                    | component 렌더링 |
        |                                    -------------------
        |                                       |

순서를 정리하면 다음과 같다.

  1. Browser가 nextjs app server에 mypage.html을 요청
  2. nextjs는 react component와 html, css를 통해 mypage.html을 만들어 반환
  3. browser가 mypage.html을 받아 렌더링
  4. 이후 browser에 js bundle이 전달됨
  5. browser의 interaction으로 component가 렌더링, 이때 전달받은 js bundle을 통해서 react component를 업데이트하는 것이다.

nextjs를 통해서 SSR, CSR, SSG(정적 사이트 생성)을 page별로 선택적으로 설정해볼 수 있다. 지금은 SSR을 적용시켜보도록 하자.

적용 방식이 조금 재밌는데 SSR을 적용시키려는 component에 getServerSideProps함수를 선언하고 export시켜야 한다. 여기서 반환값으로 props property는 적용 객체에게 props로 전달되는 특징이 있다. 즉, getServerSideProps을 통해서 props가 될 값들을 미리 받아내어, html파일을 만들어내 client에게 전달하는 것이다.

  • pages/index.js
export default function Home({ name }) {
  return (
    <div>
      {name}
    </div>
  )
}

// SSR이 적용된다. SSR을 위해 서버측에서 component에게 전달할 데이터를 설정하는 함수
// 반환되는 props는 바로 component에게 전달된다. 
export const getServerSideProps = async () => {
  return {
    props: {
      name: "KOREA",
    }
  }
}

다음의 예시를 보도록 하자. Home component에게 SSR을 적용시키기 위해서 getServerSideProps 함수를 선언해놓도록 하는 것이다. 반환값으로 전달한 props.nameHome component의 props로 전달된다. 따라서 name이라는 값으로 접근이 가능한 것이다.

중요한 것은 getServerSideProps은 반드시 nextjs server안에서만 호출된다는 것이다. 때문에 console.log로 msg를 찍어도 browser에 찍히지 않을 것이다. 대신 우리의 server log에 쭉 찍힐 것이다.

따라서, browser환경에서만 쓸 수 있는 window 객체라던지 browser에 특화된 객체들을 쓸 수가 없다. 이는 getServerSideProps함수 뿐만 아니라 SSR적용을 받은 component역시도 마찬가지이다. 단, SSR 적용을 받은 component에 console.log를 찍어보면 server에서도 찍히고 browser에서도 찍히는 것을 볼 수 있을 것이다.

export default function Home({ name }) {
  console.log("HOME")
  return (
    <div>
      {name}
    </div>
  )
}

// SSR이 적용된다. SSR을 위해 서버측에서 component에게 전달할 데이터를 설정하는 함수
// 반환되는 props는 바로 component에게 전달된다. 
export const getServerSideProps = async () => {
  return {
    props: {
      name: "KOREA",
    }
  }
}

먼저 nextjs server에서 SSR을 위해 react component를 하나의 html파일로 뭉칠 때, server에 console.log가 찍힌 것이고, browser에서는 이 html파일을 화면에 렌더링하는 과정에서 console.log가 찍히는 것이다. 그래서 SSR적용을 받은 component도 browser에서 console.log가 찍히는 것이다.

만약, client측에서만 console.log를 찍게 만들고 싶다면 useEffect를 쓰면 된다.

import { useEffect } from "react"

export default function Home({ name }) {

  useEffect(() => {
    console.log("HOME")
  },[])
  
  return (
    <div>
      {name}
    </div>
  )
}

// SSR이 적용된다. SSR을 위해 서버측에서 component에게 전달할 데이터를 설정하는 함수
// 반환되는 props는 바로 component에게 전달된다. 
export const getServerSideProps = async () => {
  return {
    props: {
      name: "KOREA",
    }
  }
}

이렇게 쓰면 useEffect는 nextjs 서버에서는 실행되지 않고, component가 browser에 마운트되는 딱 그때에만 실행되기 때문에 console.log가 server측에는 찍히지 않는다.

SSR component의 경우 실행 순서 상 무조건 server측에서의 component 조립으로 html파일을 만드는 시간이 먼저이다. 따라서, 외부로부터 정보를 가져오거나하는 로직은 server측에서 실행시켜주는 편이 좋다.

import { fetchCountries } from "@/api"


export default function Home({ countries }) {
  return (
    <div>
      {countries.map((country) => {
        return <div key={country.code}>{country.commonName}</div>
      })}
    </div>
  )
}

export const getServerSideProps = async () => {
  const countries = await fetchCountries()
  return {
    props: {
      countries
    }
  }
}

다음과 같이 fetchCountries는 외부 API server로부터 요청을 보내, 나라에 대한 정보들을 담은 배열을 반환해준다. 해당 부분을 Home component의 props로 전달해주기 위해 server에서 먼저 data를 받고 return문으로 반환해주면된다.

getServerSideProps의 Context객체

그런데, 만약 query string과 같은 data가 필요하다면 어떻게해야할까?? 이럴 때 사용하는 것이 Context객체이다. Context객체는 browser에서 nextjs server로 요청을 보낸 정보들을 담은 배열로 query string이나 url과 같은 정보들을 담고 있다.

import { fetchSearchResults } from "@/api"
import SubLayout from "@/components/SubLayout"

export default function Search({countries}) {
    return (
        <div>
            {countries.map((country) => {
                return <div key={country.code}>{country.commonName}</div>
            })}
        </div>
    )
}

Search.Layout = SubLayout

// context객체는 browser에게 받은 정보들이 담긴다.
export const getServerSideProps = async (context) => {
    const { q } = context.query
    let countries = []
    if (q) {
        countries = await fetchSearchResults(q)
    }

    return {
        props: {
            countries
        }
    }
}

다음의 Search component는 /search?q=kor로 요청이 오면 렌더링된다. 이 때, query string인 q값을 nextjs server 측에서 알고 있어야 하기 때문에 q에 대한 정보를 담은 객체가 필요한데, 이것이 context이다. context안에서 query 객체가 바로 query string을 담은 object이다.

URL parameter(path parameter) 역시도 마찬가지이다. Context 객체안에 params로 parameter들이 정의되어있어, 이를 활용하면 된다. 가령 /country/KOR이라는 요청이 올 때 Country component를 렌더링해준다고 하자.

  • [code].js
import { fetchCountry } from "@/api"
import SubLayout from "@/components/SubLayout"

export default function Country({country}) {
    return (
        <div>
            {country.commonName} {country.officalName}
        </div>
    )
}

Country.Layout = SubLayout

export const getServerSideProps = async (context) => {
    const {code} = context.params

    let country = null
    if (code) {
        country = await fetchCountry(code)
    }

    return {
        props: {
            country
        }
    }
}

위 예제에서 context.params를 통해서 URL parameter를 얻어오는 것을 볼 수 있다. 해당 파일의 이름이 [code].js이기 때문에 URL parameter인 codecontext.params.code에 저장된다. 따라서 /country/KORKOR값은 code에 저장되는 것이다.

여기서 이전에 살펴본 useRouterContext객체가 무슨 차이인지 궁금할 수 있을 것 같다.

  • pages/country/[code].js
import { useRouter } from "next/router"

export default function Country() {
    const router = useRouter()
    const { code } = router.query
    return (
        <div>Country {code}</div>
    )
}

위의 useRouterContext객체와 동일하게 query string, URL parameter들을 가져올 수 있었다. 그럼 Context객체와 무슨 차이일까?? 다음과 같이 정리하면 된다.

  1. Context객체는 SSR component의 getServerSideProps에서만 사용할 수 있다. 즉, server측에서 Context객체를 얻고 SSR component를 js,css,html과 조합해 하나의 페이지를 만드는 것이다.
  2. useRouter는 component가 browser에 렌더링 될 때, 실행된다. 따라서 browser에서 실행되는 것이다.

결론적으로 둘 다 같이 쓸 수 있지만, SSR의 이점을 살리기 위해서는 Context객체를 통해 먼저 data를 받아놓고 전달해주는 것이 좋다. 그러면 browser입장에서는 html 파일만 렌더링하면 끝이기 때문이다.

SSG(정적 사이트 생성)

SSR은 서버 측에서 페이지를 렌더링하는 것이라면, SSG는 서버 측에서 page를 빌드 타임에 한번만 렌더링 하는 것이다. 즉, SSR은 매번 페이지를 렌더링한다는 것이고, SSG는 딱 한 번만 빌드해놓은 결과를 전달해준다는 것이다.

 nextjs                               browser
   |                                     | 
   |                                     |
  빌드(page 생성-mypage.html)             |
   |                                      |
   |  <---------/mypage 요청              |
   |  -----------mypage.html 반환 ------> |
   |                                      |
   |                                  browser에 렌더링
   |  ----------------js bundle --------->|
   |                                      |
   |                                      |
   |                                 js code와 html 요소 연결

js bundle 전달 이후는 SSR과 동일하지만, 이전이 다르다. SSR의 경우는 browser의 요청이 오면 그때서야 js, css, html파일을 합쳐서 html파일을 생성한 다음 browser에 전달한다. 반면, SSG는 browser가 요청이 오기도 전 시점인, 빌드 타임에 page html파일을 만들어내고 browser의 요청에 page html파일을 전달해준다.

이후 js bundle을 전달하여 page내의 component 렌더링은 CSR과 동일하게 렌더링된다. 이 부분은 SSR이든 SSG이든 동일하다.

SSG는 미리 만들어놓은 page를 반환하여, 빠른 페이지 응답을 보장할 수 있지만 최신 데이터를 반영하기는 어렵기 때문에, page내부 data가 변경되지 않은 페이지에 사용하기 적절한 렌더링 전략이다.

SSG를 사용하는 방법은 SSR과 마찬가지로 react component에 함수를 하나 선언해주면 된다. SSR이 getServerSideProps함수를 선언해야했다면 SSG는 getStaticProps를 선언해주면 된다. 로직과 반환값은 동일하지만, 빌드 타임에 딱 한번만 실행된다는 특징이 있다.

import { fetchCountries } from "@/api"

export default function Home({ countries }) {
  return (
    <div>
      {countries.map((country) => {
        return <div key={country.code}>{country.commonName}</div>
      })}
    </div>
  )
}

//SSG -> page를 서버측에서 렌더링하기 위해서 propr를 넘겨준다.
//단, 빌드 time에만 딱 한번 실행되기 때문에 새로고침을 해도 SSR처럼 다시 실행되지 않는다. 
export const getStaticProps = async () => {
  const countries = await fetchCountries()
  return {
    props: {
      countries
    }
  }
}

단, npm run dev로 실행할 시에는 SSG의 효과를 볼 수 없다. 동작이 마치 SSR과 동일하게 동작할 것이다. 따라서, npm run build를 통해서 결과를 확인해보도록 하자.

npm run build

빌드 후의 결과를 보도록 하자.

Route (pages)                              Size     First Load JS
┌ ● /                                      306 B          75.3 kB
├   /_app                                  0 B              75 kB
├ ○ /404                                   182 B          75.1 kB
├ ○ /about                                 264 B          75.2 kB
├ λ /api/hello                             0 B              75 kB
├ λ /country/[code]                        436 B          75.4 kB
└ λ /search                                490 B          75.4 kB
...
λ  (Server)  server-side renders at runtime (uses getInitialProps or getServerSideProps)
○  (Static)  automatically rendered as static HTML (uses no initial props)
●  (SSG)     automatically generated as static HTML + JSON (uses getStaticProps)

λ는 SSR 는 SSG, 는 SSG인데 props가 없는 경우라고 생각하면 된다. 따라서, 기본적으로 SSG방식으로 빌드를 한다는 것을 알 수 있다.

빌드된 결과를 실행해보도록 하자.

npm run start

문제없이 실행될 것이고, 굉장히 빠른 속도로 page가 렌더링될 것이다. 이는 static하게 만들어진 page를 전달하기 때문이다.

SSG를 사용할 때 조심해야할 것이있다. 만약, page에 동적으로 변경되는 data가 있는 경우는 SSG를 사용하면 error가 발생할 수 있다는 것이다. 가령 [code].js와 같이 동적 path를 가지는 경우가 그렇다.

이렇게 동적 경로를 가지는 page를 처리하는 getStaticPath라는 함수도 있지만, 그닥 추천하진 않는다. 이 정도는 SSR로도 충분히 처리가 가능하기 때문이다.

SSG를 사용할 때, 일정 주기로 page를 재생성하는 기술이 있다. 이를 ISR(incremental static regeneration) 증분 정적 재생성이라고 한다. 말은 좀 어려운데, 일정 시간이 지나면 새로 page를 만들어 browser에 제공한다는 것이다.

----project build---->nextjs        browser
                      |               |
                      |               |
                   page 생성 <ㅡ요청ㅡ |
                      |               | 
                   생성한 page 전달ㅡㅡ>
                      |               |
                      |               |
                      |               |
  page updateㅡㅡㅡㅡㅡ>               |
                   page 생성 <ㅡ요청ㅡ |
                      |               | 
                   생성한 page 전달ㅡㅡ>

처음 빌드한 SSG로 인해 처음 빌드된 page가 있다면 계속해서 browser의 요청에는 같은 page를 반환할 것이다. 그러나 ISR을 사용한다면 정해진 시간(가령 60초) 이후에 새로 빌드한 page를 만들어 새로운 data를 담은 page를 전달하는 것이다.

ISR은 이미 만들어져 있는 page를 반환하여 매무 빠른 속도로 렌더링한다는 점에서는 SSG의 장점과 같지만, 동적 data를 다루기 어렵다는 SSG의 문제를 해결해준다.

사용 방법은 매우 간단한데, getStaticPropsreturn문안에 revalidate를 설정하면 된다.

export const getStaticProps = async (context) => {
    const {code} = context.params

    let country = null
    if (code) {
        country = await fetchCountry(code)
    }

    return {
        props: {
            country
        },
        revalidate: 3
    }
}

이렇게 만들면, 3초마다 새롭게 page를 재생성하는 것이다.

0개의 댓글