next.js

next.js ?

1) 특징

next.js 는 서버 사이드 렌더링, 동적 라우팅 등을 아주 손쉽게 사용할 수 있도록 해주는 강력한 리액트 프레임워크이다.

아래와 같은 특징을 가지고 있다

  • 서버 사이드 렌더링 지원 (서버에서 렌더링을 해서 넘겨줌)
  • 기본적으로 사전 렌더링 지원 (빌드할 때 미리 HTML 을 만들어놓음)
  • 서버 사이드 렌더링으로 인한 검색 엔진 최적화 (이미 HTML 이 채워져 있으므로, 검색 엔진이 우리 웹 페이지 정보를 원활하게 수집할 수 있음)
  • 동적 라우팅 지원 (react-router-dom 필요없이 그냥 파일을 만드는 것만으로 자동으로 route 가 구성됨)
  • 간단한 API 서버 구축 지원
  • 이미지, 폰트 최적화 지원
  • 기본적으로 타입스크립트 지원
  • 코드 분할 지원 (따로 설정하지 않아도, 알아서 번들을 조각내어서 필요한 부분만 전송함)
  • 서버 사이드 렌더링 + 사전 렌더링 + 코드 분할로 인한 빠른 로딩

2) 렌더링 방식

  1. 클라이언트 사이드 렌더링(CSR)

기본적으로 SPA는 클라이언트 사이드 렌더링 방식으로 구현되고 있다.

리액트의 index.html은 아래와 같이 구성되었었다.

리액트는 우선 index.html을 던지고, 그 다음에 필요한 자바스크립트를 불러오면서(해당 스크립트가
실행되면서) 우리가 작성한 컴포넌트가 표시되는 방시긍로 작동한다.

자바스크립트를 실행하는 주체는 바로 사이트에 접속한 이용자이다.

CSR로 구현된 페이지는 html만 보았을 때는 요소들이 비워져 있다. 그렇지만 사이트에 사용자가 접속해서, 자바스크립트가 실행되는 순간 요소들이 채워지기 시작하는 것이다.
따라서 요소들을 그리는 주체가 클라이언트라는 점에서 클라이언트 사이드 렌더링이라고 말하는 것이다.

그럼 이것만 써도 상관없나요? 😊
아뇨 그랬으면 이런거 안배우죠. 마냥 좋은것만은 아니다.

  • 사용자가 첫 화면을 보기까지의 시간이 오래걸린다.(화면을 그리는 자바스크립트가 실행되기 전까지 사용자는 빈 화면을 쳐다봐야한다.)
  • html이 비어있기 때문에, 검색 엔진이 우리 페이지의 정보를 원활하게 수집할수가 없다.

이런 단점들을 보안하기 위해서

  1. 서버 사이드 렌더링 (SSR)
    얘가 나온 것이다.

말 그대로 요소들을 그리는 주체가 서버 가 되는 것이다.
사용자가 사이트에 접속하는 순간, 서버는 html에다가 요소들을 그리고, 요소들이 그려진 html 파일과 함계 필요한 자바스크립트 코드를 사용자에게 보낸다.
그렇게 되면 사용자가 받는 파일은 빈 파일이 아니라, 요소들이 채워져있는 html 파일이 된다.

요소들이 그려져있다고 해서 실제로 사용자와의 인터렉션(클릭) 이 가능한 것은 아니다

SSR의 특징은

  • 페이지 로딩이 빨라짐
  • 검색 엔진 최적화(SEO)가 가능하다!

오! 그럼 이것만 쓰면 되겠네! SEO도 가능하면 좋은거잖아

꼭 그렇진 않다

단점

  • 서버가 요소를 그려야 하기 때문에, 서버에 부하가 걸린다
  • 사용자가 사이트를 본 순간 (Time To View) 과 인터렉션이 가능한 순간 (Time To Interact)의 차이가 발생함 (요소가 그려지기만 하고, 아직 자바 스크립트 코드가 실행되지는 않았기 때문)
  • 새로고침시 페이지를 다시 서버에서 받아와야하기 때문에, 새로고침 시 화면이 비었다가 채워지는 Blinking Issue 가 발생

마냥 좋은 것도 아니면 이걸 또 왜배움? 걍 react 쓰지

Next.js 의 핵심적인 기능때문이며, 이것은 바로

3) 정적 생성 (Static Stie Generation, SSG)

Static Generation 이란 특정 페이지의 HTML을 빌드할 때 미리 그려놓는 것을 말한다. (react로 만든 앱은 배포시에 항상 빌드하는 과정이 필요하다 -> yarn build)

사용자가 사이트에 접속할 때마다 서버가 직접 HTML을 그리는 것이 아니라, 미리 그려놓은 채로 해당 HRML을 저장해놓고, 사용자가 사이트에 접속하면 저장해놓은 HTML을 공급하는 것이다.

오! 그러면 SSR의 장점은 그대로 유지하면서, 서버에 대한 부하를 줄이면서, 로딩시간은 훨씬 더 빨라지겠구나!

바로 그렇다.
Next.js 는 컴포넌트가 따로 외부에서 데이터를 가져오지 않는 한, 항상, static generation 방식으로 사전 렌저링을 진행한다. (따로 설정을 해주면, 외부에서 데이터를 가져오더라도 static generation방식으로 렌더링이 가능하다)

물론 이럼에도 단점은 있다.

  • 빌드 시간이 오래 걸릴수 있음 (빌드하면서 HTML을 그려놓기 때문에 당연히, 평소보다 오래 걸린다.)
  • 오래된 데이터를 미리 그려놓는 경우, 유저가 옛날 데이터를 보는 상황이 발생할 수 있다.(항상 업데이트되는 데이터라면, SSB 보다는 SSR을 쓴느것이 좋다)

next.js 앱을 yarn dev 로 실행한 경우에는 SSG 로 구현해놓은 코드라도 SSR 과 동일한 방식으로 작동한다. (아직 미리 빌드하지 않았기 때문)

3) 시작하기

한번 해보자

yarn create next-app (앱 이름) --ts

명령어를 입력하고 나면 pages 폴더가 생성되어있는데, 여기에 폴더를 만들거나, 파일 이름을 적으면 해당 이름대로 자동으로 route가 설정이 된다.

yarn dev를 통해서 서버를 실행했을때, 가장 처음 보에는 페이지 컴포넌트는 index.js이다.

그런데 사실 _app.js_document.js를 거쳐서 우리 웹사이트에 표시가 되는 것이 index.js인데, _app.js는 서버 자체에 접근하면 제일 먼저 실행되는 컴포넌트이다.

yarn dev 명령어로 실행해보면 데모 페이지가 나타난다.

next.js 기본기능

1) pages 폴더를 통한 라우팅

next.js는 pages 폴더 안에 파일 혹은 폴더를 만든느 것만으로 해당 파일 이름으로 route 가 구성된다.

뿐만 아니라 id번호 등에 맞추어 페이지를 보여줘야하는 경우의 동적 라우팅도 지원한다.

  • pages/index.jsx 라고 하면 해당 파일이 ‘/’ 주소에 연결
  • pages/post.jsx 라고 하면 해당 파일이 ‘/post’ 주소에 연결
  • pages/post/index.jsx 라고 하면 해당 파일이 ‘/post’ 주소에 연결
  • pages/post/first.jsx 라고 하면 해당 파일이 ‘/post/first’ 주소에 연결
  • pages/post/[id].jsx 라고 하면 해당 파일이 ‘/post/1’, ‘/post/2’ 등의 동적 주소에 연결 (이렇게 하면 해당 파일에서 useRouter 를 사용해서 해당 id 를 가져올 수 있게 된다)
  • pages/post/[…params].jsx 라고 하면 해당 파일이 ‘/post///*’ 등의 동적 주소에 연결 (route 의 깊이를 무한하게 지정할 수 있습니다.) (단, ‘/post/1’ 로 접근할 경우 위 [id].jsx 가 우선하게 된다.)

한번 만들어보자.

리액트 작업할 때 하던 대로 컴포넌트를 만들듯이 채워주면 된다. [id] 컴포넌트의 경우, 함수 이름을 그대로 사용하는 것이 아니라, PostDetail등으로 바꿔줘야 한다.

post[id].jsx

import { useRouter } from 'next/router'
import React from 'react'

function PostDetail() {
    const router = useRouter()
    const { id } = router.query

    return <div>PostDetail {id}</div>
}

export default PostDetail

이렇게 작성해놓고 post/1 로 이동해보면

react-router-dom 설치 안했는데도 이렇게 라우팅이 가능하다!

페이지간 이동 시에는 Link태그를 활용한다.

index.jsmain태그 부분을 비우고, 아래처럼 Link 태그를 추가한다.

 <Link href='/post'>post로 가고싶어?</Link>

이렇게 해서 해당 문구를 클릭하면 post 페이지로 이동한다!

여기서 post로 이동하면 post/index.tsx를 우선시한다.

'import React from 'react'

function index() {
  return (
    <div>
      하이! post 페이지!
    </div>
  )
}

export default index

Link 태그는 Link 태그가 사용자에게 보이면 (viewport 안에 들어오면) 자동으로 해당 페이지를 미리 로드 (prefetch) 한다. (정적 생성 방식으로 이용하는 페이지만 미리 로드하고, 서버 사이드 렌더링을 이용하는 페이지의 경우 Link 태그를 눌렀을 때만 로드한다.)

확인해보고 싶으면, yarn build 후에 yarn start를 해서 프로덕션 모드에서 확인할수 있다. (yarn dev를 통해 실행한 경우에는 미리 로드하지않음)

만약에 쿼리 파라미터를 함께 전달하고 싶다면?

<Link href='/post?name=hello'>
    name과 함께 post로 가기
</Link>

이렇게 해주면 post/index.jsx 를 router.query를 사용해서 쿼리를 가져올수 있다.

import { useRouter } from 'next/router'
import React from 'react'

function Post() {
    const router = useRouter()
    const { name } = router.query

    return <div>Post {name}</div>
}

export default Post

만약 동적 라우팅이라면?

<Link href='/post/1?name=hello'>
    name과 함께 1번 post로 가기
</Link>

이렇게 한다면 PostDetail에는

import { useRouter } from 'next/router'
import React from 'react'

function PostDetail() {
    const router = useRouter()
    const { id, name } = router.query

    return (
        <div>
            PostDetail {id} {name}
        </div>
    )
}

export default PostDetail

요렇게 작성할수 있을 것이다.

… 을 활용해서 만든 깊이가 있는 route 에 접근한다면?

<Link href='/post/1/2/3'>
    깊이있는 post로 가기
</Link>

[...params].jsx

import { useRouter } from 'next/router'
import React from 'react'

function PostParams() {
    const router = useRouter()
    console.log(router.query)

    return <div>PostParams</div>
}

export default PostParams

파일명에 적었던 이름으로, 배열 형태의 값이 들어가는 것을 볼수 있다.

만약 Link태그가 아니라 버튼으로 이동하고 싶으면 router.push()를 사용한다.

router.push(url)
import { useRouter } from 'next/router'
import React from 'react'

function Post() {
    const router = useRouter()
    const { name } = router.query

    return  <div>
            <button onClick={() => router.push('/post/1')}>1번 글로가기</button>
        </div>
}

export default Post

  • 분명히 주소에 쿼리 파라미터를 명시했는데 컴포넌트에서 query에 접근이 안된다면?
    정적 최적화가 이루어진 페이지에 접근하는 경우, query가 빈 개체로 전달된다.

그런 경우에는 useEffet와 함께 router.isReady를 사용하면 된다. router.isReady가 true가 된다면, query부분이 채워졌으므로 이제 query에 접근해도 된다.

useEffect(() => {
    if (router.isReady) console.log(router.query)
}, [router.isReady])

3) 스타일 적용하기

next.js 는 기본적으로 css, css modules, 인라인 css (css-in-js), styled-jsx 를 모두 지원한다.

만약 sass 를 적용하고 싶다면, 그때는 sass 모듈을 (개발용 디펜던시로) 설치해야 한다.

yarn add -D sass

이전에 사용했던 styled-components 도 사용가능하다. 사용하는 방법은 기존과 다르지 않다.

yarn add styled-components

post컴포넌트에 한번 적용해보자!

import { useRouter } from 'next/router'
import React, { useEffect } from 'react'
import styled from 'styled-components'

export const Container = styled.div`
    display: flex;
    flex-direction: column;
`

function Post() {
    const router = useRouter()

    return (
        <Container>
            <button onClick={() => router.push({ pathname: '/post/[id]', query: { id: 1 } })}>1번 글로가기</button>
            <button onClick={() => router.push({ pathname: '/post/[id]', query: { id: 2 } })}>2번 글로가기</button>
        </Container>
    )
}

export default Post

이렇게 해놓고 실행하면?

???

이런 오류가 생기는 이유는 바로 최초에 우리한테 보여지는 페이지(서버가 공급한 페이지/ 정적 페이지) 와 실제로 자바스크립트 코드가 실행되면서 수정되는 페이지 간에 className이 일치하지 않기 때문이다

따라서 이걸 해결하려면 next.config.js를 아래처럼 수정해줘야한다.

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  compiler: {
    styledComponents: true
  }
}

module.exports = nextConfig

이렇게 하면, 최초 빌드할 때와 서버/클라이언트에서 렌더링할때의 className을 일치시켜주게 된다!

styled-components 관련 코드를 분리하고 싶다면, styled폴더에 js파일 형태로 분리해놓고 코드를 작성하면된다.

styles/post.js

import styled from "styled-components";

export const Container = styled.div`
    display: flex;
    flex-direction: column
`

pages/post/index.js

import { useRouter } from 'next/router'
import React, { useEffect } from 'react'
import * as S from '@/styles/post'

function Post() {
    const router = useRouter()

    return (
        <S.Container>
            <button onClick={() => router.push({ pathname: '/post/[id]', query: { id: 1 } })}>1번 글로가기</button>
            <button onClick={() => router.push({ pathname: '/post/[id]', query: { id: 2 } })}>2번 글로가기</button>
        </S.Container>
    )
}

export default Post

잘 적용되었다.

4) 이미지 태그

기본 img 태그가 아니라, Image 태그가 별도로 존재하는 이유는 바로 성능 최적화 때문이다.
next.js 의 image태그는 아래와 같은 특징이 있다

  • 직접 리사이징 해줄 필요없이 자동으로 뷰포트에 맞춰줌
  • 해당 뷰포트에 진입할 때만 이미지를 로딩
  • 레이아웃 이동을 최소화(Cumulative Layout Shift 방지)
  • 디바이스 종류에 따라 이미지 파일 크기를 조절해서 공급

기본적으로는 아래와 같이 쓴다.

import Image from 'next/image';

<Image
  src="/images/profile.jpg" // 이미지 주소, 경로
  height={144} // CLS 방지를 위해 사용됨
  width={144} // CLS 방지를 위해 사용됨
  alt="Your Name"
/>

아래의 이미지를 사용해서 태그를 만들어보자 이미지를 public 폴더에 넣어준다

next는 자동으로 public 폴더 내부에 있는 파일들을 도메인/파일명 을 통해서 접근할수 있도록 하고 있다. (Static File Serving)
그래서 src에는 rmsid /파일명으로만 작성해주면 된다!

import { useRouter } from 'next/router'
import React, { useEffect } from 'react'
import * as S from '@/styles/post'
import Image from 'next/image'

function Post() {
    const router = useRouter()

    return (
        <S.Container>
            <Image src="/doggy.jpeg" alt="doggy" height={500} width={500} />
            <button onClick={() => router.push({ pathname: '/post/[id]', query: { id: 1 } })}>1번 글로가기</button>
            <button onClick={() => router.push({ pathname: '/post/[id]', query: { id: 2 } })}>2번 글로가기</button>
        </S.Container>
    )
}

export default Post

이미지를가져올 경우 height 와 width를 직접 명시해줘야 한다.

만약에 아래와 같이 import 를 통해서 가져올 경우, height와 width 는 해당 파일의 크기대로 맞춰진다.

import { useRouter } from 'next/router'
import React, { useEffect } from 'react'
import * as S from '@/styles/post'
import Image from 'next/image'
import dogImage from 'public/dog.jpeg'

function Post() {
    const router = useRouter()

    return (
        <S.Container>
            <Image src={dogImage} alt="dog" />
            <button onClick={() => router.push({ pathname: '/post/[id]', query: { id: 1 } })}>1번 글로가기</button>
            <button onClick={() => router.push({ pathname: '/post/[id]', query: { id: 2 } })}>2번 글로가기</button>
        </S.Container>
    )
}

export default Post
  • priority (이 이미지를 제일 먼저 불러!)
    해당 페이지에서 핵심적인 요소라면, priority 옵션을 주어야한다. 그러면 해당 이미지를 다른 요소들에 비해 먼저 로딩한다.
<Image src={dogImage} alt="dog" priority />
  • fill (그냥 부모 태그에 맞춰서 이미지 채워줘)
    이걸 쓰려면 부모 요소가 poistion:relativedisplay:block 속성을 가지고 있어야한다.
<Image src={dogImage} alt="dog" fill />
  • sizes(크기 별 이미지 제한)
    기본적으로 Image 태그를 사용하면 srcSet 이라는 속성으로 디바이스 크기별 이미지를 지정해준다! (디바이스 크기가 작으면 작은 이미지를 쓰고, 크면 큰거 쓰게 지정해줌)
    만약 디바이스 크기 별 이미지를 지정하고 싶으면 sizes 속성을 통해서 명시해주면 된다.
<Image src={dogImage} alt="dog" fill sizes="(max-width: 768px) 33vw, (max-width: 1200px) 50vw, 100vw"/>
  • 이미지를 외부에서 가져오는 경우 (next.config.js)
    만약에 이미지를 링크 형태로 명시하는 거라면, 아래와 같이 next.config.js에 허용할 도메인을 추가해줘야한다!
images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'via.placeholder.com', // 전부 허용하고자 할 경우에는 **
        port: '',
        pathname: '**', // 전부 허용하고자 할 경우에는 **
      },
    ],
  },

5) 폰트 설정하기 (next/font)

next 는 웹푼토 또한 빌드 시에 미리 받아놓을수 있따.
https://fonts.google.com/ 에 존재하는 폰트는 모두 사용할 수 있다.

앱 전체에 Black Han Sans를 적용해보자

import type { AppProps } from 'next/app'
import { Black_Han_Sans } from 'next/font/google'

const blackHanSans = Black_Han_Sans({
    weight: '400',
    preload: false, // 한글 폰트라면 필요하다(정확히는 한글 subset을 지원하지 않아서 필요)
})
export default function App({ Component, pageProps }: AppProps) {
    return (
        <main className={blackHanSans.className}>
            <Component {...pageProps} />
        </main>
    )
}

이렇게 해주면 컴포넌트 전체의 폰트가 Black Han Sans로 적용된다.
페이지 컴포넌트에도 동일하게 사용할수 있기 때문에, 특정한 부분에만 폰트를 적용하고 싶다면 위와 같이 작성하고 jsx요소에다가 className을 할당해주면 된다.

그럼 만약 styled-component와 함께 사용하고 싶으면..?

6) font 설정하기 (styled-component와 함께 사용하기)

https://hangeul.naver.com/font

여기로 들어가서 나눔 글꼴을 다운로드 받고 그중에서 나눔 바른펜 폰트를 사용하려고 한다.

styled component에서 font를 사용할때는 global style을 활용했었다. 따라서 globalstyle을 위한 파일을 만든다.

styles/globalStyle.js

import { createGlobalStyle } from 'styled-components'

export const GlobalStyle = createGlobalStyle`
    @font-face {
        font-family: Nanum Barun Pen;
        src: url('/fonts/NanumBarunpenR.ttf');
    }

    body {
        font-family: Nanum Barun Pen;
    }
`

pages/_app.js

import { GlobalStyle } from "@/styles/globalStyle";

export default function App({ Component, pageProps }) {
  return <><GlobalStyle/><Component {...pageProps} /></>
}

글꼴이 적용된 모습이다.

  • FOUT 해결하기

새로고침을 할때마다 글자가 깜빡거리는 현상이 있다. 이를 FOUT 현상이라고 하는데 (Flash of Unstyled Text) styled component가 새로고침할때마다 폰트를 불러와서 생기는 현상이다. 이를 해결하기 위해서는 해당 폰트를 불러오는 부분을 따로 css형태로 분리해야한다.

다시 말해서 폰트를 불러오는 부분은 styled component를 사용하지 말라는 뜻

이렇게 global.css 에 폰트 관련 코드를 추가하고
global.css

@font-face {
  font-family: Nanum Barun Pen;
  src: url('/fonts/NanumBarunpenR.ttf');
}

body {
  font-family: Nanum Barun Pen;
}

app.js 에 작성했던 GlobalStyle 내용을 삭제하고 아래와 같이 작성한다.
_app.js

import '@/styles/globals.css'
import Head from 'next/head'

export default function App({ Component, pageProps }) {
    return (
        <>
            <Head>
                <link rel="preload" href="/fonts/NanumBarunpenR.ttf" as="font" crossOrigin="anonymous" type="font/ttf" />
            </Head>
            <Component {...pageProps} />
        </>
    )
}

이제 다시 보니까 이전에 적용한 폰트 화면은 제대로 적용된 것이 아니었던 것 같다..

폰트를 여러번 가져오는 일을 없애기 위해서 next.config.js에 아래 header부분을 추가한다.

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  compiler: {
    styledComponents: true
  },
  headers: () => {
		return [
			{
				source:  '/fonts/:font*',
				headers: [
					{
						key:  'Cache-Control',
						value:  'public, max-age=31536000, immutable',
					},
				],
			},
		]
	},
}

module.exports = nextConfig

이렇게 하면 Link 태그로 페이지를 이동하더라도, font를 다시 가져오지 않는다.

7) Head 태그 , Script 태그

next.js 에서는 Head태그를 통해서 각 페이지별 Head 태그를 설정해줄수 있다. 아래처럼 페이지 컴포넌트에다가 Head 태그를 작성하면, 브라우저에 글 있는곳 이라는 타이틀이 표시된다.

import { useRouter } from 'next/router'
import React, { useEffect } from 'react'
import * as S from '@/styles/post'
import Image from 'next/image'
import Head from 'next/head'

function Post() {
    const router = useRouter()

    return (
        <>
            <Head>
                <title>글 있는 곳?!</title>
            </Head>
            <S.Container>
                <Image src="/doggy.jpeg" alt="doggy" height={500} width={500} />
                <button onClick={() => router.push({ pathname: '/post/[id]', query: { id: 1 } })}>1번 글로가기</button>
                <button onClick={() => router.push({ pathname: '/post/[id]', query: { id: 2 } })}>2번 글로가기</button>
            </S.Container>
        </>
    )
}

export default Post

Script태그를 사용하면, 외부 스크립트를 효율적으로 가져올수 있다.
아래처럼 작성하면 js를 불러오는 과정에도 next.js 만의 최적화 기법이 적용된다.

pages/post/index.tsx

import React from 'react'
import * as S from '@/styles/post'
import Script from 'next/script'

function Post() {
    return (
        <>
            <Script src="https://example.com/script.js" />
            <S.Container>
                <p>폰트가 적용됐나요?</p>
            </S.Container>
        </>
    )
}

export default Post

8) Layout Component 활용(Nav,Footer)

nav, footer 등 페이지 별 공통 요소를 작성하기 위한 Layout 컴포넌트를 하나 작성해보자
(컴포넌트에 불과하기 때문에, 이 컴포넌트 단독으로 정적 생성이 된다거나 서버 사이드렌더링이 되지는 않는다.)

components/Layout.jsx

import React from 'react'

function Layout({ children }) {
    return (
        <>
            <nav>내브바</nav>
            <main>{children}</main>
            <footer>푸터</footer>
        </>
    )
}

export default Layout

_app.js에 아래처럼 작성하면 된다.

import type { AppProps } from 'next/app'
import '@/styles/globals.css'
import Head from 'next/head'
import Layout from '@/components/Layout'

export default function App({ Component, pageProps }: AppProps) {
    return (
        <>
            <Layout>
                <Head>
                    <link rel="preload" href="/fonts/NanumBarunpenR.ttf" as="font" crossOrigin="anonymous" type="font/ttf" />
                </Head>

                <Component {...pageProps} />
            </Layout>
        </>
    )
}

이제 어느 페이지로 가던 layout이 보이게 된다.

next.js 심화 기능

1) 정적 사이트 생성(SSG)

외부에서 데이터를 가져오는 사이트를 SSG 방식으로 구현해보자.
외부에서 데이터를 가져오지 않는 경우에는 next.js가 기본적으로 SSG 방식으로 HTML을 그려놓는다.
하지만 외부에서 데이터를 가져오는 경우, 따로 설정을 해줘야 빌드할때 해당 데이터를 가져와서 저장해 놓고 HTML을 그려두게 된다.

  • getStaticProps 사용하기
    설정하는 방법은, 페이지 컴포넌트에 아래와 같은 함수를 추가하고
export async function getStaticProps() {
    요청을 보내고 데이터 받아오기
    return { props: { 데이터 } }
}

페이지 컴포넌트에서 해당 데이터를 props 로 받아오듯이 가져오면 된다.

function 페이지컴포넌트({ 데이터 }) {
    return (
        <div>
        </div>
    )
}

한번 해보자 우선 axios를 설치한다.

Post 컴포넌트내부에 api 호출을 통해 데이터를 받는 함수를 작성한다.

export async function getStaticProps() {
    const response = await axios.get('https://jsonplaceholder.typicode.com/posts')
    const posts = response.data
    return { props: { posts } }
}

그러면 Post 컴포넌트에서 {posts} 와 같은 형태로 해당 데이터를 가져와서 사용하면 된다.

import { useRouter } from 'next/router'
import React, { useEffect } from 'react'
import * as S from '@/styles/post'
import Image from 'next/image'
import Head from 'next/head'
import Script from 'next/script'
import Link from 'next/link'

export async function getStaticProps() {
    const response = await axios.get('https://jsonplaceholder.typicode.com/posts')
    const posts = response.data
    return { props: { posts } }
}

function Post({ posts }) {
    const router = useRouter()

    return (
        <>
            <Head>
                <title>글 있는 곳?!</title>
            </Head>
            <Link href="/">홈으로 가기</Link>
            {posts.map((post) => (
                <div key={post.id}>
                    <h1>{post.title}</h1>
                    <p>{post.body}</p>
                </div>
            ))}
        </>
    )
}

export default Post

  • getStaticPaths 사용하기

여기서 만약에 예를 들어서, post/[id].jsx를 위한 정적 사이트를 생성한다고 생각해보자.
만약 1번 게시글을 미리 받아와서 정적 사이트를 생성한다면?

export async function getStaticProps() {
    const response = await axios.get(`https://jsonplaceholder.typicode.com/posts/1`)
    const post = response.data
    return { props: { post } }
}

그런데 사용자는 post/1로도 접근할수 있고, post/2로 접근할 수도 있다.
그렇다면 특정 게시글을 위한 정적 사이트 생성을 위해서는,

  • 유저가 접근할 수 있는 모든 id번호가 무엇무엇이 있는지 찾고 (getStaticPaths)
  • 해당 번호마다 요청을 보내서 게시글을 저장해놓아야한다. (getStaticProps)

즉, getStaticPaths는 유저가 접근 가능한 동적 라우트 주소가 무엇이 있는지를 지정해주는 함수인 것이다.

만약에 post를 가져온다면, 아래처럼 전체 리스트를 가져오도록 요청보내고, id 부분만 추출해서 하나의 배열에다가 저장하면된다. 현재 posts는 100번 까지 있으므로 ,[{ params: { id: 1 } }, { params: { id: 2 } }, …. , { params: { id: 100 } }] 형태의 배열이 생성된다. id값은 문자열로 작성해줘야함

export async function getStaticPaths() {
    const response = await axios.get(`https://jsonplaceholder.typicode.com/posts`)
    const posts = response.data
    const paths = posts.map((post) => ({ params: { id: `${post.id}` } }))
    return { paths, fallback: false }
}

fallback 같은 경우는 3가지 옵션이 있는데,

  • false: 미리 데이터를 받아놓은 path가 아니라면 404 페이지로 이동
    -> 데이터가 한정된 경우 이 방식을 사용
  • true: 미리 데이터를 받아놓은 path가 아니라면 요청시 해당 path에 대한 HTML과 JSON데이터를 정적으로 생성하면서, 로딩을 보여줌(하넙ㄴ 생성하면 캐싱됨)
    -> 데이터가 너무 많은 경우에 이방식을 사용
  • blocking: 미리 데이터를 받아놓은 path가 아니라면 요청시 해당 path에 대한 HTML과 JSON 데이터를 정적으로 생성하면서, 로딩을 보여주지 않음(로딩이 완료되어야 페이지를 보여줌, 한번 생성하면 캐싱됨) 로딩시 보여줄 페이지는, 페이지 컴포넌트에 아래와 같은 코드를 추가해서 작성해줄수 있다.
    const router = useRouter()

if (router.isFallback) {
return

로딩 중..

}


만약 getStaticPaths를 작성했다면, 이제 아래와 같이 getStaticProps를 작성하고 페이지 컴포넌트에서 데이터를 사용해주면 된다.

```jsx
// getStaticPaths 에서 리턴한 paths 내부 값을 인자 형태로 받아올 수 있음 
export async function getStaticProps({ params }) {
   const response = await axios.get(`https://jsonplaceholder.typicode.com/posts/${params.id}`)
   const post = response.data
   return { props: { post } }
}

이제 빌드할때 1번 게시글부터 100번 게시글 모두에 요청을 보내고 데이터를 저장해두게 된다.

정적 사이트 생성 방식은 데이터를 미리 받아서 저장해놓기 때문에, 예전 데이터를 보여줄 가능성이 있다. 만약 지속적으로 데이터가 업데이트 되는 경우라면, 서버사이드 렌더링을 사용하되, 그 외의 경우는 SSG를 사용하는 것이 좋다. 왜냐고? SSG가 성능이 더 좋으니까

yarn dev 로 실행한 경우에는 SSG로 구현해놓은 코드라도 SSR과 동일한 방식으로 작동한다.(아직 빌드하지 않았으므로)

2) SSG 의 단점 해결하기 (ISR)

ISR(Incremental Static regeneration)란 해당 주기마다 다시 데이터를 가져와서 정적 사이트에 그려놓는 방식을 말한다.
사용하기 위해서는 getStaticProps에 revalidate를 명시해주면 된다.

export async function getStaticProps() {
   const response = await axios.get('https://jsonplaceholder.typicode.com/posts')
   const posts = response.data
   return { props: { posts }, revalidate: 10 }
}

만약 위처름 revalidate 값을 10으로 해놓으면, 정적 사이트가 생성되고 10초동안 저장해놓은 데이터를 사용하게 된다. (정적 사이트가 생성되고 10초동안은 사이트에 접속한 누구든지 동일한 데이터를 보게된다.)

10초가 지난 뒤에 누군가가 다시 해당 사이트에 접근한다면, Next.js는 그제서야 해당 요청을 다시 보내고 업데이트된 데이터를 또 10초동안 저장해놓고 사용자들에게 보여주게 된다.

3) 서버 사이드 렌더링 구현(SSR)

서버 사이드 렌더링은 getServerSideProps함수를 사용하면 된다. 사용방식은 SSG와 동일함

함수 이름만 변경해주는 느낌으로 작성하면 된다!

import axios from 'axios'
import Link from 'next/link'
import React from 'react'

export async function getServerSideProps() {
    const response = await axios.get('https://jsonplaceholder.typicode.com/posts')
    const posts = response.data
    return { props: { posts } }
}

function Post({ posts }) {
    return (
        <div>
            <Link href="/">홈으로 가기</Link>
            {posts.map((post) => (
                <div key={post.id}>
                    <Link href={`/post/${post.id}`}>
                        <h1>{post.title}</h1>
                    </Link>
                </div>
            ))}
        </div>
    )
}

export default Post

홈으로 가기 버튼을 눌렀다가 다시 post 로 오면, 위와 같이 저장해놓은 데이터를 사용하는 것을 알수 있다. (서버사이드 렌더링/ 정적 사이트 생성은 html과 json을 활용해서 미리 데이터를 넣어놓는다!)

이제 사용자가 해당 페이지로 접근할 때마다, Next.js는 데이터를 가져오는 요청을 다시 보내고 해당 데이터를 저장한다면

getServerSidePaths도 있을까?

  • 없다. 빌드할때 미리 각각의 게시글을 가져오는게 아니라, 유저가 특정한 게시글을 정해서 요청을 보내면 그제서야 데이터를 받아와서 그리는 것이므로, 당연히 getServerSidePaths는 없다.

정말 업데이트가 잦은 경우가 아니라면, SSR 방식보다는 SSG방식에 revalidate 옵션을 명시하면서 사용하는 것을 추천한다.

getInitialProps라는 기능도 있지만, next가 업데이트 되면서 getServerSideProps가 사실상 해당 기능을 대체하게 되었다.

profile
개발자 꿈나무

0개의 댓글