더스윙 기술 블로그 최신 글
theswing.tech에서 발행한 기술 글을 테크곳간이 수집해 최신순으로 보여드립니다.
최신 글
작은 수정이 전체를 깨뜨리기 시작했을 때: 옐로우버스 DB 재설계 이야기
안녕하세요! 더스윙에서 백엔드 개발을 담당하고 있는 명수찬입니다. 옐로우버스의 학원 셔틀버스 운행 시스템은 수년간 기능을 확장하고 고객 규모가 커지면서 빠르게 성장해 온 서비스입니다. 실제로 서비스 초기였던 2020년에는 연간 약 2만 4천 회 수준이던 운행량이, 2023년에는 약 21만 회, 그리고 2025년에는 약 70만 회 수준까지 증가하며 짧은 기간 동안 30배 이상 성장을 기록했습니다. 그러나 서비스의 복잡도와 규모가 증가할수록, 초기 단계에서 구축된 데이터베이스 구조는 그 성장을 충분히 뒷받침하지 못하고 있었습니다. 특히 운행일정과 탑승일정처럼 파이프라인이 촘촘하게 얽힌 도메인에서 문제는 더욱 두드러졌습니다. 우리는 어느 순간부터 작은 수정 하나가 시스템 전체로 파급되는 나비 효과를 겪고 있었습니다. 이 구조 위에서 기능을 더하고 당장의 문제를 해결하는 것은 가능했지만, 지속 가능한 형태는 아니었습니다. 데이터 계층은 애플리케이션 코드보다 훨씬 변경 비용과 리스크가 크며, 잘못된 수정은 곧바로 운영 장애나 데이터 불일치로 이어질 수 있습니다. 그럼에도 불구하고, 옐로우버스의 일정 시스템은 더는 ‘땜질식 개선’으로 유지할 수 있는 수준이 아니었습니다. 결국 우리는 시스템을 근본부터 다시 설계하는 것만이 지속 가능한 선택이라고 판단했습니다. 이 글에서는 우리가 겪었던 구조적 문제와 이를 해결하기 위해 도입한 설계 전략, 그리고 그 결과를 공유하고자 합니다. 재설계를 불가피하게 만든 구조적 문제들 문제를 해결하기 위해선 먼저 현상을 정확히 진단해야 했습니다. 옐로우버스 일정 시스템은 크게 두 가지 축으로 구성됩니다. 버스가 언제, 어디를 운행하는가 — 운행일정 학생이 언제, 어디서 타고 내리는가 — 탑승일정 두 축은 서로 밀접하게 연결되어 있고, 실제 운영에서는 항상 함께 움직입니다. 문제는 이 두 영역이 시간이 지나면서 변경에 극도로 취약한 구조로 굳어졌다는 점이었습니다. 아래에서는 운행일정과 탑승일정 각각에서 어떤 문제가 있었는지, 그리고 두 도메인이 어떻게 서로의 변경을 증폭시키고 있었는지 순서대로 살펴보겠습니다. 1. 끝없이 파편화되는 운행일정 운행일정은 특정 기간 동안 반복적으로 운행되는 노선, 기사, 동승매니저 정보 등을 포함하는 데이터입니다. 운행일정은 통상 긴 기간으로 생성되지만, 운영 현실에서는 빈번한 수정이 발생합니다. 가령 "1년 치 일정 중 특정 한 달(anchor period)만 경로를 변경한다"는 단순한 요구사항이 들어오면, 기존 시스템은 이를 다음과 같이 운행일정을 물리적으로 분할했습니다. Anchor Period 이전의 운행일정 Anchor Period 기간 동안의 운행일정 Anchor Period 이후의 운행일정 하나였던 운행일정이 세 개의 별도 운행일정으로 분리되는 것입니다. 세 개로 분리된 운행일정 이 패턴이 반복되면 어떤 일이 벌어질까요? 운영 과정에서 하루 단위로라도 수정이 발생할 때마다 해당 기간이 물리적으로 세 개로 분리되며 새로운 운행일정이 추가됩니다. 결과적으로 운행일정은 시간이 지날수록 끝없이 잘게 쪼개지고, 원래의 흐름을 추적하기 어려운 조각난 데이터 집합으로 변하게 됩니다. 2. 기하급수적으로 증가하는 탑승일정 인스턴스 학생의 탑승일정은 두 가지 레이어로 구분할 수 있습니다. 반복되는 일정의 패턴 (화-목 / A 정류장 / 학생 1 / 3월~6월) 패턴이 실현된 실제 인스턴스 (3월 2일 화요일 / A 정류장 / 학생 1) 패턴은 일정이 반복되는 구조를 정의하고, 인스턴스는 해당 패턴이 특정 날짜에 적용될 때 파생될 수 있는 실제 일정입니다. 기존의 구조는 탑승일정의 패턴이 생성되는 순간, 해당 기간 동안 발생 가능한 모든 날짜의 인스턴스를 즉시 물리적으로 생성(eager generation)하였습니다. 이 방식은 치명적인 비효율을 낳았습니다. 학생 10명이 1년 치 일정을 등록하면 약 2,600개의 인스턴스가 즉시 생성됩니다. 패턴을 수정해야 할 경우, 이미 생성된 수천 개의 인스턴스를 검색 → 삭제 → 재계산 → 재생성해야 했습니다. 이후 탑승일정의 패턴에
2026년 1월 30일
CSR 환경에서 Dynamic OpenGraph 구현하기
개발팀에서 CloudFront Function을 활용하여 CSR 웹 애플리케이션에서 동적 OpenGraph 메타 태그를 제공하는 방법을 소개합니다.
2025년 12월 3일
스윙 고객센터, 어떻게 응답 시간을 71% 단축했나? - 2부: Synapse AI 도입
안녕하세요! 더스윙에서 개발총괄을 맡고 있는 허승균입니다. 이번 포스팅은 스윙 고객센터를 어떻게 개선하여 최고의 효율성을 얻게 되었는지 지난 1부에 이어서 2부를 소개해드리겠습니다. Series note. Synapse AI는 스윙의 사내 AI 플랫폼의 이름입니다. 이번 글의 주인공은 그 중 고객문의를 전담하는 Synapse Concierge이며, 이후에 시리즈로 각 제품군에 대해서 연재할 예정입니다 시작하며 1부에서 우리는 실시간 채팅 → 케이스(게시판)로 전환하며 FRT(첫 응답 시간)와 TTR(해결까지 걸리는 시간)을 함께 줄이는 구조를 만들었습니다. 그런데 게시판 전환으로 상담의 ‘허리’는 단단해졌지만, 운영을 이어가면서 이전에는 보이지 않던 새로운 문제가 드러나기 시작했습니다. 고객이 맥락을 한 번에 정리해 올릴 수 있는 바로 그 게시판 구조 때문에 케이스가 쌓일수록 새롭게 더 명확해진 병목들이 발견되었습니다. 문제: 게시판 전환 이후 드러난 세 가지 병목 ① 장문 케이스의 ‘초기 해석’ 병목 핵심 의도가 여러 문단에 흩어지는 경우가 많다 (이해를 돕기 위해 재구성한 내용입니다) 게시판은 고객이 하고 싶은 말을 한 번에 길게 적을 수 있는 환경을 제공합니다. 그만큼 맥락은 풍부해졌지만, 상담사에게는 새로운 부담이 생겼습니다. 핵심 이슈가 여러 문단에 흩어지고 고객마다 서술 방식이 달라 맥락 파악 난이도가 크게 달라지며 언제·어디서·어떤 기기·어떤 결제 흐름인지 다시 확인하는 데 시간이 많이 소요되었습니다. 이 과정은 상담사 개인의 독해력과 맥락 추론 능력에 크게 의존했습니다. 같은 케이스라도 상담사에 따라 해석이 달라지는 상황이 반복되었습니다. 풍부해진 맥락은 장점이었지만, 동시에 해석 비용과 편차를 높이는 요인이 되었습니다. ② 상담사별 응대 품질 편차의 ‘가시화’ 케이스 기반 게시판 구조는 상담사가 작성하는 한 문장 한 문장의 차이를 그대로 드러냈습니다. 같은 유형의 고객 문의에 각기 다른 답변 스타일 (첫 인사말에서 이미 다 다르다...) 템플릿을 제공하더라도 상담사마다 다음과 같은 차이가 나타났습니다. 문제를 해석하는 방식 근거를 첨부하는 깊이와 표현 강조하는 정보 어조·형식·맞춤법 등 세세한 표현 방식 이러한 작은 차이들이 쌓여 고객이 경험하는 품질이 상담사마다 달라지는 문제가 발생했습니다. 게시판 전환은 이 편차를 오히려 더 또렷하게 보이게 했고, 지속 가능한 품질 관리를 위해 반드시 해결해야 할 핵심 과제로 자리 잡았습니다. ③ 데이터는 쌓이기 시작했지만, 이제는 연결의 문제였다 게시판 전환은 동시에 예상치 못한 기반도 만들어주었습니다. 케이스가 문서처럼 축적되며 유사 사례를 분류할 수 있게 되었고 고객 요구가 세분화되며 내부 지침서를 고도화할 여지가 생겼으며 정책·FAQ·유사 스레드를 연결한 지식 구조를 구축할 수 있는 환경이 갖춰졌습니다. 품질을 표준화할 준비물은 모두 마련된 셈입니다. 그러나 문제는 실행이었습니다. 정책은 계속 업데이트되고, 유사 사례는 매일 새로 생기며, 상황 조합은 끊임없이 늘어납니다. 이 모든 흐름을 사람이 기억하고, 해석하고, 매 케이스마다 일관되게 적용하는 것은 사실상 불가능했습니다. 따라서 정책 연결 → 판단 흐름 정리 → 표준 문안 적용 이 과정을 ‘사람’이 아닌 시스템 레이어가 도와주는 구조가 필요했습니다. 해결: 스레드 위에 얹은 Synapse AI 3단 레이어 앞서의 세 가지 병목은 결국 하나의 질문으로 모였습니다. “풍부한 맥락과 쌓인 지식을, 어떻게 매 케이스에 일관되게 적용할 수 있을까?” 게시판 전환으로 기반은 갖춰졌지만, 사람이 직접 읽고, 찾고, 정리하는 방식으로는 더 이상 스케일을 감당하기 어려웠습니다. Synapse AI (Concierge) - Architecture 이에 케이스 처리 흐름 위에 Synapse AI는 Databricks 기반 Data Lake에 축적된 고객 행동·운행·결제·IoT 로그·정책 문서 등의 맥락을 결합하고, 이를 LangChain 기반 Agent–LLM과 자동으로 연결함으로써 케이스마다 일관된 정보·근거·초안을
2025년 11월 18일
스윙 iOS에서 더 개선된 애니메이션 시스템 만들기
들어가며 안녕하세요. 더스윙에서 iOS 팀장 및 스윙 앱의 iOS 개발을 담당하고 있는 박영수입니다. 앱을 개발하다 보면 다양한 애니메이션 요구사항을 마주하게 되는데요, 애니메이션을 사용하다 보면 의도치 않은 동작을 겪는 경우가 많습니다. 이 글에서는 더스윙에서 애니메이션 관련 이슈를 어떻게 해결했는지 소개하려고 합니다. iOS 개발에서 애니메이션은 필수적인 요소입니다. 최근 앱들은 요소들을 상황에 맞춰 이동시키거나 숨기거나 변형시켜야 할 때가 많습니다. 이럴 때 일반적으로 UIView.animate 함수나 UIViewPropertyAnimator를 사용하게 되는데요, 예상치 못한 문제가 발생하기 마련입니다. 기본적으로 다음과 같은 뷰들이 배치되어 있다고 가정해보겠습니다. menuButton.frame = CGRect(x: 50, y: 250, width: 50, height: 50) menuButton.setTitle("☰", for: .normal) menuButton.setTitleColor(.black, for: .normal) menuButton.backgroundColor = .lightGray menuButton.addTarget(self, action: #selector(menuTapped2), for: .touchUpInside) view.addSubview(menuButton) redBox.backgroundColor = .red redBox.frame = CGRect(x: 50, y: 100, width: 100, height: 100) view.addSubview(redBox) testLabel.frame = CGRect(x: 50, y: 250, width: 300, height: 300) testLabel.text = "Label" testLabel.textColor = .black view.addSubview(testLabel) profileImageView.backgroundColor = .blue profileImageView.layer.cornerRadius = 20 view.addSubview(profileImageView) 여기서 메뉴 버튼을 누르면 화면 배치를 바꾸고, 뷰의 색상과 텍스트 컬러도 변경하려고 합니다. override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() profileImageView.frame = CGRect( x: isOpen ? view.bounds.width - 60 : 60, y: 50, width: 40, height: 40 ) } @objc func menuTapped() { isOpen = !isOpen UIView.animate(withDuration: 2.0) { self.redBox.frame.size.width = (self.isOpen) ? 300 : 100 self.redBox.backgroundColor = (self.isOpen) ? .yellow : .red self.testLabel.textColor = (self.isOpen) ? .blue : .black self.view.layoutIfNeeded() } } 예시로 든 코드는 극단적이긴 합니다. 일반적으로 프레임을 다시 잡을 때 viewDidLayoutSubviews를 직접 활용하지는 않습니다. 하지만 현재 화면의 bounds 등을 참조해야 할 때나 AutoLayout을 사용한다면 자주 활용되는 방식입니다. 위의 코드를 실행하면 다음과 같은 결과가 나옵니다. 의도치 않게 파란색 뷰가 애니메이션되며, UILabel의 textColor는 점진적으로 바뀌지 않고 순간적으로 변경됩니다. 위의 예시에서는 UIView.animate 함수를 사용했지만, UIViewPropertyAnimator를 사용해도 동일한 결과가 나타납니다. 무엇이 문제인가? CATransaction과 암묵적 애니메이션 이 문제에 대해서 이해하기 위해서는 먼저 UIView.animate 함수의 구동방식에 대해 이해하여야 됩니다. UIView.anim
2025년 11월 5일
R-tree를 사용한 가까운 마커 찾기 기능 구현과 개선기
안녕하세요! 더스윙에서 프론트엔드 개발을 담당하고 있는 정주형이라고 합니다. 모빌리티 서비스 기업인 더스윙에서는 지도 관련 도메인을 다룰 일이 많습니다. 이번 포스팅에서는 지도와 관련된 요구사항을 구현한 내용과 그 과정에서 마주한 문제, 그리고 어떻게 개선할 수 있었는지에 대해 소개해 드리고자 합니다. 마커를 움직일때마다 500m 이내의 가장 가까운 마커가 커지게 해주세요! 다음과 같은 요구사항이 들어왔습니다. 지도에 많은 마커들(택시, 정류장 등등)이 존재하는 상황에서 사용자가 마커를 움직일때 최근접한 마커를 찾아 크기를 키워야 합니다. 복잡해 보이는 작업이니 먼저 요구사항을 작은 단위로 하나씩 분해해보도록 하겠습니다. 해당 요구사항들은 다음과 같은 작은 기능 몇가지를 필요로 합니다. 마커를 움직일 수 있어야 함. 500m 내의 마커들을 찾을 수 있어야 함. 움직이는 마커와 가장 가까운 마커들을 찾을 수 있어야 함 가장 가까운 마커의 ui를 업데이트 할 수 있어야 함. 좀 더 개발 친화적으로 정의한다면 마커를 움직이는 이벤트마다 콜백 함수를 호출, 해당 함수 내에서 현재 마커와 다른 마커들간의 거리를 비교하고, 조건에 맞다면 마커를 다시 렌더링 시켜줘야 합니다. 구현하기 뭘 구현해야 할지 정리했으니, 이제 실제로 구현을 해볼 차례입니다. 첫번째 작업인 마커를 움직일때마다 이벤트를 받을 수 있도록 해 봅시다. 구현의 경우 react와 카카오맵 sdk를 기반으로 하였습니다. 마커를 움직이려면 맵에서 특정 element를 drag 할때마다 이벤트를 쏘고, 해당 이벤트를 감지해서 우리가 만들 콜백 함수를 호출해야 합니다. 일견 복잡해보이지만 다행히 대부분의 map sdk들은 마커 드래그 이벤트와 리스너를 지원합니다. // 도착 마커에 dragstart 이벤트를 등록합니다 kakao.maps.event.addListener(arriveMarker, 'dragstart', function() { // 도착 마커의 드래그가 시작될 때 마커 이미지를 변경합니다 arriveMarker.setImage(arriveDragImage); }); 다음으로 움직이는 마커의 500m 이내 마커를 찾아봅시다. 아쉽게도 특정 마커의 반경 안의 마커를 찾아주는 메소드는 없으니, 직접 구현해야 합니다. 드래그 하고 있는 마커와 다른 마커들간의 직선거리를 구해서 500m 내인지 판별하면 가장 가까운 마커를 찾을 수 있을 것입니다. 마커간의 거리를 구하는 방법은 몇가지가 있습니다. 가장 간단한 방법은 두 좌표 사이에 polyline을 그리는 것입니다. 보이지 않는 가상의 선을 그어서 해당 선의 길이가 바로 마커간의 거리가 되겠죠. const calculateDistance = ( pos1: kakao.maps.LatLng, pos2: kakao.maps.LatLng, ): number => { const polyline = new kakao.maps.Polyline({ path: [pos1, pos2], }) return polyline.getLength() ... const distance = calculateDistance(currentPosition, candidateLatLng) // 500m 체크 if (distance > MAX_DISTANCE_METERS) { updateNearbyMarker(null) return } 이렇게 빠르고 간단하게 구할 수 있습니다. 그러나 해당 접근 방식은 문제가 있습니다. 폴리라인으로 지도상에서 확인할 수 있는 두 좌표 사이의 거리가 실제로 500m가 아니라는 점입니다. 곡률을 고려하지 않았기 때문입니다. 지구는 구형이지만 맵은 평면이므로 두 점 사이의 거리는 곡률을 고려하지 않는다면 오차가 발생하게 됩니다. (다만 sdk마다 내부 구현 방식이 상이해, 이미 polyline의 길이가곡률이 고려되는 경우도 있습니다) 따라서 이를 보정하기 위해 구체 표면에서 거리를 계산하는 하버사인 공식을 사용할 수 있습니다. 직접 구현할 필요 없이 JS로 만들어놓은 함수를 사용해도 되고, turf js처럼 지리공간 데이터를
2025년 10월 27일
스윙 고객센터, 어떻게 응답 시간을 71% 단축했나? - 1부: 게시판으로 전환
안녕하세요! 더스윙에서 개발총괄을 맡고 있는 허승균이라고 합니다. 이번 포스팅은 스윙 고객센터를 어떻게 개선하여 최고의 효율성을 얻게 되었는지 총 2부로 나누어 소개해드리겠습니다. 시작하며 스윙은 오랫동안 실시간 채팅으로 고객을 만났습니다. 서비스 초창기, “지금 바로 묻고, 바로 답한다”는 경험은 분명 강력했고 성장의 핵심 동력이었습니다. 하지만 서비스가 폭발적으로 성장하며 하루 수십만 건의 라이드에서 문의가 쏟아지자, 실시간 응대를 위한 여러 장치가 오히려 상담 효율을 떨어뜨리는 ‘확장 마찰’을 일으키기 시작했습니다. 여러 솔루션을 도입하고 운영 프로세스를 개선하려 했지만, 구조적 한계는 여전했습니다. 우리는 '빠른 응대'가 아닌 '완결성 있는 해결'이라는 본질로 돌아가 고객상담 운영 모델 자체를 재설계하기로 했습니다. 문제: 과연 채팅이 맞는가? ① "무엇을 묻는지"부터 물어야 한다 고객의 요구사항을 파악하는데 24분이 소요되었다 채팅은 고객이 문제를 명확히 정리하기 전에 대화가 시작됩니다. 상담사가 핵심 정보를 확보하려면 초반 여러 차례의 질의응답, 이른바 '초반 핑퐁'이 필수적이고, 이 과정에서 첫 응답 시간(FRT)과 총 해결 시간(TTR)이 함께 증가합니다. 초반 핑퐁이 만든 FRT/TTR 상승 구조 복붙, 매우 번거롭고 잘못 입력할 가능성이 높다 이때 정보 누락·오입력이 발생하면 재확인과 재질문이 반복됩니다. 특히 서드파티 환경에서는 내부 라이드/결제 이력이 자동 연동되지 않아 고객이 앱에서 값을 찾아 직접 입력해야 하는데, 이 역시 지연과 오류를 키웁니다. ② '빨리'의 함정: 첫 답에 집착하다 끝이 멀어진다 3분 만에 물음표 10개와 느낌표 9개를 받았다 채팅창은 고객에게 “곧바로 답이 올 것”이라는 기대를 만듭니다. 그러나 실제로 결론을 확정하려면 데이터 대조와 정책 검토에 시간이 필요합니다. 규모가 커지고 문의가 몰리면 “바로 답이 없다”는 불만이 쌓이고, 이를 낮추려 첫 응답 시간(FRT)만 무리하게 줄이는 데 집중하게 됩니다. 첫 응답 시간(FRT) 최적화의 착시와 총 해결 시간(TTR) 악화 메커니즘 그 결과 상담은 속도 우선으로 기울어 정확성과 일관성이 떨어지고, 문의가 완전히 해결되기까지 걸리는 시간(TTR)은 오히려 늘어나는 모순이 발생합니다. 속도 착시가 총 소요를 악화시키는 전형적 패턴입니다. ③ 쌓이지만 남지 않는다: 측정도, 학습도 어렵다 채팅 로그는 대표적인 비정형 데이터로, 명확한 자산으로 남기는데 비용이 발생합니다. 우선, 성과 측정이 모호해집니다. 실시간 대화의 흐름에서 '고객의 진짜 의도를 파악한 시점'이나 '정확한 해결을 제시한 순간' 을 칼 같이 잘라내기 불가능합니다. 여기에 고객이나 상담사의 응답 지연 같은 수많은 변수가 더해지며, 결국 우리는 안개 속에서 어렴풋한 감으로 성과를 판단하게 됩니다. 비정형 데이터가 만드는 측정·자동화의 한계 대화를 AI로 요약하는 것은 난이도가 높다 더 큰 문제는 자동화의 한계입니다. 이처럼 변동성 크고 비정형적인 대화 데이터는 AI가 학습하기에 매우 비싼 재료입니다. 대화의 맥락이 수시로 바뀌고 주제가 중첨되는 특성 때문에, 요약·추천·답변 생성 같은 자동화의 비용과 오차율은 높습니다. 결국 데이터는 쌓일수록 운영의 신뢰도를 떨어뜨리고, 미래를 위한 학습 자산이 되지 못한 채 그저 흘러가 버립니다. 해결: ‘실시간 대화’에서 ‘완결된 케이스’로 확장성 문제를 풀고 품질·일관성을 높이기 위해, 우리는 운영 모델을 '대화' 에서 '케이스(Case)' 로 전환했습니다. 이를 위해 자체 문의 게시판을 개발하고 기존 운영 어드민과 완벽하게 통합하여, 모든 상담이 한 곳에서 이루어지는 단일 어드민 콘솔을 완성했습니다. 핑퐁에서 한 방으로: 문의의 정형화 우리는 불필요한 '탐색전'을 없앴습니다. 고객이 문의를 남기는 시점에 필요한 정보를 '한 번에' 모두 제공하도록 문의 플로우를 개선 했습니다. 내 문의함에서 내역 확인이 가능하다 문의는 글/댓글(스레드) 형태로 진행되어, 시작부터 끝까지 모든 맥락을 한눈에 파악할 수 있습니다. 고객은 한
2025년 10월 20일
pnpm을 사용한 모노레포로 프론트엔드 생산성 늘리기
안녕하세요! 더스윙에서 프론트엔드 개발을 담당하고 있는 정주형이라고 합니다. 이번 포스팅에서는 개발팀에서 모노레포를 도입한 배경과 과정, 그리고 어떤 것이 달라졌는지에 대해서 소개해 드리고자 합니다. The Cost of Interrupted Work 어바인에 위치한 캘리포니아 대학의 교수 Gloria Mark는 48명의 대학생을 대상으로 한 연구를 진행했습니다. 연구의 내용은 다음과 같았습니다. 업무에서의 맥락 전환(Context switching)이 실제로 어떤 영향을 끼칠까? 실험의 결과는 다음과 같았습니다. 만약 맥락 전환으로 인해 업무가 방해받으면 다시 본래의 일로 돌아가는데 평균 23분 15초가 걸리게 됩니다. 그리고 본래 업무로의 복귀는 생산성을 떨어트리며 높은 스트레스를 수반합니다. 에어팟을 끼는 이유.jpg 여러분이 개발자라고 가정해보겠습니다. 오늘 비슷하지만 다른 3가지의 작업을 할당받았습니다. 세가지의 작업을 하루에 끝낸다고 가정했을때, 여러분은 세번의 맥락 전환을 겪게 됩니다. 근무시간 8시간 중 3번의 전환을 겪었으니, 23*3 69분만큼의 시간을 손해를 보게 되었습니다. 앗, 해당 시간은 단순히 다시 집중하는데 걸리는 시간이고, 환경을 설정하거나 히스토리를 파악하는 시간은 포함하지도 않았습니다. 실제로는 더 많은 시간을 손해보고 있는 셈입니다. 개발자의 경우 평균적으로 주당 4번의 맥락 전환을 경험한다고 합니다. 맥락 전환 비용으로 주당 1~2시간을, 월에는 4~8시간을 내고 있는 셈입니다. 끔찍하죠? 더스윙의 프론트엔드팀도 맥락 전환에 따른 문제를 겪고 있었습니다. 지금까지 7년이 넘는 시간동안 8개가 넘어가는 모빌리티 서비스를 운영하면서 지오펜스, 인사, 계약 등등 백오피스 앱들이 자연스럽게 많아지게 되었죠. 세어보니 20여개의 백오피스 어드민이 존재하고 있는 상황이었습니다. 모든 앱이 액티브하지는 않았지만 계속해서 수정사항이나 디버깅이 필요했습니다. 각 팀원들은 A 레포에서 작업을 하다가, B 레포의 PR을 체크하고, C 레포의 변경사항을 pull 받고.. 계속해서 이를 반복하는 식이었습니다. 크지 않은 작업들이 주어진다 하더라도 금새 피곤함을 느낄 수 밖에 없었죠. 서비스가 커지고 늘어날수록 컨텍스트가 많아지고, 이에 비례해서 맥락 전환 비용은 커지고, 생산성은 낮아지는 좋지 못한 구조였습니다. 프론트엔드 팀이 실제로 겪고 있었던 문제들을 정리하자면 다음과 같았습니다. 잦은 맥락 전환에 따른 생산성 저하 다른 패키지의 변경사항을 파악하기 힘듬 DRY하지 못한 코드의 증가 미묘하게 다른 일관되지 못한 DX 점점 증가하는 인프라 관련 관리 포인트 비단 맥락 전환만이 문제가 아니였죠. 서비스의 크기가 커져가면서 점점 다른 문제들도 생기기 시작하고 있었습니다. 결국 이러한 문제를 해결하기 위해 프론트엔드 팀에서는 모노레포를 도입하여 기존 어드민들을 마이그레이션 하기로 결정하였습니다. 왜 모노레포인가? 맥락 전환에 대한 이야기로 잠깐 돌아가 보겠습니다. 맥락 전환 비용이 문제라면, 사실 해결책은 간단합니다. 맥락을 없애버리는 것이죠. 하나의 소스코드 위에서(monolith) 개발한다면 위 단점들을 한큐에 해결할 수 있습니다. 전환할 맥락 자체가 없고, 관리 포인트조차 하나죠. 그러나 이러한 구조는 유연성이 부족합니다. 관심사가 분리되지 않고, 배포, 리팩토링 등이 어려워지죠. 거대한 프로그램에서 한 부분만 수정했는데 모두 배포할 수는 없으니까요. 그렇다고 해서 여러개의 모듈로 구성된 멀티레포로 돌아가자면, 위에 서술한 문제가 다시 생길 것이 뻔합니다. 그렇다면 모노리스의 장점과, 모듈로 개발하는 장점만 쏙쏙 뺄 수는 없을까요? 관심사를 분리해서 각 모듈들을 자율적으로 개발할 수 있으면서, 맥락 전환과 관리 포인트들은 줄이는 방법이요. 네, 모노레포가 이러한 문제를 해결할 수 있습니다. A monorepo is a single repository containing multiple distinct projects, with well-defined relationships 모노레포란 여러개의
2025년 1월 8일
SWING이 피크타임 택시 배차성공률을 2배 높인 전략
안녕하세요. SWING Product 팀 최우혁 CPO입니다. 3개월전부터 SWING이 택시업계에 본격적으로 진출하면서 다양한 문제들을 해결해 나가고 있는데요. 그 중에서 최근에 피크타임 문제를 해결한 사례를 소개하려고 합니다. 피크타임은 단순히 '택시 안 잡히는 시간'을 넘어, 택시 업계에서 가장 중요한 순간입니다. 이 시간 동안 얼마나 효율적으로 배차를 성공시키느냐에 따라 고객들의 플랫폼에 대한 신뢰도와 충성도가 결정되기 때문이죠. 이러한 상황에서 저희는 크게 두 가지 문제를 해결해야 했습니다. 문제정의 피크타임 내 공급에 비해 폭발적인 수요 증가로, 고객과 기사를 매칭하기가 어렵습니다. 피크타임 동안 택시를 원하는 고객 수는 기하급수적으로 증가하지만, 이를 감당할 만큼의 기사가 충분하지 않았습니다. 이는 배차 실패로 이어지고, 고객의 불만족이 커지면서 플랫폼에 대한 신뢰도가 떨어지는 악순환을 초래했습니다. 이는 결과적으로 고객의 이탈을 가져오고 콜이 적어지면 플랫폼에서 기사님들이 이탈하게 되고, 이는 또다시 배차 실패를 가져오는 악순환을 낳게 됩니다. 특히, 특정 지역에서의 수요 급증은 해당 지역의 배차 실패율을 평균 대비 2~3배 높이는 결과를 가져오기도 했습니다. 쏟아지는 콜 속에서 택시 기사님들의 앱 선택 경쟁에서 우위를 점하기 어렵습니다. 그때 당시에는, 스윙택시 기사용 앱을 사용하는 기사는 서울에서 법인과 개인을 전체 기사 중 약 15%에 불과했습니다. 대부분의 기사는 콜이 많을때에는 오랜시간 써왔던 타 택시기사 앱(예: 카카오T, 우버)을 선호했으며, 이는 SWING 택시 기사용 앱이 기사들에게 타 경쟁앱 대신 선택할 만큼 충분한 매력도를 제공하지 못했음을 의미했습니다. 해결책: 데이터와 인센티브를 활용한 접근 두 가지 문제를 해결하기 위해 데이터 기반 분석과 게이미피케이션 요소를 결합한 보상 체계를 설계했습니다. 요금 상승 시스템 콜을 잡으면 추가로 제공하는 보너스 요금이 점진적으로 상승하는 구조를 설계해 기사들에게 경제적 인센티브를 제공했습니다. 기사는 실시간으로 상승하는 요금을 보며 배차를 수락할 최적의 순간을 판단하게 되었고, 이는 "눈치 게임"과 같은 긴장감 있는 환경을 조성했습니다. 발맞추어, 기사앱 인터페이스에 상승된 요금이 실시간으로 게임처럼 표시되도록 개선했습니다. 피크타임 보너스 요금 최적화 보너스 요금의 시작 금액, 상승 간격, 지속 시간을 정밀 조정했습니다. 피크타임에는 더 높은 시작 금액으로, 짧은 간격으로 빠르게 금액이 오르는 방식으로, 피크타임 콜 수락률을 높였습니다. H3 기반 지역과 시계열 데이터로 피크타임 정의 특정 시간대에 배차 성공률이 낮았던 지역에 대해서 H3기반으로 피크타임을 정의했습니다. 단, 특정 지역에서 최근 배차 실패율이 높은 경우, 기존에 피크타임으로 판단하지 않았던 경우에도 10분 이내에 피크타임 보너스 요금이 적용될수 있게 했습니다. 갑자기 소나기가 내리거나, 고척돔에서 방탄소년단이 콘서트를 하거나 하는 것 처럼 갑자기 수요가 오르는 경우를 고려한 접근이었습니다. 성과 측정 전략의 효과는 두 가지 주요 지표를 통해 확인했습니다: 배차 성공률 피크타임의 배차 성공률이 기존 대비 두배 상승했습니다. 이는 업계에서 피크타임이 아닐때 배차 성공률에 근사한 수치였습니다. 기사 취소율 배차 후 기사 취소율은 기존대비 절반으로 줄었습니다. 그만큼 콜 자체에 대한 만족도가 올라갔다고 볼 수 있고, 고객에게 치명적인 경험인 기사 취소를 줄이는 큰 성과를 가져왔습니다. +) 추가적으로, 배차 실패 후 재호출한 경우의 재배차 성공률이 특히 가파른 증가를 보였습니다. 피크타임에 대한 로직이 잘 작동하고있다는 보조지표로 판단했습니다. 해결 과정에서의 도전 새로운 전략이 처음부터 순조로웠던 것은 아닙니다. 처음에는 기사님들로부터 아래와 같은 불만이 제기되었습니다. “고정 보너스 체계보다 시작 보너스 금액이 적어 불만족스러워요.” “돈 떨어져서 돈 아끼려고 보너스 금액 줄인거 아니에요?” 그러나 시간이 지나면서 긍정적인 변화가 나타났습니다 기사들은 상승하는 보상 체계의
2025년 1월 7일