Nextjs13 - Static Export

dante Yoon·2023년 4월 13일
10

NextJS

목록 보기
7/8
post-thumbnail

안녕하세요 단테입니다.
오늘은 nextjs 13.3에서 추가된 static export의 사용방법과 사용 예시에 대해 알아보고 직접 nextjs13 버전을 github-pages에 배포하는 실습을 진행하겠습니다.

예제 코드는
https://github.com/dante01yoon/nextjs13.3 에서 확인하실 수 있습니다.

영상으로 보기

https://www.youtube.com/watch?v=-1lXeSZYRS4

SPA 배포 과정

서버에서 실행되는 코드가 하나도 없이 Create-React-App과 같이 js bundle 파일들과 정적 파일들을 생성해 아래와 같이 간단한 구조로 서빙하려고 합니다.

가장 많이 사용되는 Nginx를 예시로 들었지만 정적 파일을 서빙하는 곳은 AWS S3와 같은 CDN 서버여도 무방합니다.

중요한 것은 위 파일들이 danteVelog.com 을 통해 유저로 전달되는 것입니다.

이 경우 danteVelog.com은 -> index.html을, index.html 내부에서는 아래와 같이 번들된 js 파일과 css 파일을 다운로드하는 코드가 선언되어야 합니다.

<!DOCTYPE html>
<html>
  <head>
    <link href="/cd5678.css" rel="stylesheet" />
  </head>
  ...
  <body>
    <script src="/ab1234.chunk.js"/>
  </body>
</html>

index.html에서 로딩되는 ab1234.chunk.js 덕분에 리엑트가 Client Side Rendering을 할 수 있게 됩니다.

지금 위 구조는 결국 앞서 작성한 html 파일 하나만 사용해 모든 주소의 페이지들을 그려주는 것입니다.

  • danteVelog.com/
  • danteVelog.com/about
  • danteVelog.com/commerce
    그 어떤 경로로 들어가도 항상 동일한 index.html 파일이 엔트리 포인트가 됩니다.
    유저에게 처음 보여지는 화면은 index.html에 선언된 코드들로 동일하다는 것이지요.

한 단계 더 나아가서

만약 우리가 danteVelog.com 도메인을 통해 제공되는 경로가 아래 세 가지가 전부라는 사실을 알고있다면
index.html, about.html, commerce.html을 만들어 사용자 접속 url에 따라 각기 다른 html 파일을 서빙해줄 수 있게 됩니다. 당연히 처음 보여지는 화면도 다르게 구성할 수 있겠지요.

유저가 진입하는 html 파일이 다름에 따라 한번에 다운받아야 하는 번들 사이즈도 줄어듭니다.

AS IS

TO BE

Next.js13.3 버전에서 나온 static export는 위와 같이 SPA를 배포할 수 있게 도움을 주는 기능입니다.

static export가 제공되지 않았을 때의 문제점

그러면 왜 굳이 static export를 써서 SPA를 만들어야 할까요
static export를 쓰기 전에는 어땠을까요?

next build

next.js를 이용하여 작성된 앱은 배포시 다음과 같은 명령어를 통해 빌드합니다.

$ next build

nextjs 프로젝트 초기 세팅 후 별도 설정 없이 빌드하면 다음 처럼 .next 디렉토리 하위에 빌드된 파일들이 생성됩니다.

빌드 설정은 next.config.js를 통해 할 수 있는데 이 때 output 옵션으로 빌드파일의 형상을 정할 수 있습니다.

이때 output 옵션을 사용해 배포파일의 형상을 정할 수 있는데요,
아무것도 설정하지 않았을 시 SSR과 CSR을 모두 지원하는 hybrid 앱이 빌드됩니다. 이렇게 빌드된 앱은 next start를 통해 실행시킬 수 있습니다.

output으로 standalone 설정시 배포에 필요한 최소한의 파일만 복사해내는데요
이 때 경량된 노드 서버가 빌드 산출물에 만들어지며 이를 실행시킴으로 앱을 실행시킬 수 있습니다.

standalone 모드라면 .dist/standalone/server.js를 통해 최소한의 서버 코드로 앱을 실행시킬 수 있으며 코드는 대략 아래와 같이 생겼는데요, 내용을 읽어보면 평범한 노드 서버를 하나 3000 포트로 띄우는 것을 알 수 있습니다.

const NextServer = require('next/dist/server/next-server').default
const http = require('http')
const path = require('path')
process.env.NODE_ENV = 'production'
process.chdir(__dirname)

// Make sure commands gracefully respect termination signals (e.g. from Docker)
// Allow the graceful termination to be manually configurable
if (!process.env.NEXT_MANUAL_SIG_HANDLE) {
  process.on('SIGTERM', () => process.exit(0))
  process.on('SIGINT', () => process.exit(0))
}

let handler

const server = http.createServer(async (req, res) => {
  try {
    await handler(req, res)
  } catch (err) {
    console.error(err);
    res.statusCode = 500
    res.end('internal server error')
  }
})
const currentPort = parseInt(process.env.PORT, 10) || 3000
const hostname = process.env.HOSTNAME || 'localhost'
const keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10);

if (
  !Number.isNaN(keepAliveTimeout) &&
    Number.isFinite(keepAliveTimeout) &&
    keepAliveTimeout >= 0
) {
  server.keepAliveTimeout = keepAliveTimeout
}
server.listen(currentPort, (err) => {
  if (err) {
    console.error("Failed to start server", err)
    process.exit(1)
  }
  const nextServer = new NextServer({
    hostname,
    port: currentPort,
    dir: path.join(__dirname),
    dev: false,
    customServer: false,
    ... 중략 ... 
    
  handler = nextServer.getRequestHandler()

  console.log(
    'Listening on port',
    currentPort,
    'url: http://' + hostname + ':' + currentPort
  )
})

보시면 코드 내부에 3000 포트로 서버를 띄우는 작업이 있음을 알 수 있습니다.

이렇게 노드 서버를 통해 서버사이드 렌더링과 클라이언트사이드 렌더링 모두를 지원하는 하이브리드 앱을 만드는 용도로 Next.js가 많이 활용됩니다.

하지만 클라이언트 사이드 렌더링만 필요하며 cdn이나 nginx와 같은 간단한 웹서버를 이용해 웹앱을 배포하고 싶은 경우도 있겠지요.

이 때는 위와 같은 빌드 산출물을 이용하기 어렵습니다. 매우 혼잡하게 빌드 결과물이 섞여있기 때문입니다.

한가지 다행인 점은 각 페이지별 엔트리 포인트별로 나뉘어 html 파일이 생성된다는 점입니다.

Nextjs에서 사용할 수 있는 빌드 기능을 그대로 사용하면서 앞서 봤던 각 url별로 경량화된 번들파일을 제공하고 싶습니다. 어떻게 할 수 있을까요?

개발자들의 문제제기와 토론

페이스북의 리엑트팀 Dan abramov는 Create React App이나 Vite와 같은 도구를 통해 만들어진 정적 산출물과 같이 간단한 형태로 용이하게 Next.js의 빌드 산출물을 만들어낼 수 있는 방법이 필요하다고 이야기했습니다.

그와 함께 토론에 참여한 여러 개발자중 vercel소속 엔지니어인 Lee Robinson은
이번 13.3 버전에서 해당 기능을 제공한다는 소식을 전합니다.

static export 사용법

이제 static export를 사용해 Next.js를 이용해 CSR 앱 산출물을 만들고 이 정적파일을 github-pages환경에 배포해보는 방법을 알아보겠습니다.

next.config.js

먼저 다음과 같이output: export 옵션을 사용해 앱을 빌드합니다.

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    appDir: true,
  },
  output: "export",
}

module.exports = nextConfig

이렇게 하면 빌드된 정적 파일들이 out 폴더 내부에 생성되는 것을 볼 수 있습니다.
이때 CSR 환경에서 필요한 css, js 번들 파일들은 out/_next 디렉토리
내부에 있는데요

html 파일 내부를 보면 minify되어 잘 알아보기 어렵지만 실제로 생성된 정적 파일들을 올바른 경로로 참조하고 있음을 알 수 있습니다.

gh-pages를 이용해 배포해보자.

s3나 여타 다른 cdn 서버를 사용하시는 분들은 out 폴더 내부에 있는 정적파일들을 그대로 서버 내부에 옮겨두시면 사용이 가능합니다.

실습의 용이함을 위해 aws 계정을 만들지 않아도 되는 gh-pages를 이용해 실제 어떤 원리로 CSR이 서빙되는지 확인해보겠습니다.

저는 https://github.com/dante01yoon/nextjs13.3 라는 저장소를 만들었습니다. next.js13.3 버전 프로젝트를 생성하시고 깃허브 원격 저장소에 푸쉬하면 모든 준비가 마쳐집니다.

앱 구성요소

아래처럼 두 페이지로 나뉘어져 있습니다. 그리고 각 index.html, about.html을 생성할 page.tsx들만 app 디렉토리 내부에 만들어주면 됩니다.

별다른 작업은 할 필요 없고 about 페이지와 내부에 있는 page.tsx만 app 디렉토리 바로 아래 있는 page.tsx를 그대로 복사해서 넣어주었습니다.

배포를 위한 action.yml 작성

배포를 위한 브랜치에 코드가 푸쉬되었을 때 자동으로 gh-pages로 배포해주는 workflow를 작성하겠습니다. 간단한데요, 저는 feat/static이라는 이름의 브랜치에 push를 했을때 배포가 되게 만들었습니다. 이때 github_token: ${{ screts.GITHUB_TOKEN}}은 따로 설정해줄 필요가 없습니다. 그냥 써두면 알아서 잘돌아갑니다. 이 토큰은 세팅 창에서 따로 발급받아야 하는 access token이 아닙니다.

name: Deploy to GitHub Pages

on:
  push:
    branches:
      - feat/static

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v2
      - name: Install dependencies
        run: yarn install # or yarn install
      - name: Build production-ready SPA
        run: yarn build # or yarn build
      - name: Deploy to GitHub Pages
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./out # or the folder with your static assets

publish_dir을 통해 빌드 산출물들이 ./out 내부에 있음을 알려줍니다. 이 폴더 내부에 있는 내용물들이 gh-pages 브랜치에 생성됩니다.

코드를 푸쉬하면 action이 잘 동작하고 저장소 주소를 기반으로 웹이 발행됨을 확인할 수 있습니다.


배포가 잘 되었을까요?

action이 잘 실행되었다면 gh-pages라는 브랜치가 생성되고 내부에 생성된 정적파일들이 있는 것을 확인할 수 있습니다.

배포 url

배포 저장소가 dante01yoon/nextjs13.3이기 때문에 https://dante01yoon.github.io/nextjs13.3 링크로 배포가 됩니다.

그런데 문제가 하나 있습니다.

저희가 앱을 제공하는 주소가 https://dante01yoon.github.io로 도메인 레벨에서 끝나는게 아니라 뒤에 path가 같이 붙어버려 index.html 내부에서 참조하고 있는 정적 파일들이 올바른 위치를 가르키지 못합니다.

앞서봤던 html 내부에서 참조하고 있는 정적 파일의 위치가 https://dante01yoon.github.io/nextjs13.3 하위 path가 아닌
https://dante01yoon.github.io 하위 path를 가르키며 올바르지 않은 장소에서 파일을 불러오려고 하는 것입니다.

이는 Next.js에서 제공하는 assetPrefix 옵션을 통해 빌드된 파일 위치 참조에 prefix를 붙임으로 해결할 수 있습니다.

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    appDir: true,
  },
  output: "export",
  assetPrefix: "/nextjs13.3",
}

module.exports = nextConfig

이후 다시 빌드된 후 생성되는 html파일 내부를 보면 올바른 경로를 참조하고 있음을 알 수 있습니다.

사실 gh-pages는 별로 좋은 서빙위치는 아닌 것 같습니다.

사실 위와 같이 빌드시 특정 파일의 참조위치를 변경해준다든가 하는 일은 확실히 조금 번거롭습니다.
cdn 서버나 별도 도커 인스턴스에 nginx 웹서버를 띄운 후 해당 웹서버에서 정적파일을 서빙하는게 제일 간편한 것 같습니다.

profile
성장을 향한 작은 몸부림의 흔적들

4개의 댓글

comment-user-thumbnail
2023년 4월 14일

안녕하세요 단테님

https://nextjs.org/docs/advanced-features/static-html-export

설명해주신 기능이 상기링크에 있는 Static HTML Export와 다른 기능인가요?
Next 12 에서 정적웹호스팅이 가능했던걸로 알고있어서 질문드립니다!

2개의 답글
comment-user-thumbnail
2024년 11월 20일

좋은글 감사합니다!

답글 달기