Next.JS에서의 끊김없는 배포 환경 전환

2021-09-09, Next.JS 사용 환경에서 Static Export와 Server Side Rendering 사이의 차이를 극복하여 개발자 경험과 확장성을 최대화했습니다.

*이 글의 헤더 이미지는 [email protected]에서 제공되었습니다.

Story 목록으로 돌아가기

Next.JS의 아키텍쳐는 기본적으로 SSR에 대한 접근을 목적으로 설계되었습니다. 그렇기 때문에 꼭 SSR이 아니더라도 SSG 등의 방법으로도 활용을 할 수 있습니다. 동시에 빌드된 Next.JS 애플리케이션을 GitHub, Netlify, 그리고 Vercel 등 수많은 플랫폼에 배포할 수 있습니다. 일부 플랫폼은 정적 배포만 지원할 수도 있으며 이는 비용 상 효율적입니다. 혹은 모든 Next.JS의 기능을 활용하기 위해 직접 서버를 운용할 수도 있습니다.

이 때 잠재적인 확장성으로 이어질 수 있는 배포의 다양성을 챙긴다면 더욱 적은 비용으로 애플리케이션을 마이그레이션하고 활용할 수 있습니다. 이는 처음 Typed.sh의 안정적인 컨텐츠 공급을 위해 생각되었으며 저는 이를 통하여 백업의 용이성을 확보하고 배포의 환경 제한을 최소화할 수 있었습니다. 서버를 사용할 수 없을 환경이 되면 즉시 정적 배포 환경으로 대체하여 다운 타임을 최소화하는 것입니다.

Next.JS에서 SSR을 대체할 수 있는 경우

무중단 배포 환경 전환을 구현하기 전에 가장 먼저 고려해야 하는 점은 Next.JS 애플리케이션이 정적 파일로 대체될 수 없는 Serverless 함수인지 아닌지 확인하는 것입니다. Typed.sh와 atwork 프로젝트는 기본적으로 정적 웹 사이트에 블로그 기능을 추가한 것으로 대체가 고려된 기능은 다음과 같습니다. 모두 정적 파일로 제공될 수 있는 경우로 build 전후 스크립트 실행으로 구현에 무리가 없는 경우입니다.

  • 글과 그 목록 조회
  • 사이트맵 생성
  • 이미지 및 파일 제공

*사이트맵의 경우에는 대부분 Atom 및 RSS 피드로도 대체될 수 있습니다.

fs 등 웹 브라우저에서 사용 불가능한 모듈 제외

블로그의 글은 파일의 형태로 저장되는 경우가 대부분이며 이를 정상적으로 NextPage에서 읽어오기 위해서는 Webpack이 웹 브라우저에서 polyfill이 불가능한 모듈은 Webpack 설정에서 제외해야 합니다. 혹은 최종 번들 사이즈를 줄이기 위해서 추가로 설정을 적용할 필요도 있습니다.

Webpack 4

이전 버전 Webpack을 사용하는 경우에는 아래의 방법으로 모듈을 제외할 수 있습니다.

/**
 * @type {import('next').NextConfig}
 */
const nextConfig = {
  webpack: (config, { isServer }) => {
    if (!isServer) {
      config.node = {
        fs: 'empty'
      }
    }

    return config
  }
}

module.exports = nextConfig

Webpack 5

Next.JS에서 새롭게 적용된 Webpack 5에서는 resolve.fallback을 통해 모듈을 제외해야 합니다.

/**
 * @type {import('next').NextConfig}
 */
const nextConfig = {
  webpack: (config, { isServer }) => {
    if (!isServer) {
      config.resolve.fallback = {
        fs: false,
        path: false
      }
    }

    return config
  }
}

module.exports = nextConfig

Sitemap 등 정적 파일 생성

Sitemap 등의 정적 파일은 애플리케이션의 개발 목적에 따라서 2가지 방법을 사용할 수 있습니다. 저는 atwork 프로젝트 등 비교적 업데이트 빈도가 낮고 실시간으로 수정될 필요가 없는 애플리케이션에는 public 폴더에 직접 파일이 생성되도록 하였습니다. 반면에 Typed.sh 등 다수가 사용해 업데이트가 빈도가 높은 애플리케이션의 경우에는 Serverless 함수를 이용하고 캐시를 적용해 그 때 그 때마다 생성되도록 합니다. Serverless 함수는 기본적으로 Static Export를 하면 무시되므로 이를 Next.JS의 Redirect 설정과 함께 이용하면 무중단 배포 환경 전환 조건을 만족할 수 있습니다.

public 폴더에 직접 생성

public 폴더에 직접 생성하는 경우에는 주기적으로 생성하지 않는 한 빌드 타임의 결과물이 곧 최종 결과물이 되어야 합니다. 이 경우에는 자신의 프로젝트 구조에 맞춰서 Sitemap을 생성하는 스크립트를 설정합니다.

Next.JS의 페이지 이름은 상당히 단순하게 맞추어져 있어 큰 어려움이 없습니다.

  • [를 포함하는 param이 있는 페이지 등을 제외
  • _로 시작하는 특수 페이지를 제외

Serverless API로 생성

Serverless API로 생성하는 경우에는 Static Export에서의 일관성을 위해 Next.JS가 빌드 시 무시하는 성질을 사용하여 Redirect를 사용하면 됩니다. 여기에서 주의해야 할 점은 /sitemap.xml의 경우 Content-Type 헤더를 application/xml로 설정하여 올바른 XML 파일임을 알려주어야 한다는 것입니다.

/**
 * @type {import('next').NextConfig}
 */
const nextConfig = {
  rewrites: async () => {
    return [
      {
        source: '/sitemap.xml',
        destination: '/api/sitemap'
      }
    ]
  }
}

module.exports = nextConfig

위와 같이 구성하면 Next.JS 서버를 실행할 때는 Serverless API로 Sitemap이 즉시 생성되고 정적 웹 사이트일 때도 Sitemap을 제공할 수 있습니다. 정적일 때에만 Sitemap을 생성하면 되므로 항상 Sitemap.xml에 표시하는 것보다 오버헤드가 적다고 볼 수 있습니다.

경로 일관성 유지

마지막으로는 Next.JS가 Static Export를 할 때 파일을 생성하는 방법을 알아야 합니다. 그 이유를 예를 들어보겠습니다.

- .
  - pages
    - index.js
    - sub.js

위와 같은 Next.JS 애플리케이션이 있을 때 Next.JS는 Static Export를 수행하며 다음과 같은 결과물을 생성합니다.

- .
  - out
    - index.html
    - sub.html

하지만 대부분의 웹 서버는 index.html만 인덱스 파일로 인정합니다. 그러므로 이를 방지하기 위해 Next.JS에 trailingSlash 값을 붙여 각 서브 디렉터리에 index.html이 생성되도록 해줍니다.

/**
 * @type {import('next').NextConfig}
 */
const nextConfig = {
  trailingSlash: true
}

module.exports = nextConfig

이렇게 하면 이제 내부 링크만 끝에 /이 추가되도록 할 경우에 Static Export와 SSR, SSG 모두에서 같은 결과물을 얻을 수 있게 됩니다. 또한 동시에 같은 디렉터리에 이미지 등의 에셋 또한 배치할 경우 Static Export 후 복사했을 때 별 다른 처리없이 사용가능하다는 이점도 있습니다.

아래에서 asset.pngtrailingSlash 값이 true이므로 별 다른 처리없이 링크를 걸어주면 URL 단에서 모든 것이 해결됩니다.

- .
  - posts
    - [title]
      - index.mdx
      - asset.png
      - ...
  - out
    - posts
      - [title]
        - index.html
        - asset.png
        - ...

프로젝트 스토리를 읽어주셔서 정말 감사합니다.
다른 스토리도 읽어보시겠어요?

프로젝트 스토리는 개발을 진행하면서 새롭게 제시하는 아이디어와 개발하는 중 가치있었던 경험을 적어나가는 곳입니다.

Story 목록으로 돌아가기Typed.sh에서 더 많은 글 보기

번들링없는 프론트엔드로의 전환

2021-09-10, 번들링없는 프론트엔드는 HTTP/2 스펙과 함께 웹 브라우저의 캐싱 전략을 최대한으로 끌어올릴 수 있게 해줍니다.

대규모 채팅 플랫폼 작성

2021-10-07, 대규모 커뮤니케이션 플랫폼에서 사용될 아키텍쳐와 방식들을 직접 구상하고 작성해보았습니다.

홈 서버의 변화들

2021-10-13, Ubuntu부터 가상화까지 제가 홈 서버를 운영하면서 변화한 점을 다루었습니다.