Next.js & React Study (3)

들어가기 앞서

Next.js에서 제공하는 Rendering의 종류와 사용이유 데이터 패칭 방법에 대해서 정리 하였습니다.

1. 왜 Next.js에서 별도의 Rendering 방법을 제공 할까요?

별도의 Rendering을 제공하는 이유는
앞서 Next가 해결 할 수 있는 문제에 대한 정리에서 React의 CSR(client side rendering) Rendering 방식을 해결하기 위해서 입니다.
이를 사전 렌더링(Pre-Rendering)이라는 개념을 이용해서 문제를 해결을 합니다.

2. 사전 렌더링(Pre-Rendering)

어떤 페이지가 있다고 가정해 봅시다.

이 페이지를 사용자에게 보여주려고 한다면 표준 React라면 사용자가 접근 하는 순간 제일 먼저 HTMML 파일과 Javascript 코드가 표시될 겁니다. 그리고 Javascript 코드가 실행되면서 스크린에 Rendering된 내용을 출력하게 될겁니다. 물론 이 속도는 아주 빨라서 사용자에게는 문제로 보이지 않을 수 있습니다. 하지만 서버로 부터 데이터를 받아서 해당 데이터로 화면을 구성하는 경우는 데이터를 불러올떄 까지 화면을 볼 수 없게 됩니다. (로딩이 필요한 이유)

페이지를 클라이언트로 전송된 뒤에만 데이터를 로딩하는 대신 Next.js는 페이지와 필요할 법한 모든 데이터가 있는 HTML 콘텐츠를 사전에 렌더링 합니다

바로 이것이 표준 React와의 차이점 입니다.
사전에 HTML 페이지를 완성해 놓고 완전히 채워진 HTML 파일을 클라이언트에게 전송하는 겁니다.

’잠깐.. 이게 무슨 소리지? ’ 어렵게 느껴질 수 있습니다.

간단히 말해서 React는 사용자가 접근하면 이제 화면을 그리기 시작합니다 반면 Next.js는 미리 화면을 그려놓고 사용자가 접근하면 전달 합니다.
Next.js는 단순히 사전 렌더링된 페이지를 재전송하는 데 그치지 않고 번들링된 JavaScript 코드를 모두 재전송합니다 이런 재전송을 해당 페이지에 대해 수화(hydrate)한다고 합니다

재전송된 JavaScript 코드는 나중에 사전 렌더링된 페이지를 대체하고 이후 React가 알맞은 작업을 수행합니다.

’방금 사전에 렌더링해서 전달해준다고 했는데..? Javascript 코드를 재전송? 수화(hydrate)?? ’ 위기가 찾아 옵니다.

여기가 마지막! 이해의 마지막 고비 입니다.
사전 렌더링된 페이지는 사용자에게 보여줄 페이지 자체를 HTML 콘텐츠로 변환하여 보여주는 것이라 Javascript 요소들이 하나도 없는 상태입니다. 이는 단순 클릭과 같은 이벤트 리스너들이 각 웹 페이지의 DOM 요소에 하나도 적용되어 있지 않은 상태라는 것 입니다.
이를 해결 하기 위해서 재전송된 Javascript 코드들이 사전 렌더링 된 HTML DOM 위에서 한번 더 렌더링 하면서 이벤트 리스너등 비어있던 부분들을 채워나가게 됩니다.

이 과정을 수화(Hydrate)라고 부릅니다. 용어가 다소 낯설게 느껴집니다. 사전적으로는 수분을 공급한다는 의미입니다. 앞서 정리한 내용처럼 사전 렌더링된 페이지는 이벤트 리스너등이 비어있는 HTML DOM 입니다.
여기에 수분을 공급하는 것 처럼 필요한 부분을 채워 넣는 것이라 이해 하면됩니다.

페이지의 수화(Hydrate), 즉 첫 번째 렌더링이 끝나고 나면 다시 표준 싱글 페이지 애플리케이션으로 돌아갑니다 그때부터는 React가 프론트엔드에서 모든 처리를 수행합니다. 그 후에 페이지가 바뀔 때, 즉 같은 웹사이트의 다른 페이지로 넘어갈 때는 해당 페이지가 사전 렌더링되지 않고 React를 통해 CSR로 동작합니다.

사전 렌더링(Pre-Rendering)이 동작하는건 최초 접속하는 페이지 뿐입니다. 따라서 React가 준비를 마칠 때까지 빈 페이지를 보는 대신에 모든 초기 콘텐츠가 포함된 사전 렌더링된 페이지가 표시되는 겁니다.

참고로 이때 처음에 재전송한 사전 렌더링된 페이지에는 주요 콘텐츠가 모두 포함되어 있는 상태이기 때문에 검색 엔진 크롤러도 전체 페이지에 이미 포함된 모든 콘텐츠를 살펴보게 됩니다. 따라서 SEO의 관점에서 표준 React 방식보다 유리 합니다.

이러한 사전 렌더링을 수행하기 위해서 Next.js에는 두 가지 사전 렌더링 방법 있습니다


2.1 정적 생성(Static Generation)

정적 생성의 개념은 간단합니다 빌드하는 동안 페이지를 사전 생성합니다. 사전 생성이라 함은 콘텐츠를 구성하는 모든 HTML 코드와 모든 데이터를 사전에 준비시켜 놓는다는 뜻입니다 빌드 시간 중 페이지가 사전에 구축되었기 때문에 배포되고 나면 구축된 페이지는 서버나 앱을 실행시키는 CDN을 통해서 캐시로 저장됩니다

Next.js는 기본적으로 동적 데이터가 없는 모든 페이지를 사전 렌더링합니다.

function About() {
  return <div>About</div>
}

export default About

위 같은 페이지가 있다면 외부에서 데이터를 가져올 필요가 없기 때문에 빌드시점에 정적 생성(Static Generation)되고 사전 렌더링 됩니다.

  • getStaticProps

    사전 생성할 페이지에 어떤 데이터가 포함되어야 하는 경우는 어떻게 해야 할까요? 이에 대한 해답은 페이지 컴포넌트에서 가져올 수 있는 특정 함수에서 찾을 수 있습니다 이는 반드시 사용하는 페이지 컴포넌트의 내부에 있어야 하며 다른 React 컴포넌트가 아닌 pages 폴더의 component 파일 내부가 그 위치여야 합니다

    그 안에서 특수한 비동기 함수인 getStaticProps를 가져오는데 이때 그 이름이 정확히 getStaticProps여야 합니다

export default function Blog({ posts }) {
  // Render posts...
}

// 빌드 시점에 호출됩니다.
export async function getStaticProps() {
  // 외부 데이터 가져오기
  const res = await fetch('https://.../posts')
  const posts = await res.json()

  // 빌드시 props로 posts를 전달 받습니다.
  return {
    props: {
      posts,
    },
  }
}

Next.js에서 이 이름 그대로 함수를 찾기 때문입니다. 그리고 이 함수에서 주목할 점은 보통 서버 사이드에서만 실행되는 모든 코드도 실행할 수가 있다는 겁니다. 이 함수에서는 클라이언트 사이드 코드로만 제한되는 게 아니라 특정 클라이언트 사이드 API에 액세스가 없거나 윈도우 객체에 대한 액세스가 없는 일반적으로는 서버 사이드에서만 가능한 모든 코드도 실행할 수 있습니다.

더 나아가서 getStaticProps 내에 작성한 코드는 클라이언트에게 재전송되는 코드로 포함되지 않는다는 겁니다.

즉 해당 함수 내에 포함하는 코드는 클라이언트는 볼 수 없다는 것 입니다. 가령 데이터베이스 크리덴셜(credential)을 포함하고 있는 경우에는 이를 클라이언트 사이드에 노출하고 싶지 않을 테니 getStaticProps 내에 해당 코드를 작성하여 클라이언트 사이드에서 볼 수 없게 할 수 있습니다

이렇게 서버 사이드에서getStaticProps에 외부 데이터를 전달하여 정적 생성을 수행합니다. 하지만 여기서 한가지 생각해봐야 할 점이 있습니다.

자주 변경되는 데이터 형태를 가진 경우 사전에 정적 생성하는게 적당하지 않을 수 있습니다. 이를 해결하기 위해서 Next.js에서는 증분 정적 생성(ISR)이라는 내장 기능이 존재 합니다. 한번 생성해놓고 끝이 아니라 일정시간마다 다시 페이지를 업데이트 해주는 기능입니다. 사용방법은 간단합니다. 아래 예시 처럼 revalidation 옵션을 사용합니다.

export async function getStaticProps() {
  const res = await fetch('https://.../posts')
  const posts = await res.json()

  return {
    props: {
      posts,
    },
    revalidation: 10, // 10초 마다 생성한 페이지를 업데이트 합니다.
  }
}
  • getStaticPaths

    Next.js의 라우팅 시스템에서 /pages/post/[id].js 의 형태로 동적으로 페이지를 생성할 수 있습니다. 하지만 이렇게 생성된 페이지는 Next.js에서 정적 생성을 수행하지 않습니다. 동적 페이지는 사실 여러 페이지가 뭉쳐 있기 떄문입니다. 어떤 페이지가 생성되는지 알려줘야 합니다. Next.js에서는 이를 getStaticPaths 를 사용해서 해결합니다. 아래는 사용 예시입니다.

export async function getStaticPaths() {
  return {
    paths: [{ params: { id: '1' } }, { params: { id: '2' } }], //동적 경로
    fallback: false, // false == paths 전달한 페이지 이외는 생성하지 않음
  }
}

export async function getStaticProps(context) {
  return {
    props: { post: {} },
  }
}

export default function Post({ post }) {
  // Render post...
}

한가지 fallback 옵션에 대해서 추가적으로 알아야 합니다. fallback 옵션이 true라면 요청이 서버에 도달하는 순간 paths에 정의되어 있지 않더라도 사전 생성을 수행합니다. 매우 유용한 옵션이지만 여기서 한가지 주의해야 할 부분이 있습니다. 사용자가 동적 경로에 대해서 url 을 통해서 접근하는 경우 동적 생성이 끝나지 않는 상태로 처리됩니다. 이를 해결하기 위해서는 fallback : 'blocking' 으로 값을 변경 한다면 생성이 끝난 후 정상적으로 페이지 접근 할 수 있습니다.


2.2 서버 사이드 렌더링(Server-Side Rendering)

때때로 매번 요청에 대한 사전 렌더링에 대해서 요청 객체에 대해서 접근이 필요 할 수 있습니다.(Cookie 접근)
하지만 앞서 설명한 두 방법으로는 실제 요청에 대해서 접근 할 수 없습니다. 이에 대한 해결방법은 다음과 같습니다.

  • getServerSideProps

    내용은 간단합니다 페이지 요청이 서버에 도달할 때마다 실행되는 함수입니다 빌드 시간이나 매초마다 사전 생성하지 않고 서버에서만 작동하는 코드로 애플리케이션을 배포한 후 유입되는 모든 요청에 대해서만 재실행됩니다

export default function page(props) {
  return <div>{props.title}</div>
}

//매 요청마다 수행합니다.
export async function getServerSideProps(context) {
  const { params } = context
  return {
    props: {
      title: params.title,
    },
  }
}

Written by@JinhyeongKim
주 1회 작성하는 개발 블로그

GitHub