Published on

스테이징 테스트 환경 개선

Authors
  • avatar
    Name
    최영준 (Youngjun Choi)
    Twitter

이 글에서 도메인·서비스명은 모두 service.com처럼 추상화한 예시이며, 실제 사내 구성과는 무관하다.

들어가며

staging은 라이브와 동일한 환경에서 배포 전 검증을 할 수 있게 해주는 테스트 환경이다. 실제와 같은 조건에서 미리 확인할 수 있을수록 팀 전체의 배포 안정성과 생산성이 올라간다. 그런데 staging을 "완전히 분리된 별도 도메인"으로만 운영하면, 앱에서 특정 인코딩된 스킴으로만 진입할 수 있어 테스트가 번거로웠다.

예를 들면 staging 환경에 진입하려면 앱에서 app://webview?url=https%3A%2F%2Fservice-staging.a.com%2F 처럼 staging 주소를 통째로 퍼센트 인코딩한 스킴으로 들어가야 했다. 새로운 서비스를 자주 만들어야 하는 인플로우 팀에서 일하다 보니, 서비스마다 이 인코딩된 주소를 만들어 테스트하는 일이 꽤나 시간이 소비되는 작업이었다.

그래서 신뢰할 수 있는 네트워크(VPN, 사내 WiFi)에서 진입하면 라이브 앱이더라도 자동으로 staging으로 전환되도록 개선했고, 이 글에서는 그 작업과 그 과정에서 마주친 Next.js assetPrefix 문제를 정리한다.

1. Standalone Frontend 인프라

Next 서비스의 경우 output 설정에 따라 다양한 방식으로 서빙이 가능하다. static은 정적 배포나 CSR, standalone은 SSR, SSG, ISR로 프론트 서버를 띄운 후에 요청 시 HTML을 만들거나 미리 만들어진 결과물들을 서빙할 수 있다.

그중 이번에 개선한 서비스는 SSR 서비스라 standalone 옵션을 적용했고, 정적 리소스(_next/static/*)는 CDN을 통해, 나머지 요청은 서버에서 서빙하는 방식으로 되어 있다. 이를 구조도로 나타내면 아래와 같다.

즉 사용자 요청은 CDN을 진입점으로 받고, _next/static/* 같은 정적 리소스는 S3를 origin으로 두어 캐싱·서빙한다. HTML을 비롯한 나머지 요청은 프론트 서버(standalone)로 흘러가 SSR로 처리된다.

여기서 주목할 점은 정적 리소스의 출처(CDN/S3)와 그 외의 출처(프론트 서버)가 분리되어 있다는 것이다. 평소엔 이 둘이 같은 도메인 기준으로 맞물려 문제가 없지만, 라이브 도메인으로 들어와 staging이 응답해야 하는 순간 이 분리가 곧 뒤에서 다룰 assetPrefix 문제의 출발점이 된다.

2. assetPrefix·basePath 문제와 해결

우리 서비스는 기존 코어의 다수 서비스를 서빙하던 인프라를 그대로 이식해오다 보니, 서비스별로 DNS·CDN·S3를 따로 두는 구조와는 맞지 않게 설계돼 있었다. 코어는 여러 도메인을 하나의 DNS·CDN·S3로 묶어 관리하는 편의성을 위해, assetPrefix에 도메인 값을 넣는 방식을 쓰고 있었다.

예를 들어 service.com/a, service.com/b 두 서비스를 하나의 인프라에서 함께 서빙한다고 하자. 각 서비스의 assetPrefix를 자신의 경로(service.com/a, service.com/b)로 설정하고, 공통 DNS(service.com)를 CDN에 연결하면, 정적 리소스가 서비스별로 구분되어 같은 CDN·S3 안에서도 충돌 없이 매핑된다.

  • service.com/a/_next/*
  • service.com/b/_next/*

한편 보안·devops팀에서는 신뢰 네트워크(VPN, 사내 WiFi)로 진입한 요청을 감지해 staging으로 보내도록 구성해주셨다. 다만 라이브 도메인에서 staging CDN을 경유하는 경로는 AWS 정책상 불가능했기 때문에, CDN을 거치지 않고 곧장 staging 프론트 서버로 진입하도록 설정해주셨다.

여기서 문제가 드러난다. 위 방식은 CDN에 _next/* behavior를 두고 원본 S3에 빌드 결과물을 올려두면 잘 서빙되지만, CDN을 건너뛰고 프론트 서버로 바로 진입하면 그 경로에 맞는 결과물이 없어 정적 리소스 서빙이 실패한다.

이를 해결하기 위해 코어 인프라 의존성을 제거하고 사내 환경에 맞는 CD 인프라를 다시 구성했다. 핵심은 basePathassetPrefix의 역할을 나눈 것이다.

  • basePath: '/a' — 페이지 라우팅을 /a 아래로 옮긴다. 서비스가 /a, /b로 격리된다.
  • assetPrefix: 'https://service.com' — 정적 리소스(/_next/*)의 출처를 cdn의 도메인으로 고정한다.
// @ts-check
import { PHASE_DEVELOPMENT_SERVER } from 'next/constants'

export default (phase) => {
  const isDev = phase === PHASE_DEVELOPMENT_SERVER
  /** @type {import('next').NextConfig} */
  const nextConfig = {
    basePath: '/a',
    assetPrefix: isDev ? undefined : 'https://service.com',
  }
  return nextConfig
}

이렇게 하면 페이지는 basePathservice.com/a 하위에 라우팅되고, 정적 리소스는 assetPrefixservice.com/a/_next/*로 정리된다. 여기에 프론트 서버도 같은 정적 파일을 갖고 있으면, CDN을 경유하든 프론트 서버로 바로 진입하든 무관하게 서빙할 수 있게 된다.

standalone 빌드는 원래 public·static 폴더를 포함하지 않기 때문에, 프론트 서버가 직접 서빙하려면 CD 과정에 아래 복사 단계가 필요했다. 그만큼 Docker 이미지가 커지는 건 트레이드오프였다.

cp -r public .next/standalone/ && cp -r .next/static .next/standalone/.next/

3. publicRuntimeConfig — 클라이언트에서 런타임 환경 판별하기

한편 클라이언트는 host를 기준으로 환경을 판단하고 있었는데, host가 service.com/a이면 항상 live로 판단했다. 이번 요구사항은 도메인이 service.com/a라도 staging 결과물이 나와야 하는 것이라, 이 판단 로직을 고쳐야 했다.

여기서 한 가지 제약이 있다. NEXT_PUBLIC_* 환경변수는 빌드 시점에 값이 번들에 인라인되기 때문에, 하나의 빌드 결과물로 live와 staging을 동시에 서빙하려는 이번 구조와 맞지 않는다. 같은 번들이 도메인·환경에 따라 다르게 동작해야 하므로 런타임에 읽히는 값이 필요했다.

Next 15(Pages Router)에서는 publicRuntimeConfig가 이 역할을 한다. 서버가 요청을 처리하는 런타임에 값을 주입하므로, 같은 빌드라도 실행 환경에 따라 다른 값을 내려줄 수 있다.

// next.config.js
module.exports = {
  publicRuntimeConfig: {
    // 서버·클라이언트 양쪽에서 접근 가능, 런타임에 평가된다
    serviceEnv: process.env.SERVICE_ENV, // 'live' | 'staging'
  },
}
import getConfig from 'next/config'

const { publicRuntimeConfig } = getConfig()
const env = publicRuntimeConfig.serviceEnv // 런타임에 결정되는 환경 값

다만 publicRuntimeConfig는 Pages Router 전용이라 App Router에서는 동작하지 않고, Next 16에서는 사용할 수 없다. 그래서 이후에는 다른 방식으로 대체했다.

정리

이번 작업으로 라이브 도메인으로 진입해도 staging을 검증할 수 있게 되면서, 팀이 배포 전 실제와 동일한 조건에서 편리하게 확인할 수 있게 되었다. 사무실에서는 WiFi만 연결하면 되고, 재택이라도 VPN으로 라이브 앱에 접속하면 되어 한결 편하게 테스트할 수 있게 되었다.

추가로 나는 드디어 모호했던 사내 프론트 인프라 구성을 제대로 이해할 수 있게 되었고, 보안팀과 devops팀원 분들께 많이 배우면서 인프라 개발의 즐거움을 더 느낄 수 있게 되었다.

이 과정을 거치며, 점점 늘어나는 서비스를 어떻게 잘 관리할지, 그리고 늘어나는 서비스 코드 속에서 속도와 안정성을 어떻게 지켜낼지에 대한 고민이 시작되었다. 그래서 새 서비스를 만들 때 필요한 서비스·인프라 스펙을 구조화하고 템플릿화하는 작업을 진행하고 있고, 다음 글로 남겨보려 한다.