배포 없이 앱과 로컬 웹을 잇다
들어가며
배달의민족은 네이티브 앱과 웹으로 구성된 하이브리드 앱입니다. 배달의민족 앱을 열면 상단 탑바와 하단 탭바가 빠르게 반응하며 자연스럽게 전환되는 모습을 쉽게 확인할 수 있습니다. OS와 직접 상호작용하고 사용자의 입력에 즉각적으로 반응해야 하는 영역은 네이티브 앱이 맡습니다.
앱의 모든 화면을 네이티브로 구현할 필요는 없습니다. 빠르게 만들고 배포할 수 있는 웹의 장점을 살려, 일부 화면은 웹뷰로 구성합니다. 배달의민족 앱에서 웹과 앱의 경계는 기술적 한계가 아니라, 각 기술이 가장 잘하는 일을 맡도록 의도적으로 나눈 결과입니다. 다만 웹의 입장에서 보면, 결국 앱이라는 집 안에 웹뷰로 셋방살이를 하는 구조이기도 합니다. 문제는 남의 집에 얹혀 사는 형편이 녹록지 않다는 것입니다.
웹 개발자는 개발한 내용을 확인하기 위해서 기본적으로 베타 배포가 필요합니다. 베타 배포를 하기 위해서는 매서운 코드 리뷰를 거쳐서 유닛 및 통합 테스트를 지나 CI 빌드, 배포, 캐시 무효화의 과정을 거쳐야 합니다. 작은 코드 수정 하나를 확인하기 위해서 번거로운 과정을 반복해 거치다 보면 월셋집이더라도 과감하게 리모델링하고 싶은 욕구가 샘솟곤 합니다.
배포하지 않는다고 개발을 못하는 것은 아닙니다. 브라우저 확장 프로그램을 잘 활용하면 벽에 못질을 하지 않고도 걸 수 있는 선반처럼 앱과 유사한 환경이 그럴싸하게 만들어집니다. 그 외에도 배달의민족 앱에서 내 로컬 환경을 바라볼 수 있도록 해주는 좋은 도구들이 있습니다. 하지만 월셋집 무타공 선반에 너무 많은 걸 얹을 수는 없는 걸까요. 앱의 새로운 기능이 나올 때마다 브라우저 확장 프로그램은 계속 유지보수가 필요하고 사내 업무 환경에 맞지 않는 도구를 사용하다 보면 일상다반사로 버그가 생깁니다.
위와 같은 한계를 극복하기 위해 앱 위에서 실시간으로 동작할 수 있는 로컬 환경을 구성하고 싶었습니다.
“배포 없이 빠르게 개발 환경을 앱에서 확인할 수는 없을까?”
이 질문의 해답을 찾기 위해 사내 환경에 최적화된 터널링 기반 프록시 시스템인 ‘잇다’(ITDA – Instant Tunnel for Development Apps) 개발 프로젝트가 시작되었습니다.
우리 집 구조 먼저 알기
가장 먼저 해야 할 일은 개발 환경이 어떻게 동작하고 있는지 확인하는 것이었습니다.
배달의민족 앱에 로컬 웹 개발환경을 구성하기 위해서는 웹 요청을 가로채서 로컬 환경을 바라볼 수 있도록 해야 합니다. 단순히 모든 웹 요청을 로컬 환경으로 보낼 수는 없습니다. 배달의민족 앱에는 수많은 다른 세입자가 함께 살아가고 있습니다. 모두를 한 집으로 몰아넣으면 앱이 제대로 동작할 수 없습니다. 가장 좋은 방법은 다른 세입자가 영향받지 않도록 특정 사용자의, 특정한 요청만 우리가 원하는 로컬 환경으로 보내줄 수 있도록 깔끔하게 분리해내는 것입니다.
조금 더 자세히 들여다보면, 앞선 설명처럼 배달의민족 앱은 웹뷰 컨테이너 안에서 웹 콘텐츠를 실행하고 있습니다. 웹뷰는 DNS 조회로 CloudFront의 주소를 확인한 뒤 HTML을 요청합니다. CloudFront는 S3에서 HTML을 가져와 웹뷰에 응답합니다. 이 흐름 위에서 어떤 요청을 로컬 환경으로 보내야 하는지 식별할 수 있어야 합니다.
한편, VPN 문제도 있습니다. 사내 VPN은 동적 IP 주소 정책을 채택하고 있습니다. 특정 송신인의 택배만을 적절히 추려낸 뒤 수신인의 집으로 보내주어야 하는데 집 주소가 자꾸 바뀌는 꼴입니다. 사내 VPN이 고정 IP 주소 기반이었다면 주소가 바뀌지 않았겠지만 어쩔 수 없는 노릇입니다.
택배를 해당 사용자가 아닌 다른 사람이 열어볼 수 없도록 보안도 신경써야 합니다. VPN은 아주 프라이빗한 오피스텔처럼 훌륭한 보안을 제공하지만, 택배가 출발하는 베타앱은 사내망 외부에 존재합니다. 택배는 베타앱을 떠나 사내망 영역에 들어오기 전까지 늘 위험과 함께합니다.
트래픽 전환 설계
먼저 어떻게 베타앱의 요청을 로컬 환경으로 전환할 수 있을지 검토했습니다. 모두 로컬 환경으로 이어지는 것은 동일하지만 트래픽을 전환하는 주체는 다릅니다. 아래에서 말하는 ITDA 프록시는 앱의 요청을 연결된 개발자의 로컬 서버로 전달하는 역할을 합니다.
| 구분 | A. CFF rewrite | B. 앱 내부 호스트 라우팅 | C. CFF redirect |
|---|---|---|---|
| 동작 방식 | 기존 웹 주소를 유지한 채 내부 목적지만 ITDA 프록시로 변경 | 앱에서 특정 웹 주소의 목적지를 변경 | ITDA 프록시 주소로 다시 요청하도록 안내 |
| 기대한 점 | 앱을 수정하지 않고 요청을 전환할 수 있음 | 앱 네이티브 계층에서 요청의 목적지를 직접 전환할 수 있음 | 앱을 수정하지 않고 일반 베타 트래픽과 ITDA 트래픽을 분리할 수 있음 |
| 판단 | 운영·보안 요구사항과 결합하기 어려워 기각 | 지원 대상마다 앱 변경이 필요하고 라우팅 책임이 앱에 쌓여 기각 | 도메인 전환에 따른 웹 보안 정책과 쿠키·세션을 검토한 뒤 채택 |
A안: CFF rewrite – 주소는 그대로, 목적지만 바꾸기
첫 번째로 검토한 방법은 CFF(CloudFront Function)에서 요청의 origin, 즉 실제로 콘텐츠를 가져올 서버를 ITDA 프록시로 바꾸는 rewrite 방식이었습니다. 요청이 들어오는 엣지에서 목적지를 바꾸기 때문에 앱을 수정하지 않아도 되고, 웹뷰에 보이는 기존 주소도 유지할 수 있다는 장점이 있었습니다.
하지만 내부 목적지를 직접 바꾸는 구성은 보안 요구사항을 충족하기 어려웠고, A안은 제외했습니다.
CloudFront Function를 사용할 수 없는 건 아니었습니다. origin을 교체하는 역할에는 맞지 않았지만, 요청을 판별하는 위치로는 활용할 수 있었습니다. C안에서는 이 역할만 남겼습니다.
B안: 앱 내부 커스텀 DNS – 앱에서 바꿔치기
두 번째로 검토한 방법은 앱의 네이티브 계층에서 분기하는, 즉 앱 내부 호스트 라우팅을 적용해 특정 웹 주소의 요청을 ITDA 프록시로 보내는 방식이었습니다.
앱이 직접 요청의 목적지를 바꾸면 웹에서는 원래 주소를 그대로 사용할 수 있습니다. 반면 지원해야 할 웹 도메인이 추가될 때마다 앱 개발팀에 변경을 요청해야 했고, 원래 서비스 주소와 세부 경로처럼 웹 라우팅에 관한 정보와 책임이 앱에 쌓이는 문제가 있었습니다.
이 방식에서는 ITDA가 지원하는 주소가 늘 때마다 앱 수정과 배포 일정을 함께 고려해야 합니다. 그래서 B안은 채택하지 않았습니다.
C안: CFF redirect – 새 주소를 알려주고 요청을 다시 부르기
최종적으로 선택한 방법은 CloudFront Function이 ITDA용 요청을 판별한 뒤 ITDA 프록시로 302 응답으로 redirect하는 방식이었습니다.
rewrite가 하나의 요청 안에서 목적지만 내부적으로 바꾸는 방식이라면, redirect는 웹뷰에 새로운 목적지를 알려주고 그 주소로 다시 요청하도록 하는 방식입니다. 일반 베타 요청과 ITDA 요청의 흐름을 명시적으로 분리할 수 있었고, 새로운 웹 도메인을 지원할 때마다 앱을 수정할 필요도 없었습니다.
요청의 도메인이 바뀌면 서로 다른 웹 주소 사이의 요청을 제한하는 브라우저 보안 정책과 쿠키·세션의 적용 범위를 함께 검토해야 합니다. 이 문제를 웹과 인프라에서 관리할 수 있다고 판단해 C안을 선택했습니다.
C안은 지원 대상이 늘어나도 앱을 수정할 필요가 없고, 라우팅과 관련된 운영·보안 요구사항을 만족한다는 점에서 목표에 가장 잘 맞았습니다.
앱에서 내 컴퓨터까지
베타 앱에서 개발자 맥북까지 이어지는 ITDA의 최종 구조는 다음과 같습니다.
1. 터널은 로컬에서 직접 엽니다
외부의 모바일 기기를 개발자의 컴퓨터에 직접 연결하기는 어렵습니다. 그래서 반대 로, 개발자의 컴퓨터에서 실행한 ITDA 클라이언트가 ITDA 프록시와 웹 소켓 연결을 먼저 만들고 유지합니다. 개발자 환경이 프록시에 먼저 연결하므로, 모바일 기기에서 개발자의 컴퓨터로 요청을 전달할 수 있습니다.
2. 요청과 터널을 잇는 규칙
터널이 연결되었다고 해서 모든 요청을 그 터널로 보낼 수는 없습니다. 어떤 프로파일로 들어온 어느 서비스의 요청을 어느 터널로 전달할지 미리 정해야 합니다.
라우팅 규칙은 프로파일과 요청 대상, 현재 연결된 개발 터널을 연결합니다. ITDA 프록시는 요청에 담긴 정보와 이 규칙을 비교해 알맞은 터널을 고릅니다.
3. 이 요청, ITDA 것 맞나요?
사용자가 베타앱에서 ITDA용 프로파일을 선택하고 화면을 열면, 최초 요청에 사용자를 식별할 수 있는 리모트 컨피그 프로파일 파라미터가 포함됩니다.
CloudFront Function은 이 값이 ITDA용 프로파일인지 확인합니다. 일반적인 요청이라면 아무것도 바꾸지 않고 기존 요청을 그대로 보냅니다. ITDA용 요청이라면 프록시가 터널을 찾는 데 필요한 최소한의 라우팅 정보를 쿠키에 담아 redirect합니다.
아래 코드는 실제 판별 규칙과 내부 주소, 쿠키 이름·속성, URL 조합 로직을 제외한 예시입니다.
CFF에서는 다음 세 가지 작업을 수행합니다.
- 요청에 포함된 리모트 컨피그 프로파일(remote_config_profile)을 읽어 ITDA용 프로파일인지 확인합니다.
- 일반 베타 요청이면 기존 흐름을 그대로 유지합니다.
- ITDA용 요청이면 라우팅 정보를 전달하며 프록시로 302 redirect합니다.
CFF는 어느 개발자의 컴퓨터로 요청을 보낼지까지 결정하지 않습니다. ITDA가 처리할 요청인지 판별하고 라우팅 정보만 넘깁니다. 프록시는 이 정보와 미리 등록한 라우팅 규칙을 비교해 요청을 전달할 터널을 결정합니다.
4. 한 터널에 여러 짐 싣기
웹 화면 하나를 표시하려면 HTML뿐 아니라 JavaScript, CSS, 이미지 등 여러 리소스를 함께 요청해야 합니다. 요청이 생길 때마다 새로운 터널을 만드는 대신, ITDA는 미리 연결된 하나의 웹 소켓을 함께 사용합니다.
ITDA는 각 요청에 고유한 ID를 붙입니다. 로컬 개발 서버에서 응답이 돌아오면 같은 ID를 기준으로 원래 요청과 연결합니다. 따라서 여러 요청이 하나의 웹 소켓 연결을 함께 사용해도 응답이 뒤섞이지 않습니다.
개발자의 컴퓨터에서 실행 중인 ITDA 클라이언트는 전달받은 요청을 로컬 개발 서버로 보냅니다. 로컬 서버의 응답은 같은 경로를 반대로 이동해 실제 앱 화면에 나타납니다.
5. 길이 막히면 원래 집으로
로컬 환경은 언제든 종료될 수 있고, 터널은 끊길 수 있습니다. 일반 웹 요청에 맞는 라우팅 규칙이 없거나 연결된 터널을 찾을 수 없다면 요청을 원래 베타 환경으로 돌려보냅니다. ITDA를 사용하지 않는 요청에 영향을 주지 않고, 개발 중 연결이 끊겨도 기존 베타 화면을 계속 확인할 수 있도록 마련한 안전장치입니다. 또한 앱의 요청과 개발자의 로컬 환경을 연결하는 라우팅 규칙들은 오래 보관해야 하므로 별도의 DB에 저장하고, 현재 연결된 웹 소켓은 프록시가 직접 관리합니다.
마치며
유사한 서비스나 기능들은 이미 존재하고 있고, 현재도 많은 곳에서 활용하고 있습니다. 다만 우리의 문제를 완벽하게 해결하기엔 부족했기에, ITDA를 맞춤 개발했습니다. ITDA에서 활용하는 웹 소켓이나 프록시, 터널링은 기존에 없던 기능이나 개념이 아닙니다. 고민을 거듭하는 과정에서 최선이라고 생각한 하나의 방안일 뿐입니다. 이제는 여러 AI 도구의 도움을 받아 생각을 쉽게 구체화할 수 있습니다. 이런 시대에서 중요한 것은 명확한 목표, 그리고 활용할 수 있는 자원에 대한 정확한 이해입니다.
이번 과제를 진행하는 도중 여러 번 작업 방향을 바꾸며 기존과 다른 새로운 설계를 고민해 볼 수 있었습니다. 그 과정에서 얻은 가장 큰 교훈은 문제를 해결하기 위한 정답은 단 하나가 아니라는 것입니다. 아무리 좋은 재료라도 다룰 수 없는 것이라면 활용할 수 없는 것이니까요. 완성된 ITDA 시스템의 전체 모습을 보면 웹뷰를 프록시하는 과정이 기능에 비해 생각보다 간단한 구조라고 느껴지실 수도 있습니다.
하지만 지금의 형태는 현재 활용 가능한 자원과 여러 제약 내에서 가장 효과적이고 효율적인 구조를 찾고, 적합하다는 판단하에 만든 결과물입니다. 만약 다른 환경과 자원이었다면 다른 선택을 했을지도 모릅니다. 그땐 그때 가장 최적이라고 판단한 선택지를 고르지 않을까요?