본문 바로가기
프론트엔드

SSR(Server Side Rendering)

by 흥부와놀자 2021. 9. 26.

MPA vs SPA

MPA는 Multiple Page Application의 약자로써 이미 만들어진 정적 페이지를 요청에 따라 반환하는 방식이고, SPA는 Single Page Application의 약자로써 하나의 페이지에서 시작하여 동적으로 페이지를 만드는 방식이다.

 

SSR vs CSR

SSR은 Server Side Rendering의 약자로써 서버에서 코드를 실행해 렌더링 하는 것을 말한다. CSR은 Client Side Rendering의 약자로써 클라이언트단(브라우저)에서 코드를 실행해 렌더링 하는것을 말한다. 보통 MPA에선 SSR을 사용해 서버에서 렌더링 시켜서 브라우저로 페이지들을 넘겨주고, SPA에선 CSR을 사용해 서버에선 첫 빈 페이지와 모든 페이지와 로직이 들어있는 자바스크립트 번들파일만 넘겨준 후 번들파일 로딩이 끝나면 브라우저에서 페이지들을 렌더링 시켜준다.

 

SSR 장점

- 검색 엔진 최적화(seo)

처음 빈 페이지만 넘겨 받는 SPA의 경우 검색엔진이 보기엔 내용이 없는 사이트가 된다. 검색엔진에 높은 점수를 얻기 위해선 곧바로 렌더링된 페이지를 보여줄 수 있는 SSR방식이 필요하다.

   

- 빠른 첫 페이지 렌더링

SPA의 경우 용량이 큰 자바스크립트 번들파일을 받고 첫 페이지를 렌더링 하기 까지 시간이 길어진다. 그렇기에 사용자에게 빠른 렌더링을 보여주기 위해선 SSR방식이 필요하다.

 

 

CSR 장점

- 부드러운 사용자 인터랙션

CSR을 사용하는 SPA의 경우 첫 페이지 로딩을 제외하고 나머지 유저 인터랙션, 페이지의 전환 되는 속도가 브라우저상에서 바로바로 진행되기때문에 SSR에 비해 빠르고 페이지가 전환될때 깜빡임이나 새로고침없이 부드럽게 진행된다. 

- 부분적 렌더링

SSR의 경우 같은 페이지에서 일정부분만 바뀐다 해도 해당 페이지 다시 렌더링해서 서버에서 보내준다. 하지만 CSR을 사용하는 SPA의 경우 해당 페이지에서 바뀔 부분만 부분적으로 렌더링 시킬 수 있다. 이러한 특징은 더 효율적으로 페이지를 렌더링 하는 특징이 된다.  

 

어떤 방식을 선택해야 할까?

만들려는 어플리케이션이 검색엔진에 노출되기 위해 검색엔진 최적화가 필요하다면 CSR보단 SSR을 고려해야 한다. 또한 만약 사용자가 해당 앱을 사용할때 느끼는 인터랙션이 중요하다고 생각되면 CSR을 고려해야 한다. 하지만 첫화면 로딩속도와 검색엔진 최적화는 포기해야 한다. 결국 어떤 방식을 선택하든 단점이 생긴다는 것인데, 더 좋은 방식이 없을까?

 

Next.js

기존의 CSR방식이 가진 단점을 보안하기 위해 SSR의 방식을 부분적으로 도입해주는 react방식의 프레임워크가 바로 Next.js이다. 기본적으로 CSR로 동작하지만 원하는대로 SSR방식을 사용할 수 있다.Next.js를 사용하면 더 쉽고 코드 스플릿, 이미지 최적화 등 다양한 기능을 사용 할 수 있고 Vue에서 지원하는 Nuxt.js 역시 Next.js를 만든 Vercel에서 만들었다.

 

Next 특징

1. 파일구조

기존 리액트는 파일구조가 하나의 페이지를 기반으로 내부에 여러 컴포넌트가 존재했다면, next는 pages폴더 안에 여러 페이지파일들을 가진다.

create-next-app

해당 페이지 파일 이름이 그 페이지의 경로가 되기 때문에 파일 또는 폴더의 이름이 중요해진다.

- index.js : root도메인의 첫 페이지를 나타낸다. 

- _app.js : 맨처음 페이지를 초기화 시켜준다. 페이지 전환시에 레이아웃을 유지시키고 글로벌한 Style지정이 가능하다.

- _document.js : 페이지가 로딩될때의 html을 커스텀할 수 있다. seo에 필요한 메타태그나 타이틀을 넣으면 유용하다.

- public 디렉토리 : 이미지 같은 정적파일이 담긴 디렉토리로써 /파일이름 으로 접근가능하다. seo에 필요한 robot.txt파일을 넣어두면 검색엔진이 도메인네임/robot.txt로 쉽게 참조할 수 있다.

- package.json : 의존성정보, 실행에 필요한 정보들이 담겨 있다.

 

2. 라우팅 방식

<Link href="/about">
    <a>about Page</a>
</Link>

react와 같이 Link로 라우팅할 수 있다. 다만 리액트에선 주소가 매핑될 곳을 Switch와 Route로 따로 지정하는 등 복잡한 방식을 사용하지만 Next에선 a태그 처럼 href를 써서 원하는 페이지파일의 이름을 경로로 쓰면 된다.

그냥 a태그만 사용하면 새로고침하여 새로 전체파일을 다시받아 페이지가 이동하기 때문에 SPA가 가진 이점을 살리지 못한다.  Link는 CSR로 동작하여 부드러운 이동을 가능하게 한다.

 

Link말고도 Router hook을 사용해서도 csr로 라우팅 할 수 있다.

import { useRouter } from 'next/router'
function ActiveLink() {
  const router = useRouter()
  const handleClick = (e) => {
    router.push(href)
  }

  return (
    <button onClick={handleClick}/>
  )
}

이벤트함수로 따로 지정해줘야하지만 path경로나 이동전 후에 호출할 이벤트 함수 등을 지저할 수 있다. 

 

- 동적 라우팅

파일이름을 저렇게 쓰면 해당 경로인 view/???에서 ???안에 있는 부분에 들어가는 파일의 이름을 지정하지 않고 저렇게 변수처럼 받아서 사용할 수 있다. router의 query객체안에 해당 대괄호안의 이름으로 된 프로퍼티가 존재하고 그안에 사용자가 입력한 경로명이 들어간다.

 

3. pre-rendering

next에선 SSR지원을 위해 pre-rendering을 위해 두가지 방식을 지원한다.

- getStaticProps

서버가 빌드되는 순간에 실행되며,  return되는 props가 해당 페이지의 props로 전달된다. 전달되어 완성된 페이지는 서버가 가지고 있다가 브라우저에서 요청할때 전달해 준다.

 

- getServerSideProps

해당 페이지에 요청을 받으면 서버에서 실행되는 함수로써 return되는 props가 해당 페이지의 props로 전달되어 페이지가 완성되고, 그걸 브라우저에 보낸다.

 

4. 빌드 후 전체적인 동작과정(SSR, Data Fetching)

- .Next디렉토리 (빌드폴더)

빌드를 하게되면 위와 같은 폴더가 생기고 getServerSideProps를 구현한 페이지를 제외한 나머지 페이지들에 html파일이 생겼음을 볼 수 있다. 함수를 구현안해도 Next가 자동으로 정적으로 페이지를 생성해준다.

getStaticProps에서 Fetch한 데이터가 이미 들어있는것을 볼수있고, 어떤 함수도 구현안한 index.js에는 아직 Fetch한 데이터가 들어있지 않은 것을 볼 수 있다. 

첫로딩으로 해당 페이지가 요청되면, Next는 이 정적html을 반환하며, getServerSideProps를 구현한 페이지는 요청된 순간 데이터를 Fetch하여 html을 만들어서 반환한다.  

 

react는 처음에 빈페이지와 JS번들파일을 보낸 후 브라우저에서 JS파일을 렌더링 시킨다.

리액트 실행 결과

하지만 Next는 위와 같은 방식으로 완성된 페이지를 반환함으로써 기존 react보다 빠르게 페이지를 확인할 수 있고 검색엔진에 최적화될 수 있다.

 

또한 한번 페이지와 여러 js파일을 받은 이후론 csr과 같이 동작한다. 다만 getStaticProps나 getServerSideProps에서 반환된 데이터들은 해당 브라우저에서 해당 페이지로 넘어갈때 json형식으로 서버에서 받게된다. 

받은 json데이터를 가지고 브라우저는 페이지를 구성하여 깜빡임없이 부드럽게 사용자에게 화면을 보여줄 수 있다.

 

- 그럼 어떤 경우에 getStaticProps를 쓰고, getServerSideProps를 쓰고 해야 할까?

보통 이둘의 차이는 결국 해당 페이지를 그리는데 필요한 데이터를 언제 Fetch할 것인가로 귀결될 수 있다. 

getStaticProps의 경우 서버가 빌드할때 데이터를 가져오므로 만약 빌드 이후의 해당 데이터가 변하면 다시 빌드해야 한다.

그렇기 때문에 보통 정말 바뀔리 없는 정적인 페이지, 예를 들면 about페이지에 사용된다. 

물론 ISR이라고 이러한 경우에 대비한 기능이 있다.  지정된 시간초마다 Fetch한 데이터가 바뀌었는지 확인해준다.

export async function getStaticProps({ params }) {
  return {
    props: {
      product: await getProductFromDatabase(params.id),
    },
    revalidate: 60,
  };
}

위와 같이 기존함수의 리턴객체에 revalidate 프로퍼티만 선언해주고  시간초를 써주며 된다.

서버가 빌드될때 미리 만들어 둠으로써 속도면에선 가장 빠르다는 장점이 있다. 

 

getServerSideProps의 경우 페이지가 요청될때마다 서버에서 데이터를 즉석해서 Fetch해준다. 그렇기 때문에 페이지를 요청한 주체에 따라 페이지가 바뀌거나 그때그떄 최신 데이터를 유지해야 하는 페이지에서 사용된다. 예로 유저정보 페이지나 상품상세페이지, 운영대시보드 등이 있다.

 

그렇다고 모든 페이지에 저 두 함수를 적용한다면 어떻게 될까? 만약 페이지가 많고, 가져올 데이터양이 많을 수록 서버에 부담이 커질것이다. 그렇기 때문에 그때그때 적절히 사용하는게 좋을것이다.

 

5. StaticPropsPath

getStaticProps가 구현된 페이지는 최초 접속 시 빌드타임때 생성된다. StaticPropsPath로 서버에서 동적으로 path정보를 받을 수 있다.

getStaticPaths가 리턴하는 paths객체를 정의해 주고 그안에 params 값으로 해당 페이지의 []안에 들어있는 동적라우팅 아이디를 써준다. 그러면 먼저 저렇게 적어놓은 아이디마다 빌드타임에 페이지가 생성되게 되고 요청시 반환하게 된다. getStaticProps에선 해당 함수의 첫 파라미터의 params프로퍼티로 사용자가 입력한 pathid를 가져올 수 있다. 

fallback은 만약 false면 미리 정해놓은 id가 아닌 다른걸 요청했을때 404를 띄워주고, 만약 true면 다른걸 요청해도 일단 주요데이터 없이 페이지를 그려준 후 해당 pathid를 가져와서 정적으로 문서를 만들어 둔다. 그래서 첫 접속 이후엔 해당 pathid를 정적으로 반환 할 수 있다.