https://www.udemy.com/course/react-next-master/
SSR은 서버 측에서 page를 렌더링하는 것을 의미한다.
------------------- ---------
|Nextjs App server| |Browser|
------------------- ---------
| <----- mypage.html 요청 ---------- |
| ------ mypage.html 반환 ---------> |
| ---------------
| |페이지를 화면에|
| | 렌더링 |
| ---------------
| |
| ------ Javascript Bundle ------------>|
| |
| -------------------
| |interaction에 따른|
| | component 렌더링 |
| -------------------
| |
순서를 정리하면 다음과 같다.
mypage.html
을 요청mypage.html
을 만들어 반환mypage.html
을 받아 렌더링nextjs를 통해서 SSR, CSR, SSG(정적 사이트 생성)을 page별로 선택적으로 설정해볼 수 있다. 지금은 SSR을 적용시켜보도록 하자.
적용 방식이 조금 재밌는데 SSR을 적용시키려는 component에 getServerSideProps
함수를 선언하고 export
시켜야 한다. 여기서 반환값으로 props
property는 적용 객체에게 props
로 전달되는 특징이 있다. 즉, getServerSideProps
을 통해서 props
가 될 값들을 미리 받아내어, html파일을 만들어내 client에게 전달하는 것이다.
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.name
은 Home
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
문으로 반환해주면된다.
그런데, 만약 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를 렌더링해준다고 하자.
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인 code
는 context.params.code
에 저장된다. 따라서 /country/KOR
의 KOR
값은 code
에 저장되는 것이다.
여기서 이전에 살펴본 useRouter
과 Context
객체가 무슨 차이인지 궁금할 수 있을 것 같다.
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>
)
}
위의 useRouter
도 Context
객체와 동일하게 query string, URL parameter들을 가져올 수 있었다. 그럼 Context
객체와 무슨 차이일까?? 다음과 같이 정리하면 된다.
Context
객체는 SSR component의 getServerSideProps
에서만 사용할 수 있다. 즉, server측에서 Context
객체를 얻고 SSR component를 js,css,html과 조합해 하나의 페이지를 만드는 것이다. useRouter
는 component가 browser에 렌더링 될 때, 실행된다. 따라서 browser에서 실행되는 것이다.결론적으로 둘 다 같이 쓸 수 있지만, SSR의 이점을 살리기 위해서는 Context
객체를 통해 먼저 data를 받아놓고 전달해주는 것이 좋다. 그러면 browser입장에서는 html 파일만 렌더링하면 끝이기 때문이다.
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의 문제를 해결해준다.
사용 방법은 매우 간단한데, getStaticProps
의 return
문안에 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를 재생성하는 것이다.