흩어져 있던 배민주문접수: 크로스 플랫폼으로 다시 쓰기

일시
2026.10.28 11:50-12:30
트랙
트랙 E
연사
홍영민

#프런트엔드

세션 소개

배민주문접수는 Windows, Android, iOS, macOS 네 환경을 지원하는 주문 처리 프로그램입니다. 플랫폼별 네이티브 구조를 웹뷰 기반 크로스 플랫폼으로 전환한 과정과, 늘어난 업무를 감당하려고 AI를 개발 프로세스에 들인 방법을 다룹니다.

핵심 내용

웹뷰를 고른 이유와 1차 전환

플랫폼마다 따로 개발하던 구조의 비용을 줄이려고 UI와 비즈니스 로직은 웹뷰에 두고 네이티브 기능만 브릿지로 처리하는 구조를 골랐습니다.

  • 기존 문제: 기능마다 기획·디자인·개발을 플랫폼 수만큼 반복, 플랫폼마다 다른 UX, 한 플랫폼이 늦으면 전체 배포 지연, 실시간 업데이트가 안 돼 사용자마다 버전이 다름
  • 웹뷰로 얻는 것: 중복 개발 제거, 모든 플랫폼에서 같은 UX·UI, 앱 업데이트 없는 반영
  • 1차 전환: 이미 Flutter로 작성된 Android 태블릿과 macOS. UI와 로직만 웹뷰로 옮기고 네이티브 기능은 기존 Flutter 코드를 그대로 사용

AI 하네스를 밑바닥부터 만든 방법

화면·로직·인증·네이티브 연동·네 플랫폼 관리가 모두 프런트엔드 몫이 되면서, 남은 플랫폼 전환과 VOC(고객의 소리) 대응을 같은 인원으로 하려고 AI 하네스를 직접 만들었습니다.

  • 이전 시도의 실패: CodeRabbit knowledge base에 넣은 리뷰 기준 문서를 리뷰 때 거의 참고하지 않음, QA 테스트 케이스 기반 검증에서 false positive·false negative 발생, 외부 스킬·에이전트·하네스는 서로 충돌하거나 도메인에 맞지 않음

방법 1: 귀납적으로 시도하기

단계하는 일
작업 분류기존 MR을 쉬움, 보통, 어려움으로 나눔
시키기AI에게 작업을 맡기고, 결과를 사람이 보고 피드백
기록피드백을 룰과 스킬에 계속 반영해 새 규칙을 쌓음

방법 2: 작업 단위와 역할 나누기 — 기획서 전체를 한 번에 넘기면 빠지는 내용이 생겨 작업 단위로 쪼갰습니다. 사람 검토를 통과한 테크스펙만 구현하게 하자 정확도가 크게 높아졌습니다.

단계담당내용
작업 전달사람작업마다 해당 기획서와 피그마를 넘김
테크스펙AI작업별 테크스펙 작성
검토사람테크스펙 확인
구현AI검토를 통과한 테크스펙대로 구현

방법 3: 코드처럼 관리하기 — 규칙과 메모리 파일을 git으로 관리하고 팀 공통 메모리를 써서, 누가 AI를 쓰든 같은 결과물이 나오게 했습니다.

계기규칙·메모리 파일 변경
정책·코드 변경새 정책 규칙 추가, 코드 변경에 맞춰 규칙 수정
LLM 버전 업그레이드불필요해진 규칙 삭제
AI 피드백 결과공통 메모리에 반영

개발 기간 단축과 코드 리뷰 따라잡기

과제 개발 기간이 하루에서 몇 주까지 들쭉날쭉하던 데서 평균 하루로 줄었고, 일정을 예측할 수 있게 됐습니다. 대신 MR이 사람이 따라가기 어려운 속도로 쌓여 검증도 AI로 자동화했습니다.

장치역할
CodeRabbit기본 리뷰
코드 리뷰 봇MR 단위 자동 리뷰
E2EBlink, WebKit에서 주문 접수, 주문 상세, 배달 완료 흐름 검증
구형 웹뷰 검증최소 지원 버전(WebView 51)에서 Polyfill 누락과 CSS 깨짐이 없는지 테스트
  • 코드 리뷰 봇: CodeRabbit 리뷰를 1차 검증하고 팀 컨벤션·기존 사람 리뷰 패턴을 더함. 자기 검증 오류를 줄이려고 코드 작성 모델과 리뷰 모델을 다르게 둠
  • E2E: Playwright로 두 엔진 교차 검증, 목 데이터는 실제 API 로그에서 뽑는 스킬로 관리
  • 구형 웹뷰 검증: AI가 최소 지원 버전 에뮬레이터에 결과를 띄우고 문제가 없어질 때까지 고친 뒤 MR 작성

AI 하네스 전체 흐름

단계담당내용
기획·피그마입력
테크스펙AI기획과 피그마를 보고 작성
사람 검토사람AI가 알 수 없는 영역 보완
구현AIUI와 비즈니스 로직, 단위·통합 테스트
검증AIE2E, 구형 웹뷰
리뷰AI코드 리뷰 봇, CodeRabbit
사람 승인사람MR 머지
  • AI 단계는 모두 코드처럼 관리하는 룰·스킬·에이전트 기반, 사람 검토·승인의 피드백은 다시 룰·스킬·에이전트에 반영
  • 코드베이스 밖 정보는 MCP로 연결

Windows: Delphi 컨테이너와 트레이 팝업 메모리

사용자 약 21만 명 중 약 14%가 Windows 7이라, Windows 7을 지원하는 Delphi를 컨테이너로 고르고 웹뷰는 CEF를 씁니다. Flutter와 Electron은 최신 버전이 Windows 7을 지원하지 않았고, Delphi는 기존 네이티브 기능과 PC 개발자 지원을 그대로 쓸 수 있었습니다.

Windows 전용 기능인 트레이 팝업은 SSE로 받은 주문 정보를 메인 웹뷰가 URL에 담아 넘기고, 네이티브가 그 URL로 팝업 웹뷰를 띄우는 구조입니다. 주문마다 웹뷰를 새로 만들어 흰 화면이 잠깐 보였고, 웹뷰를 미리 띄워 두자 흰 화면은 사라졌지만 메모리를 고정적으로 더 쓰게 됐습니다. 로직을 네이티브로 옮기면 네이티브와 웹뷰 두 곳을 관리하고 바뀔 때마다 앱을 업데이트해야 해서, 웹뷰에 두고 메모리를 줄이는 쪽을 골랐습니다.

  • 렌더러 프로세스 통합: CEF에 process-per-site 옵션 한 줄을 추가해 같은 사이트인 메인·팝업 웹뷰가 렌더러 프로세스를 공유
  • 엔트리 분리: 메인과 같은 번들을 받던 팝업에 전용 엔트리를 만들어 필요한 파일만 로드
  • 쿼리 문자열을 해시로 변경: 아래 표

? 대신 #

방식URL동작
? 쿼리{BASE_URL}/tray-popup?orderNo=...넘길 때마다 페이지를 이동해 새 document를 만듦. 이전 document는 GC 전까지 렌더러 메모리에 남음
# 해시{BASE_URL}/tray-popup#orderNo=...hashchange 이벤트만 발생하고, document 1개를 유지한 채 내용만 교체
덧붙임: #만 바꾸면 새 document가 생기지 않는 이유
  • URL에서 # 뒤(프래그먼트)는 문서 안의 위치를 가리키는 부분이라 서버로 보내지 않습니다. 브라우저는 프래그먼트만 바뀐 이동을 같은 문서 안의 이동으로 처리하고 페이지를 다시 불러오지 않습니다.
  • ? 뒤 쿼리 문자열은 리소스 주소의 일부입니다. 값이 바뀌면 다른 리소스로 보고 새로 요청해 document를 처음부터 만듭니다. 팝업이 뜰 때마다 이 과정이 반복되면 이전 document가 정리될 때까지 메모리가 쌓입니다.
  • 해시 방식에서는 페이지가 hashchange를 직접 받아 화면을 바꿔야 합니다.
window.addEventListener("hashchange", () => {
  const orderNo = new URLSearchParams(location.hash.slice(1)).get("orderNo");
  render(orderNo);
});
  • 프래그먼트만 바꿔도 방문 기록에는 항목이 하나씩 쌓입니다. 기록을 남기지 않으려면 location.replace()로 바꿉니다.
  • 같은 문서 안에서 URL을 바꾸는 방법으로 history.pushState()도 있지만, 페이지 안의 스크립트에서만 부를 수 있습니다. 네이티브 쪽에서 웹뷰 URL을 바꿔 값을 넘기는 구조라면 해시가 더 간단합니다.
지표결과
트레이 팝업을 띄울 때 늘어나는 메모리약 78% 감소
트레이 팝업에 필요한 JavaScript 파일 크기gzip 기준 약 85% 감소
트레이 팝업 내용 갱신 시간약 140ms에서 약 10ms로 93% 단축

공통 인터페이스에 의존하기

컨테이너가 Delphi와 React Native 두 벌이 되면서 위키 문서와 코드의 인터페이스가 어긋나거나 한쪽 기능이 빠지는 휴먼 에러 위험이 생겼습니다. 웹뷰가 인터페이스를 정의해 패키지로 배포하고 두 컨테이너가 그대로 따르게 하자 위키 관리 부담과 기능 불일치가 없어졌습니다.

계층내용
웹뷰인터페이스 정의
브릿지 인터페이스TypeScript zod 스키마에서 Delphi 타입을 코드 생성
React Nativezod 스키마를 가져와 구현
Delphi생성된 타입을 가져와 구현
// @webview/bridge-interface
export const playAudioSchema = z.object({
  source: z.string(),
  isLooping: z.boolean().optional(),
});

// React Native
import { playAudioSchema } from '@webview/bridge-interface';

PLAY_AUDIO: async (data) => {
  const req = playAudioSchema.parse(data);
  await play(req.source, req.isLooping);
},
// Delphi
uses BridgeInterface.Audio;

Req := TPlayAudioRequest.FromJSON(AData);
PlayAlarm(Req.Source, Req.IsLooping);

모바일: React Native 전환과 Expo 공용 큐

Windows가 Delphi로 정해져 컨테이너를 하나로 묶을 수 없게 되자, 모바일은 웹 프런트엔드 개발자가 프레임워크만 새로 배우면 되는 React Native를 골랐습니다. Flutter는 Dart까지 새로 배워야 했고 사내 전문 인력도 없었습니다.

주문 알림이 울리지 않거나 영수증이 늦게 출력되는 간헐적 이슈는 주문을 동시에 여러 건 발생시켜 재현했습니다. 원인은 모든 모듈이 큐 하나를 순서대로 쓰는 Expo의 기본 동작이었고, Expo 55에는 공용 큐 외의 큐가 없어 전용 큐를 네이티브에서 직접 만들었습니다. 이후 알림음과 영수증 출력은 다른 작업 때문에 지연되지 않습니다.

플랫폼전용 큐 구현
Android알림음은 우선순위 높은 스레드, 프린터는 작업을 하나씩 순서대로 처리하는 스레드
iOSasync 함수는 Expo가 공용 큐와 별도로 실행, 출력 순서를 보장해야 하는 프린터만 전용 큐
  • 모바일 전용 웹뷰 화면 약 30개: AI 하네스에 모바일 컴포넌트 구현 스킬을 추가해 하루 만에 개발

macOS: 창을 숨기면 주문이 끊기는 문제

관리 효율을 위해 macOS 컨테이너도 Flutter에서 React Native macOS로 바꿨습니다. Microsoft가 React Native를 fork해 관리하는 라이브러리라 Expo 지원도 아직 완전하지 않고 최신 버전도 다르지만, TypeScript 기준 약 70% 코드(브릿지 스키마, 라우팅 로직)를 공유합니다.

앱 창을 일정 시간 숨기면 웹뷰가 SSE를 받지 못해 주문이 들어오지 않았습니다. 1분마다 창을 잠깐 띄웠다 숨기는 임시 처리는 화면 깜빡임을 일으켰습니다. 앱 프로세스는 화면 꺼짐 방지 설정 덕에 App Nap 대상이 아니었고, 원인은 창이 숨겨지면 일정 시간 뒤 웹 콘텐츠 프로세스를 정지하는 WebKit이었습니다.

WebKit 비공개 API 네 가지를 비교했고, 유일하게 정지가 없던 _setPageVisibilityBasedProcessSuppressionEnabled도 오래 켜 두면 정지 시점만 늦어져 안정성 문제와 함께 적용하지 않았습니다.

비공개 API정지
_setHiddenPageDOMTimerThrottlingEnabled있음
_setHiddenPageDOMTimerThrottlingAutoIncreases있음
_setPageVisibilityBasedProcessSuppressionEnabled없음
_setWindowOcclusionDetectionEnabled있음

최종 해결은 macOS 14부터 쓸 수 있는 공개 API inactiveSchedulingPolicy입니다. 적용 후 창을 숨긴 상태에서도 최대 0.57초 안에 주문을 받으며, macOS 14 미만에서는 쓸 수 없다는 한계가 남습니다.

// 창이 보이지 않아도 웹 콘텐츠 프로세스를 정지하지 않음
if (@available(macOS 14.0, *)) {
  configuration.preferences.inactiveSchedulingPolicy =
    WKInactiveSchedulingPolicyNone;
}

최종 결과와 이후 계획

컨테이너는 Delphi와 React Native 두 종류로, UI와 비즈니스 로직은 웹뷰 한 곳으로 묶였습니다. 대부분의 이슈는 프레임워크보다 스레드·메모리·프로세스 같은 브라우저와 운영체제 동작을 알아야 풀렸고, 네이티브 지식의 간극은 AI가 주문접수 도메인과 기존 코드를 분석할 수 있는 환경을 먼저 갖춰 메웠습니다.

측면변화
코드약 30만 줄에서 5만 줄, 코드베이스 5개에서 1개
개발 기간과제당 며칠에서 평균 하루
사용자 경험·배포모든 기기에서 같은 UX·UI, 앱 업데이트 없이 반영
확장성Android POS 기기용 SDK 신규 프로젝트에서 새 코드 약 1,000줄, 96%는 기존 코드 재사용
  • 배포: 매출에 직결되는 제품이라 11월 18일 Windows부터 Android, iOS 순으로 점진 배포
  • 리드타임: 평균 하루는 개발 구간 기준, 기획·디자인·QA에도 AI 적용 시도
  • E2E 고도화: 브라우저·목 데이터 검증에서 에뮬레이터·실제 API 호출로 전환, 실기기 테스트 일부 자동화, 전 플랫폼 기기를 한 화면에 띄우는 도구 개발 중