당근마켓에 웹 프로젝트 배포하기 #1 — 파일 기반 웹뷰

당근마켓은 앱 화면의 많은 부분을 네이티브 웹뷰와 웹 기술을 활용해 만들고 있어요.

최근에는 많은 팀이 함께 쓰던 공용 시스템을 벗어나 큰 변화를 만들어가고 있어요. 더 나은 사용자 경험을 만들어 나가기 위해서 변화를 더 미룰 수 없다는 판단과 공감대가 있었기 때문인데요. 예전엔 어떤 선택을 했었고 왜 지금은 다른 선택을 하게 되는 걸까요? 지금까지 만났던 다양한 고민거리와 해결 전략들을 공유해볼게요.

당근마켓 웹 배포 방식 변천사

당근마켓에 웹뷰 기반 서비스가 처음 추가된 것은 2019년 중순으로 그리 오래된 일이 아니에요. 하지만 저는 그 이후인 2021년에 입사했기 때문에, 제가 잘 모르는 맥락은 당시부터 최근까지 당근마켓의 웹 프론트엔드를 일궈오다 지금은 SRE로 전환한 Sean(J2P)의 이야기를 바탕으로 풀어보았어요.

초창기 당근마켓 모바일 개발은 Android/iOS 위주였고, 웹은 상황에 따라 일부분에 사용되었어요.

웹 기반으로 작성된 화면은 어느 플랫폼에서라도 웹뷰만 있다면 똑같이 사용할 수 있기 때문에 네이티브 앱이라도 일부 화면에 웹을 채택하는 것은 드물지 않았는데요. 일부인 웹 화면을 위해 전용 인프라를 유지하기보단 API를 담당하는 레일즈 서버에서 함께 처리하고 있었어요.

하지만 이렇게 만들어진 웹 기반 화면들은 몇 가지 문제가 있었어요.

  • 네이티브 UI에 비해 사용성이 떨어졌어요.
  • 점점 확장되는 레일즈 서버에 역할이 과하게 집중되는 문제가 있었어요.
  • 레일즈 서버에 부하가 커져서 서버가 느려질 때 화면 로딩이 함께 느려졌어요.

이런 문제점들을 개선해보기 위해 그 당시 조심스럽게 SPA(Single Page Application) 방식을 검토했었고, 중고거래 서비스의 “신고하기” 기능을 Vue 기반 SPA로 분리한 것이 당근마켓 웹 프론트엔드의 첫 시작이었어요.

당근마켓 중고거래 게시글 신고하기 기능 스크린샷
Vue로 개발된 당근마켓 중고거래 “신고하기" 화면

그리고 동시에 분리된 SPA를 레일즈 서버와 독립적으로, 어떻게 더 효과적으로 서빙할 수 없을까? 라는 고민이 시작됐어요.

ZIP Archive + 로컬 파일 웹뷰

웹뷰 기반 화면은 네이티브 화면과 다르게 추가적인 다운로드가 필요해요. SPA 방식은 화면을 그리는 코드를 한 번에 다운로드 받기 때문에 네비게이션 할 때마다 네트워크 응답을 추가로 기다릴 필요가 없지만 모든 화면 코드를 한 번에 받는 만큼 첫 로딩 시간이 길어질 수 밖에 없어요.

웹뷰 진입 시 전환이 느려지는 현상을 개선하기 위해서 진입 이전에 미리 로딩하는 방법을 구상했어요. 앱을 실행했을 때 백그라운드에서 미리 웹 파일들을 다운로드 받고, 진입할 때는 네트워크가 아니라 파일로(file://) 웹뷰를 실행하면 추가 로딩 없이 화면 전환을 할 수 있어요.

이 때 새버전이 배포되었는지 여부를 먼저 확인해서, 항상 파일을 다운로드 받지 않아도 이전에 받아 둔 파일을 캐싱해뒀다가 사용할 수 있어요.

필요한 파일들만 한 번에 로딩하기 위해서, 컨텐츠를 ZIP 파일로 묶어서 배포하고 클라이언트에서 푸는 방식을 선택했어요. 이렇게 하면 배포의 원자성(Atomicity)을 쉽게 보장할 수 있어요.

1. 새 버전이 있는지 확인한다. 2. 새 버전이 있다면 ZIP 파일을 다운로드. 3. 저장된 ZIP 파일 압축을 풀어서 사용한다.
간단하게 표현한 로컬 웹뷰 로딩 방식

당근마켓 프론트엔드 챕터에선 이 방식을 로컬 웹뷰라고 부르고 있어요.

이 후 웹뷰를 사용하는 서비스가 늘어나면서 앱 실행 시 모든 서비스를 로딩 하는 대신 서비스마다 최초 진입할 때 한 번 로딩하는 방식으로 변경되었어요.

Airport™

당근마켓의 거의 모든 웹 프로젝트가 로컬 웹뷰 방식에 정착한 이 후, 배포 방식이 통합됨에 따라서 배포와 관련된 일반적인 문제를 고민할 수 있게 되었어요.

가령 A/B 테스트를 위해 여러 버전의 앱을 배포하고 사용자 별로 다르게 노출하는 기능이 필요하면 어떻게 할까요?

기존에는 모듈러 연산으로 사용자 그룹을 구분하고 기능 활성화 하는 것을 클라이언트에서 모두 처리하는 방식을 사용하고 있었어요. 새로운 실험을 넣거나 기존 실험을 제거하기 위해 코드를 수정하고 다시 배포했고요. 원시적인 방법이지만, 웹 배포는 네이티브 앱 배포에 비해 자유롭고 서비스 별로 배포가 나뉘어져 있기 때문에 그럭저럭 괜찮았어요.

그러던 어느 날… 전 이 방식의 한계에 대해 답답함을 느끼기 시작했어요. 슬랙 채널에서 팀원인 Tony(Tony W)에게 불평했고요. 자리에서 만나 각자가 생각하는 이상적인 A/B 테스트 배포 방식에 대한 이런저런 대화를 나눴던 기억이 나네요.

  • 모든 커밋에 대해 배포가 자동으로 이루어지고
  • 모든 배포에 대해 접근할 수 있는 퍼머링크가 생성되고
  • 프로덕션은 여러 배포에 대해 지정한 비중에 따라 사용자가 분산되는

이런 방식으로 웹 서비스에서 쉽게 관리도 가능했으면 좋겠다~ 이런 이야기를 나눴어요.

Tim: A/B 관련 처리는 계속 클라에서 id 보고 할게 아니라 웹뷰에서 처리해줘야 하는 거 아니에요…..?? 특정 브랜치에서 zip 파일 배포해서 로딩하는 곳에서 weight 결정해서 봐야할 거 같은데…
이 사람(=나)은 직접 구현할 것도 아니면서 불만이 많아 보인다…

그리고 이틀 후(!!), 당근마켓에는 Airport라는 이름의 새로운 배포 도구가 생겼어요.

당근마켓 새로운 배포 시스템인 Airport 공지, 로컬 웹뷰 아티팩트 관리와 롤백, 미리보기, A/B 테스트 지원 등의 기능 안내와 기존 로컬 웹뷰 배포도구에서 마이그레이션 하는 방법이 적혀있다.
그런데 그것이 실제로 일어났습니다. 불과 이틀 만에…

기존에 사용하던 로컬 웹뷰 배포용 도구들을 대체하는 Airport CLI와 웹에서 쉽게 모든 배포를 확인하고 관리할 수 있는 Airport Tower 라는 콘솔이 생겼어요.

웹에서도 Airport Tower로 쉽게 배포와 실험을 관리할 수 있습니다!

지금 방식이 최선일까?

시간이 지나면서 당근마켓의 프론트엔드 챕터는 점점 더 크고 성숙해져갔고, 웹 기반 서비스와 웹을 다루는 엔지니어들이 늘어나면서 “웹” 을 플랫폼으로 사용하는 것에 대해 다시 고민하기 시작했어요.

로컬 웹뷰 방식에는 지속하기 어려운 단점들이 있어요.

  • 웹 플랫폼 API들은 모두 “출처”(Origin) 기반 보안 모델을 따르고 있어요. 출처가 없는 파일 프토토콜(file://) 기반 브라우징 컨텍스트에서는 일부 API에 접근할 수 없는 문제가 있어요. (예시: WebWorker 사용 불가)
  • 출처 기반 격리 메커니즘을 사용할 수 없어요. 서비스마다 로컬 스토리지 공간이 격리되지 않아서 스토리지에 키를 쓰거나 제거할 때 다른 서비스에도 영향을 미칠 수 있어요.
  • 출처 정보를 활용하는 서드파티 스크립트를 사용할 수 없었어요. 대표적으로 GTM을 사용할 수 없었고 다른 어떤 스크립트는 출처가 없어 요청이 차단되기도 했어요.
  • 웹 서버가 없기 때문에 SSR(Server-Side Rendering)이나 RSC(React Server Components) 같이 서버가 개입되는 기법들을 도입해 볼 수 없었어요.
  • SPA를 더 본격적으로 사용하면서 번들 사이즈가 2MiB 가 넘어가는 프로젝트도 생겼고, 로딩시간이 너무 길어져서 사용자 경험에 안좋은 영향을 미치기 시작했어요. 하지만 로컬 웹뷰 방식에선 배포에 포함된 전체 리소스가 항상 단일 ZIP으로 묶여서 다운로드 되기 때문에 흔히 도입하는 Code-splitting 등의 기법으로 개선할 수 없었어요.

웹 생태계는 계속 발전하고 있는데, 우리는 로컬 웹뷰의 벽에 막혀 다음으로 나아갈 수 가 없었어요. 우리는 다시 무엇이 맞는 방식인가 고민해보기 시작했어요…

당근마켓 프론트엔드 시스템이 어떻게 변화하고 있는지, 그리고 이어지는 고민들은 무엇인지, 다음 글에서 공유해보도록 할게요.

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

--

--

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

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