당근마켓에 웹 프로젝트 배포하기 #2 — 웹 서버로 돌아가기

지난 글에서, 로컬 파일 기반 웹뷰를 만든 과정과 이 방식이 지속하기 어려운 이유를 소개했어요.

이후 당근마켓 프론트엔드 챕터에서 어떤 방식으로 웹 프로젝트를 배포하고 있고, 어떤 고민을 이어가고 있는지 설명해볼게요.

익숙함을 벗어나기

우리는 로컬 파일 기반 웹뷰 방식의 제약사항을 탈피하고 다음으로 나아가기 위해 기존에 익숙하던 방식을 과감하게 버리고 다시 웹 서버를 도입하기로 했어요. 다만 이번엔 각자가 API 서비스와 연관 없이 프론트엔드 서빙에 대해서만 제한된 책임을 가지는 형태로요.

당근마켓 프론트엔드 챕터에선 이 방식으로 제공되는 웹뷰를 리모트 웹뷰라고 부르고 있어요. 네, 맞아요! 특별할 것 없는 Plain Old Web 기반 웹뷰예요. 😉

새로운 걸 기대하신 분들에게는 다소 실망스러운 결론일 수도 있지만, 편하고 익숙하던 방식을 버리는건 생각보다 어려운 일이에요.

그래도 웹 서버를 다시 도입하면서 웹 플랫폼 기능들을 활성화할 수 있었고, 각 프로젝트 담당자들이 권한을 나눠가지면서 저마다의 프로젝트 요구사항에 맞춰 SSR 같은 서버 측 기법들을 자유롭게 시도해 볼 수 있게 됐어요.

자체 웹뷰 시스템을 더 고도화하는 방법을 선택할 수도 있었겠지만, 앞으로 계속 발전할 웹 플랫폼을 더 잘 활용하기 위해 더 “Webby”한 방식을 채택하기로 했어요.

실제로 몇 년 전과 비교하면 웹 생태계가 많이 발전했어요. 브라우저 지원 수준이 좋아지고, 웹 프론트엔드 개발자들이 늘어나며 이전보다 더 많은 것을 다른 플랫폼에 의존하지 않고도 직접 시도할 수도 있게 됐고요.

예를 들면, 기존에 로컬 웹뷰에서 관리하던 컨텐츠 캐시는 웹 표준 캐시 메커니즘과 Service Worker 에서 제공되는 Cache API를 사용해서, 웹뷰가 아닌 웹에서 프론트엔드 개발자들이 직접 제어할 수도 있어요. Mozilla에서 제공하는 Service Worker Cookbook에서 이전에 소개한 로컬 웹뷰와 동일한 동작을 서비스 워커에서 구현하는 방법을 소개하고 있어요. (소스코드)

편리했던 Airport를 더 이상 사용하지 못하는 건 아쉽지만, 최근엔 다행히 다양한 플랫폼 서비스들이 비슷하거나 더 나은 기능들을 제공하고 있어요. 당근마켓에선 각자가 자율과 책임을 나눠가지는 만큼 이번 기회에 각 팀에서 제한없이 서비스를 사용해보고 인사이트를 공유해보기로 했어요.

Netlify 로고, Vercel 로고, Cloudflare Pages 로고에 둘러쌓여 있는 thinking_face 이모지
어떤 걸 써볼까?

최근 리모트 웹뷰 방식으로 전환된 프로젝트들은 AWS S3, Vercel, Netlify, Cloudflare Pages 등 다양한 플랫폼 서비스를 사용하고 있어요. 프로젝트마다 배포 방식이 제각각인 것은 혼란스러워 보이기도 하지만, 각자가 사용해 본 서비스들의 장단점을 빠르게 공유할 수 있어요.

다른 방식은 없을까?

배포와 통합을 외부 플랫폼에 아웃소싱 하고나서, 중요한 비즈니스 개발에 더 집중할 수 있어서 만족감을 느끼지만 동시에 한계를 느낄 때도 많아요.

그 중에서도 변화에 가장 적극적인 당근마켓 광고실의 프론트엔드 개발자 분들이 먼저 우여곡절을 겪기도 했어요. 국내에 CDN 엣지 로케이션이 없는 플랫폼을 사용하다가 성능 문제로 빠르게 마이그레이션을 진행하기도 했고, 사용자 경험 개선을 위해 코드 스플리팅을 적용하다가 배포 중 사용자 장애가 생기는 현상을 경험하기도 했어요.

무엇보다 대부분의 플랫폼 서비스들이 처음 사용할 때는 무료지만 성장에 따라 트래픽, 성능, 동시 빌드 수 등 비용 청구 항목이 가파르게 늘어난다는 것이 가장 큰 문제점 중 하나였어요. 당근마켓은 이미 MAU 1,800만이 넘는 서비스이기 때문에 작은 프로젝트라도 시작부터 큰 비용이 청구되어 평가에 어려움이 있고 플랫폼 락-인에 대한 우려가 생겼어요.

다른 인프라들 처럼 Terraform 모듈을 만들어서 AWS 인프라의 프로비저닝을 쉽게 하는 방법도 논의했지만, 이 방식은 프로젝트 별로 필요한 특별한 구성이나 리다이렉션 같은 메타데이터 객체를 관리할 만큼 유연하지 않아서 어려운 점이 많았어요.

최근에는 서비스 프로비저닝과 구성 관리를 지원하는 Kontrol이라는 사내 배포 시스템이 생겨서 당근마켓 인프라에 Next.js 서버를 띄우는 것은 쉽게 할 수 있게 되었지만, 아직 CDN이나 커스텀 도메인을 연결하는 등 웹 프론트엔드를 위한 엔드-투-엔드 구성이 지원되지 않아요.

이러다보니 Airport 같은 프론트엔드 전용 배포 플랫폼을 다시 개발하는 것도 챕터에서 자주 나오는 이야기거리 중 하나에요. 관련해서 조사했던 옵션 중에서 몇 가지를 소개해보려고 해요.

Cloudflare Workers Sites

최근 프론트엔드 챕터 위주로 사내 인프라와 의존성이 없는 퍼블릭 인프라 컴포넌트들을 Cloudflare Workers 기반으로 구현하는 일이 늘었어요.

혹시 Cloudflare Workers 기반으로 정적 웹사이트를 쉽게 배포할 수 있다는 사실을 알고 계셨나요? (Cloudflare Pages 서비스를 떠올리셨다면 그것과는 달라요!)

Cloudflare Workers에서 쉽게 접근해서 값을 읽고 쓸 수 있는 Cloudflare Workers KV 스토리지가 제공되는데, 여기에 컨텐츠를 저장해두고 서빙하는 방식을 Workers Sites라고 해요.

동작을 직접 구현할 수도 있지만 Cloudflare Workers 공식 CLI 인 Wrangler에서 Workers Sites를 쉽게 배포할 수 있는 통합 기능을 제공하고 있어요.

# Part of wrangler.toml# Worker Site를 정의하면 정적 에셋을 워커에 업로드 할 수 있어요.
# 자세한 내용은 문서를 참고해보세요:
https://developers.cloudflare.com/workers/platform/sites
[site]
# 정적 에셋이 들어있는 디렉토리에요.
# `wrangler.toml` 파일을 기준으로 상대경로를 적어요.
# `site` 필드가 있다면 `bucket` 필드를 반드시 포함해야해요.
bucket = "./public"
# `.gitignore` 스타일로 포함할 파일이나 디렉토리 패턴을 적어요.
# 마찬가지로 버킷 위치를 기반으로 해요.
# 일치하는 파일만 업로드 될 거에요.
include = ["upload_dir"]
# 명시적으로 포함하지 않을 파일들 패턴을 적어요.
# 일치하는 파일들은 업로드에서 제외될 거에요.
exclude = ["ignore_dir"]

설정 파일에 위와 같이 [site] 섹션이 있는 경우 wrangler publish 커맨드를 실행했을 때 지정한 사이트 에셋이 함께 배포돼요.

  1. Wrangler 커맨드가 설정에 따라 업로드 할 파일 목록을 조회해요.
  2. 각 파일의 이름에 컨텐츠 해시값을 더해 불변(Immutable) 키를 만들어 KV 네임스페이스에 업로드해요.
  3. 원래 파일명을 키로 불변 컨텐츠 주소를 반환하는 인덱스인 “사이트 매니페스트"를 생성해서 컨텐츠 KV 바인딩과 함께 워커 스크립트 내부에 주입해요.

이렇게 워커와 사이트 에셋을 배포하고 나면 URL로 부터 업로드된 컨텐츠를 찾아 응답을 반환하는 모듈인 kv-asset-handler를 사용하면 컨텐츠 서버를 구현할 수 있어요.

Wrangler가 각 배포 마다 고유한 매니페스트를 생성하기 때문에 이전에 ZIP 파일로 배포하고 다운로드 받던 때 처럼 배포의 원자성을 보장할 수 있어요.

Cloudflare Workers 인프라는 제공되는 KV 저장소의 레이턴시가 매우 작고, Cloudflare CDN 서비스를 통합하거나 네트워크 캐시 동작을 프로그래밍 방식으로 커스터마이징 할 수 있는 등 유리한 점이 많아요.

Karrotmini Playground

제가 속한 “당근미니” 팀에서는 서드파티 개발자가 참여해서 당근마켓 경험을 확장할 수 있는 기반을 마련하고 있어요.

당근미니 앱들도 웹 기반으로 구현되었지만 서드파티 컨텐츠를 조직에서 사용하는 플랫폼 서비스로 호스팅 할 수는 없었어요.

Workers Sites도 구현 방식은 참고가 가능하지만 Wrangler라는 프로젝트 커맨드에 의존하는 부분이 있어서 있는 그대로 활용할 수 없어요.

당근미니 팀은 웹 배포와 관련된 것들이 우리 서비스의 핵심 요소라고 생각하고, 확장성을 위해 필요한 기능들을 직접 구현하기로 했어요.

당근미니 플레이그라운드 앱 관리 화면 스크린샷
웹 호스팅 서비스인 “당근미니 플레이그라운드” 대시보드

이 호스팅 기능을 구현할 때 ,이전에 시도해본 다양한 방식들이 많은 참고가 됐어요.

배포 기능을 만들 때, 배포에 포함된 파일을 각각 업로드하면 스토리지에 기하급수적인 쓰기 요청이 발생할 것에 대한 우려가 있었어요. Cloudflare Workers KV 나 R2 같은 주로 정적 에셋을 업로드하는 스토리지들은 반영구적인 보존과 읽기 비용은 저렴하지만 쓰기 비용은 그다지 저렴하지 않아요.

실제로 미니앱 팀이 직접 운영 중인 한 사이트는 26K 페이지를 정적 렌더링해서 Workers Site 방식으로 배포하고 있는데, 많은 페이지를 주기적으로 동기화하는 과정에서 한 달에 10만원 정도의 비용이 청구된 적 있어요. 엄청 비싼 건 아니지만 트래픽 때문이 아니라 순수하게 배포를 동기화하는데만 이 정도의 초과비용이 발생한 거라 확장하기 어려운 방식이라고 판단했어요.

그래서 저희는 이미 경험해 본 방식들을 조합해서, 배포는 단일 ZIP 포맷으로 압축하지만 서빙은 일반 웹 서버 처럼 할 수 있도록 Cloudflare Workers으로 ZipFS(ZIP 컨텐츠가 마운트 된 파일시스템) 기반 컨텐츠 서버를 구현했어요.

당근미니 플레이그라운드 “Webapp Controller” 서비스 도식화

ZIP 포맷은 파일 이름을 읽을 수 있는 헤더 영역과 실제 컨텐츠가 들어있는 데이터 영역이 나뉘어 있어요.

참고:

리소스 요청을 받았을 때 전체 ZIP 아카이브 다운로드는 네트워크 대역폭에 제한이 없는 Cloudflare 인프라 내에서만 처리하고, 네트워크 대역폭이 제한된 서버와 클라이언트 사이에서는 응답에 필요한 컨텐츠만 추가로 압축을 풀어 다운로드 내려줄 수 있어요.

이렇게 하면 로컬 웹뷰 방식처럼 배포가 심플한 ZIP 배포를 유지하면서, 클라이언트 입장에서는 일반 웹 서버 응답과 동일하게 보이고, 앞에서 언급했던 Cloudflare Workers 기반 인프라의 장점도 그대로 취할 수 있었어요.

물론 이 방식에도 단점은 있어요.

Cloudflare Pages vs 당근미니 플레이그라운드 TTFB 비교
  • 중간과정에 따라 약간의 TTFB(Time To First Byte) 지연이 있어요;
    같은 Cloudflare 인프라 기반으로 최적화된 서비스인 Cloudflare Pages와 비교했을 때 30~40ms의 추가 레이턴시가 있었어요. 하지만 어느정도 감수할 수 있고 몇몇 무료 호스팅 서비스와 비교하면 더 빠른 수준이에요. JavaScript로 구현한 PoC를 Rust + WASM 기반으로 포팅해서 p90 성능을 추가로 개선했어요. 나머지 차이도 CDN 캐시를 적용하면 완화할 수 있어요.
  • KV의 값 크기 제한 때문에 한 배포(ZIP)의 사이즈가 25MiB를 넘을 수 없어요;
    파일을 여러개로 분할해서 업로드할 수 있는 방식, R2 Storage로 스토리지 백엔드를 전환하는 방식, 제한적으로 업로드한 ZIP 아카이브를 미리 압축을 풀어 저장해두고 사용하는 방식 등을 추가로 검토하고 있어요.
  • 정적 컨텐츠 서빙 전용이기 때문에 서버 측에서 추가 코드를 실행할 수 없어요;
    엣지에서는 코드를 실행할 수는 있지만 서드파티 앱 컨텐츠가 대상이면 여러 제약사항이 생겨요. 서버 기능 확장을 안전하게 허용하는 방법을 연구하고 있어요.

어쨌든 이렇게 만든 컨텐츠 서버에 Cloudflare SSL for SaaS 서비스를 이용해서 앱별로 고유한 호스트 이름과 인증서를 만들어 연결해주면 Cloudflare 위에서 프론트엔드 앱을 위한 전체 엔드-투-엔드 구성을 완성할 수 있어요!

당근미니 팀에서는 호스팅 서비스인 “당근미니 플레이그라운드”와 이걸 코어로 하는 다른 비즈니스향 제품을 함께 만들고 있는데, 코어인 플레이그라운드는 기술을 유연하게 실험하면서 독립적으로 발전시키기 위해 비즈니스 서비스와 분리해서 개발되고 있어요. 😄

Web Packaging Standard

사실 웹 표준 그룹에는 이 것과 비슷한 문제들을 다루는 “Web Packaging”(fomally WPACK)이라는 사양 그룹이 있어요.

여기서 특정 HTTP 요청/응답 쌍을 HTTP Exchange라고 하는데 웹 패키징 그룹에는 여러 HTTP Exchange들을 하나의 Archive 파일로 묶고 서버 또는 오프라인에서 효과적으로 서빙할 수 있게 하는 Web Bundle 포맷, 원본 HTTP Exchange에 대한 서명을 만들어서 타사 캐시를 통해 제공 가능하게 만드는 SXG(Signed Exchanges) 같은 사양들이 포함되어 있어요.

혹시 모바일 환경에서 AMP(Accelerated Mobile Pages)로 개발된 웹 페이지를 열면 로딩 없이 순식간에 컨텐츠가 나타나는 것을 본 적이 있나요? 이 놀라운 경험을 제공하는 AMP 캐시는 일부 웹 패키징 사양을 기반으로 만들어졌어요.

이 중에서도 웹 번들 포맷은 ZIP + Deflate 와 비슷한 압축 성능을 가지고 있고, 압축 방식이 웹 요청/응답에 특화되어 있어서 정적인 컨텐츠 서버를 만들 때 아주 효율적이에요.

당근미니 팀에서 ZIP을 사용하는 배포 방식의 한계를 돌파하기 위해 여러 새로운 포맷을 검토하고 있고, 그 유력한 후보 중 하나로 웹 패키징 사양을 연구하고 있어요. 앞으로 웹 패키징을 구현해서 플레이그라운드에 적용해보고 유용한 것들은 오픈소스로 공개할 예정이에요.

이번에 소개한 내용들은 어쩌면 기대했던 것 보다 새롭지 않을 수 있어요. 우리는 은총알을 찾기 보다 상황에 맞는 올바른 트레이드오프를 선택하기 위해 노력하고 있어요. 그러면서도 기존 방식의 익숙함에 안주하지 않고 프로덕트 성장에 발맞춰 지속적으로 진화하고 있어요.

배포 방식만으로 당근마켓 프론트엔드 시스템을 설명할 수 있는 것은 아니에요. 당근마켓 앱의 웹뷰도 시스템의 중요한 한 축이라고 볼 수 있어요. 당근마켓의 웹뷰 활용 방식과 관련 고민들에 대해서는 또 다른 글에서 다시 찾아뵙도록 할게요!

👉당근마켓에서 멋진 프론트엔드 시스템을 함께 만들어갈 분을 찾고 있어요!

--

--

당근마켓은 동네 이웃 간의 연결을 도와 따뜻하고 활발한 교류가 있는 지역 사회를 꿈꾸고 있어요.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store