22sook00 logo
SookDev

Next.js 에서 SSR 로 데이터 가져오는 방식들

tag
next.js
productivity
date
Jul 2, 2022

Data Fetching

💡
Before we get to know the way of fetching data 👐🏻

CSR , SSR , SSG 차이

CSR ( Client Side Rendering )

특징

→ 클라이언트 사이드 렌더링으로, HTML / CSS / JS 파일들을 모두 한꺼번에 받아오는 특징을 가진다.

장점

→ 처음 모든 필요한 파일들을 받아오기 때문에 파일간 이동 시 필요한 부분만 바꿔주어 불필요한 렌더링 시간이 줄어든다.

단점

→ 초기 자바스크립트의 모든 파일들을 불러온 후 뷰를 구성하기 때문에 맨 처음 로딩속도가 길어진다.
이는 초기 사이트 진입 시 화면이 보이지 않는 시간에 따라 이탈률이 비례하기 때문에 꽤나 치명적일 수 있다.
→ 초기 페이지 정보가 받아오는 시간이 늦어지면 검색엔진 크롤러는 해당페이지를 처음 방문했을때 빈 페이지로 보여지게 되므로 읽어들일 수 없다. 그러므로 SEO(Search-Engine-Optimization) 검색엔진에 취약하다.
자바스크립트를 실행시킬 수 있는 구글 크롤러와 같은 크롤러가 등장하고 있지만 아직은 많은 검색엔진 크롤러가 지원되지 않는다.
 

SSR ( Server Side Rendering )

특징

→ HTML 뷰 구성을 위한 파일을 서버에서 생성하고 클라이언트 브라우저에서는 응답을 받아 완성된 뷰를 그대로 보여주는 특징을 가진다.

장점

→ 서버에서 모든 파일을 다운로드 하여, 이미 다 만들어진 페이지를 검색엔진 크롤러가 요청에 대한 응답으로 받기 때문에 SEO 검색엔진 기능 최적화가 되어있다.

단점

→ 새로고침 또는 새로운 페이지로 이동 시 새로 모든것을 렌더링하여 보여주기 때문에 페이지 전환 간 깜빡임 현상이 일어날 수 있으며 변경사항이 없는 부분도 불필요하게 리렌더링 될 수 있다.
→ 사용자가 늘어날수록 서버측의 부담이 커지게 된다.
 

SSG ( Static Site Generation )

특징

→ SSR 과 같이 서버로부터 모든 데이터를 받아오지만 HTML 파일의 생성 시점이 요청시간이 아닌, 빌드 시간에 있다는 차이점이 있다.
즉, 클라이언트에서 필요한 페이지들을 사전에 미리 준비해 둔 후 요청을 받으면 이미 완성된 파일을 반환하여
브라우저에서 뷰를 보여주는 특징을 가진다. Next.js 에서 권장하는 방식이기도 하다.

장점

→ 서버에서 이미 렌더링 된 데이터를 클라이언트 측으로 보내므로 사용자는 즉시 화면을 볼 수 있다.
검색엔진에 최적화 되어있으며 정적인 콘텐츠는 매우 빠르다.
서버를 실행시킬 필요가 없어

단점

→ 사이트가 커지고 라우트 경로가 많아질수록 느려질 수 있으며 간혹가다 적용되지 않는 UI 라이브러리가 있을 수 있다.
모든 URL에 대해 개별 HTML 파일을 생성해야 한다.
따라서 URL을 미리 예측할 수 없어 적용이 어렵다.
 

결론

무조건 이 세가지 중 하나만 선택해서 사용하는 것이 아니라 적절히 섞어 사용하는것이 좋다.
즉, 사이트 첫 방문 시에는 로드 속도가 빠르고 SEO 까지 가져갈 수 있는 SSR 또는 SSG 방식,
페이지가 한 번 로드된 이 후에는 사용자경험을 가져갈 수 있는 CSR 방식을 선택하는 것이 좋다.
 
 
기존 CSR 방식으로 렌더링 되던 React에서는 useEffect 를 통해 비동기 처리 및 Data fetch 했다.
Next.js 에서는 정적 데이터인지, 페이지 요청마다 렌더링 되는 데이터인지에 따라 총 세가지 방식으로 함수를 분리.

🥰 요약

  • getStaticProps (SSG) → 빌드 타임에 따라 데이터 가져오기
  • getStaticPaths (SSG) → 데이터를 기반으로 페이지를 미리 렌더링 하는 동적 경로지정
  • getServerSideProps (SSR) → 각 요청에 따른 데이터 가져오기

 

getServerSideProps

특징
  • 서버에서 바로 데이터를 주고받는 방식
  • SEO 유리
  • 컴포넌트가 아닌, 페이지 단에서만 사용 가능
 
언제 적절할까?
getServerSideProps요청 시 데이터를 가져와야 하는 페이지를 렌더링해야 하는 경우에만 사용해야 한다. 
페이지가 요청받을때마다 호출되어 pre-rendering 한다. 동적 데이터가 필요한 페이지에서 사용이 가능하고 내용이 언제든 동적으로 수정가능.
이것은 자주 변경되는 데이터를 가져오고 가장 최신 데이터를 표시하도록 페이지를 업데이트하려는 경우에 유용하다.
 
파라미터
  • params: 다이나믹 라우트를 사용할 경우, params 는 라우트의 파라미터를 포함하게 된다. 이때 페이지명은 [id].js 이런식으로 대괄호 안에 넣어주고 대괄호 안의 값은 파라미터의 키가 된다.
export async function getServerSideProps(ctx: { params: { id: string }; }): Promise<GetServerSidePropsResult<PageProps>> { try { const houseData = await getHouseDetail(ctx.params.id as string); return { props: { data: houseData, uuid: ctx.params.id, }, }; } catch (err) { if (err.response.status === 404) { return { redirect: { permanent: false, destination: "/404", }, }; } } }
  • req: The HTTP IncomingMessage object, with an additional cookies prop, which is an object with string keys mapping to string values of cookies.
  • res: The HTTP 응답 객체
  • query: An object representing the query string, including dynamic route parameters.
export const getServerSideProps = withAuthServerSideProps(async (ctx) => { const { id, numOfGuest, startDate, endDate } = ctx.query; if (!(id && numOfGuest && startDate && endDate)) { return { redirect: { permanent: false, destination: "/404", }, }; } const roomID = id as string; const [houseInfo, profile] = await Promise.all([ getHouseDetail(roomID), // getRoomCoupon(roomID), getProfile(), ]); return { props: { profile, houseInfo, numOfGuest, startDate, endDate, id }, }; });
  • preview: true / false 값에 따라 프리뷰 모드를 지원한다.
  • previewData: setPreviewData 에서 설정한 미리보기 데이터.
  • resolvedUrl: URL제거하고 원래 쿼리 값을 포함 하는 요청의 정규화된 버전
  • locale: 활성 로케일을 포함.
  • locales 지원되는 모든 로케일을 포함
  • defaultLocale: 구성된 기본 로케일을 포함
클라이언트 측에서 가져오는게 유리한 경우
데이터가 자주 업데이트 된다면 요청 시 데이터를 가져오는 클라이언트 사이드 렌더링 혹은 getStaticProps 방식을 사용해야 한다. 여기서 대시보드 와 같이 SEO 와 관련이 없는 페이지일 경우 페이지를 미리 렌더링 할 필요가 없기 때문에 클라이언트 사이드 렌더링 방식을 이용해야 한다.
페이지에 자주 업데이트되는 데이터가 포함되어 있고 데이터를 미리 렌더링할 필요가 없는 경우 클라이언트 측 에서 데이터를 가져올 수 있습니다 . 이에 대한 예는 사용자별 데이터입니다.
  • 먼저 데이터가 없는 페이지를 즉시 표시합니다. 페이지의 일부는 정적 생성을 사용하여 미리 렌더링할 수 있습니다. 누락된 데이터에 대한 로드 상태를 표시할 수 있습니다.
  • 그런 다음 클라이언트 측에서 데이터를 가져와 준비가 되면 표시합니다.

getStaticProps & getStaticPaths

빌드시 딱 한번만 호출되어 그 이후 수정이 불가하다. 호출 시마다 매번 데이터페칭을 하지 않아 성능면에서 좋다. 그렇기 때문에 앱 빌드 후 웬만하면 바뀌지 않는 내용이 있는 정적페이지에 사용하는것이 좋다. 동적 라우팅을 할 땐 getStaticPaths 와 같이 사용한다
export const getStaticPaths: GetStaticPaths = async () => { const paths = []; try { const allPosts = await getAllPost({}); for (let i = 0; i < allPosts.data.length; i++) { paths.push({ params: { postTitle: allPosts.data[i]?.attributes.url } }); } return { paths, fallback: true, }; } catch (error) { return { paths, fallback: false, }; } }; export const getStaticProps: GetStaticProps = async (context) => { const { params } = context; try { const data = await getPost(params.postTitle as string); return { props: { data, }, revalidate: 1, }; } catch (error: any) { return { props: { error: error, }, redirect: { destination: "/blog", }, }; } };
export const getStaticPaths: GetStaticPaths = async () => { const paths = []; try { const allActivities = await getActivities(""); const mockDatasIncluded = [...allActivities, ...activityData]; for (let i = 0; i < mockDatasIncluded?.length; i++) { paths.push({ params: { activityContent: mockDatasIncluded[i]?.id + "", }, }); } return { paths, fallback: true, }; } catch (error) { // console.log(error); return { paths, fallback: true, }; } }; export const getStaticProps: GetStaticProps = async ({ params }) => { const id = String(params.activityContent); if (+id >= 10001 && +id <= 10005) { const activityDetailData = activityData?.find( (activity) => activity?.id === +id, ); return { props: { activityDetailData: { ...activityDetailData, activityImages: activityDetailData?.activityImages?.map((image) => ({ location: `${process.env.NEXT_PUBLIC_ACTIVITY_IMAGE_URL + image}`, })), content: activityDetailData?.desc.reduce((acc, sentence) => { const toTagString = `<p>${sentence}</p>`; return acc + toTagString; }, ""), }, }, }; } try { const activityDetailData = await getActivityDetail(id); return { props: { activityDetailData, }, revalidate: 1, }; } catch (error: any) { return { redirect: { destination: "/activity", permanent: false, }, props: { error: error, }, }; } };
 

getStaticProps

🌟  only 페이지에서만 export 가능하며, 페이지 컨텐츠가 외부데이터에 연동된다.
async 로 함수를 export 할 경우, getStaticProps 함수 내 props로 내려주어 페이지를 빌드 시점에 렌더링 하게 된다.
// javascript + next.js export async function getStaticProps(context) { return { props: {}, } }
// typescript + next.js import { GetStaticProps } from 'next' export const getStaticProps: GetStaticProps = async (context) => { // ... }
  • params : 다이나믹 라우트 페이지라면, params를 라우트 파라미터 정보를 가지고 있으며 getStaticPaths 함수와 같이사용한다. → page 명을 [id].js로 설정 후 params : {id : ...}
  • preview: preview 모드 여부 >공식문서
  • previewData: setPreviewData로 설정된 데이터
getStaticProps가 리턴할 수 있는 값 (optional)
  • props : 해당 컴포넌트로 리턴할 값
  • revalidate : 페이지 재생성이 발생할 수 있는 시간(초). 기본값은 false이며, 이게 거짓이면 다음 빌드때까지 페이지가 빌드된 상태로 캐시됨.
  • notFound : Boolean값, 404status를 보내는 것을 허용한다.
// example // javascript + next.js function Blog({ posts }) { return ( <ul> {posts.map((post) => ( <li>{post.title}</li> ))} </ul> ) } export async function getStaticProps() { const res = await fetch('https://.../posts') const posts = await res.json() return { props: { posts, }, } } export default Blog
// typescript + next.js import { InferGetStaticPropsType } from 'next' type Post = { author: string content: string } export const getStaticProps = async () => { const res = await fetch('https://.../posts') const posts: Post[] = await res.json() return { props: { posts, }, } } function Blog({ posts }: InferGetStaticPropsType<typeof getStaticProps>) { // will resolve posts to type Post[] } export default Blog

getStaticProps 를 사용하는 시점

  • 페이지가 검색엔진을 위해 빠른 렌더링이 필요하거나 사용자 요청보다 먼저 페이지 렌더링이 요구될때
  • 매번 유저가 요청하는 fetch 에 구애받지 않고 퍼블릭하게 캐시할 수 있는 데이터를 가진 페이지에 유리.
  • Headless CMS 로부터 데이터가 올때
→ Headless CMS : 백엔드 부분만 있고 프론트엔드 부분은 없는 콘텐츠 관리 시스템(CMS) 를 뜻함
 

getStaticPaths

🌟  페이지에 동적 경로( 문서 )가 있고 getStaticProps 로 사용하는 경우에는 getStaticPaths을 통해 빌드 타임에 렌더 된 경로가 정의되어야 한다.
정의하지 않은 하위 경로는 접근해도 화면이 뜨지 않는다.
동적 경로를 사용하는 페이지에서 async 로 호출 된 함수 를 내보내는 경우 getStaticPaths 에 지정된 모든 경로를 정적으로 미리 렌더링 
 
export async function getStaticPaths() { return { paths: [ { params: { ... } } // See the "paths" section below ], fallback: true, false, or 'blocking' // See the "fallback" section below }; }
 

paths [필수]

pages/posts/[id].js 라는 동적라우트 페이지가 있을때,
getStaticPaths 함수를 export 해주고 그 안에 다음과 같이 paths를 설정해준다.
return { paths: [ { params: { id: '1' } }, { params: { id: '2' } } ], fallback: ... }

fallback [필수]

getStaticPaths 는 반드시 불리언형태의 fallback 키를 포함해야 한다.
만약 fallback 이 false 로 되어있다면 404 에러를 나타나게 되고 true 일 경우 getStaticProps 의 내용을 변경하게 된다.
// typescript + next.js import { GetStaticProps } from 'next' interface PostInterface { userId: number id: number title: string body: string } // 이 페이지에서 렌더될 컴포넌트 function Post({ posts } : { posts:PostInterface }) { return ( <ul> {posts.map((post) => ( <li>{post.title}</li> ))} </ul> ) } // 빌드될 때 실행 export const getStaticPaths = async () => { // posts를 받기 위해 fetch const res = await fetch('https://.../posts') const posts = await res.json() // pre-render할 Path를 얻음 (posts를 통해서) const paths = posts.map((post) => ({ params: { id: post.id }, })) // 우리는 오로지 이 path들만 빌드타임에 프리렌더 함 // { fallback: false } 는 다른 routes들은 404임을 의미 // true이면 만들어지지 않은 것도 추후 요청이 들어오면 만들어 줄 거라는 뜻 return { paths, fallback: false } } // 빌드될 때 실행 export const getStaticProps = async ({ params }) => { // params는 post `id`를 포함하고 있다 const res = await fetch(`https://.../posts/${params.id}`) const post = await res.json() // 해당 페이지에 props로 보냄 return { props: { post } } } export default Post

getStaticPaths 를 사용하는 시점

→ 동적 경로를 사용하는 페이지를 렌더링할 때

getServerSideProps (server-side-rendering)

🌟  빌드와 상관없이, 매 페이지 요청마다 데이터를 서버로부터 가져온다.
  • pages/Blog.tsx
// typescript + next.js import { GetServerSideProps } from 'next' function Page({ data }) { console.log(this.props.data) //res.json() 이 찍힌다. } export const getServerSideProps: GetServerSideProps = async (context) => { const res = await fetch(`https://.../data`) const data = await res.json() // data 없을 땐 리턴값을 달리함 if (!data) { return { redirect: { destination: '/', permanent: false, }, } } //pageProps로 넘길 데이터 return { props: { data: data } } } export default Page

getServerSideProps가 props로 받는 context

  • params: 다이나믹 라우트 페이지라면, params를 라우트 파라미터 정보를 가지고 있다.
  • req: HTTP request object
  • res: HTTP response object
  • query: 쿼리스트링
  • preview / previewData / setPreviewData로 설정된 데이터

getServerSideProps 리턴 (Optional)

props : 해당 컴포넌트로 리턴할 값
redirect : 값 내부와 외부 리소스 리디렉션 허용한다
→ 무조건 { destination: string, permanent: boolean } 의 형태.
getServerSideProps 는 페이지를 렌더링하기전에 반드시 fetch 해야 할 데이터가 있을 때 사용
매 페이지 요청시마다 호출 되므로 getStaticProps보다 느리지만, 빌드 이후에도 페이지 요청 마다 실행.
  • getServerSideProps는 서버사이드에서만 실행되고, 절대로 브라우저에서 실행되지 않는다.
  • getServerSideProps는 매 요청시 마다 실행되고, 그 결과에 따른 값을 props로 넘겨준 뒤 렌더링을 한다.

getServerSideProps / pre-rendering 를 사용하는 시점

→ 빈번하게 데이터가 update 될 때
→ SEO가 굳이 필요하지 않은 데이터(user-specific data 등..) 는 일부 정적 pre-render를 자료 없이 보여주고 로딩 상태 보여주며, 클라이언트 측에서 데이터가 패치되었을 때 좋다.