저는 2025년 6월까지 Play라는 Text-To-Speech 서비스의 프론트엔드를 개발하고 있었습니다. Shift 라는 Real-Time Voice Conversion(RVC) 서비스를 담당하시던 프론트엔드 개발자분께서 퇴사를 하시게 되었어요. 그래서 누가 Shift 서비스를 개발할 것인가 고민이 있었다. 동료 개발자분과 미팅을 잡으며 누가 할 것인지 깊게 이야기를 나눴었는데요, 제가 새로운 도메인과 새로운 동료분들과 일해보고 싶어서 하겠다고 그랬습니다.
웹뷰 개발은 처음이었는데, 첫 직장에서 했었던 임베디드 개발과 유사한 측면이 있었다. 웹뷰가 웹과 비교해서 어떤 특징들이 있었는지 이야기해볼게요.

목차
- 웹뷰 전환과 아키텍처
- 웹뷰의 특징
- 호환성 이슈 해결

Shift 1.0 : C++ Native UI
Shift 2.0 : Hybrid App (Native + WebView Architecture)
Shift는 기존에 C++ 기반의 Native로 구동되는 서비스였습니다. 2024년 11월에 출시되었어요. Native 기반으로 UI를 개발하는 데에 불편함이 있어 2025년 7월에 Native와 Webview로 동작하는 Hybrid App으로 출시하게 되었습니다.
좌측에 있는 보이스 캐릭터를 선택하고, 마이크에 말을 하면 제 목소리를 해당 캐릭터로 바꿔주는 재밌는 앱입니다.
사용 예시는 아래 링크를 확인하세요.
https://youtu.be/xCDAjpZJWYw?si=Jl_CKonRFhZqIpKP
백그라운드 용어 설명
먼저 앱 내에서 사용하는 용어들을 알아야 이해할 수 있어 설명드리겠습니다.
- 강제 업데이트: 강제로 업데이트해야 하는 버전이 있을 경우 앱 사용이 불가능하고 강제 업데이트 팝업이 뜹니다.
강제로 업데이트하는 것은 불편하기 때문에 최대한 지양하고 있습니다.
- 선택 업데이트: 업데이트 가능한 버전이 있는데 필수가 아닐 경우, 업데이트를 하지 않고 사용이 가능합니다.
- Native, JUCE: 사용자의 컴퓨터에서 직접 실행되는 프로그램.
C++로 작성되어 OS와 하드웨어에 직접 접근할 수 있습니다.
마이크 입력, 스피커 출력, 파일 저장, 윈도우 창 제어 등 브라우저에서는 할 수 없는 일을 담당합니다.
JUCE는 오디오 애플리케이션에 특화된 C++ 프레임워크입니다.
- Webview: 네이티브 앱 안에 내장된 브라우저.
일반 Chrome, Safari 같은 브라우저와 동일한 렌더링 엔진이지만, 주소창이나 탭 없이 앱의 일부로 동작합니다.
HTML/CSS/JS로 UI를 그리고, 사용자 인터랙션을 처리합니다.
이 프로젝트에서는 Next.js(React) 앱이 이 WebView 안에서 실행됩니다.
웹뷰 전환과 아키텍처
C++ 기반의 Native였던 Shift 1.0을
Native + Webview의 Hybrdie App으로 전환하게 된 이유를 설명드리겠습니다.
1. 개발 속도 & 생산성 향상 (UI 변경이 빠르다)

제가 좋아하는 개발자 만화 "경경수의 개발만화"에 나오는 장면입니다. (인스타툰으로 보고 있었는데 이제 네이버 웹툰에도 연재하세요..! 재밌습니다 ㅎㅎ)
종이컵 탑을 1px만 옮겨달라고 놀리고 있습니다. 종이컵 탑을 옮기려면 아래부터 다시 지어야 하기 때문에 쉽지 않은 일이죠. 이처럼 프론트엔드 UI는 1px만 옮기는 것도 힘들 수 있습니다. 하물며 C++ 기반으로 돌아가던 기존 Shift 앱의 UI를 변경하는 것은 얼마나 어려웠을까요.

요청: "VoiceChecker, HotkeyButton, VoiceRecorder 사이 간격이 너무 좁아요. 간격 늘려주세요"

C++ 기반의 코드에서는 UI를 변경하는 게 매우 어렵습니다. 위치를 수동으로 계산해야 하고, 부모 혹은 다른 컴포넌트도 신경 써야 합니다. 한 땀 한 땀 직접 그려야 한다고 생각하시면 됩니다.

반면 Web 코드로는 쉽습니다. flex의 gap을 바꿔주기만 하면 되죠.
2. 자바스크립트 생태계의 다양한 UI 라이브러리들을 사용할 수 있습니다.
Tailwind, Radix, Framer Motion, Shadcn UI 등의 검증된 풍부한 생태계를 사용할 수 있습니다.
3. 빠른 업데이트 & 배포
기존 Shift 1.0 C++ 기반의 코드에서는 UI 변경이 있을 경우 빠른 배포가 힘듭니다.
- 빌드 시간이 오래 걸린다. (1시간 이상 소요)
- 유저가 업데이트를 다운로드하여야 적용된다.
- 버그가 발생했을 때 hotfix를 빠르게 반영하기 어렵다.
- 신규 마케팅을 하기가 어렵다. (ex: 이벤트 페이지 생성)
4. UI와 비즈니스 로직 분리

- 단일 책임 원칙, 관심사의 분리
- Audio Dev 팀이 Audio 관련 개발에 더 시간 투자할 수 있습니다.
- 프론트엔드 개발자의 리소스 사용 가능합니다.
Native + Webview Architecture의 단점
이 아키텍처는 장점만 있을까요?
모든 기술이 그러하듯 단점이 있습니다.
| 단점 | 설명 |
| 성능 오버헤드 | 브라우저 엔진이 메모리를 추가로 소비 (Chromium 기준 100~200MB+) |
| 브리지 통신 지연 | 네이티브 함수 호출 시 직접 호출 대비 비동기 오버헤드 발생 |
| 앱 용량 증가 | WebView 엔진을 번들해야 할 수 있음 |
| 보안 표면 증가 | 웹 기술 스택의 취약점이 데스크톱 앱에도 영향 |
| 두 기술 스택 유지 | C++과 웹 모두 알아야 하는 인력이 필요 (또는 팀 분리) |
하지만 UI를 한 땀 한 땀 따면서 작업하던 비효율을 상쇄하는 게 더 크기 때문에 Hybrid App Architecture로 전환하게 되었습니다.
Webview의 특징
Web과 달리 Webview만이 할 수 있는 특징들이 있습니다. 여러 가지가 있겠지만 몇 가지 예시를 들어 살펴보겠습니다.
file system 접근
Shift의 Webview는 Native와의 통신을 이용해 Web에서 불가능한 기능들을 수행할 수 있습니다.

Filesystem 접근을 예시로 살펴봅시다.
web은 보안을 위해 file system에 접근이 불가능합니다. Shift webview는 아래와 같은 file system 관련 기능들을 수행합니다.
- 보이스 프로필 다운로드 및 관리
- 앱 업데이트 다운로드 및 설치
- 녹음 파일 저장
- 녹음 파일 저장 폴더 선택
Filesystem을 활용한 Frontend 기능 예시
[ 앱 업데이트를 했는데 local storage가 초기화되는 이슈 ]
Shift 프로그램을 업데이트하면 "신규 모델 사용이 가능합니다"라는 팝업을 보여주는 feature가 있었습니다.
프로그램 업데이트를 설치하기 전에 "팝업 표시 유무" 상태를 true로 저장합니다. 이 상태는 브라우저를 껐다 켜도 유지돼야 하기 때문에 Local Storage에 저장합니다. 그런데 문제가 있었습니다.
문제: 프로그램 업데이트 설치 이후 재실행하면 "팝업 표시 유무" 상태가 사라져 있음. 기존에 다른 Local Storage 들은 다 유지되는데 이것만 사라짐
왜 이 상태만 사라지는지 원인은 알 수가 없었습니다. 이에 대한 해결 방법으로
Native에서 제공하는 file system에 접근하는 함수를 통해 "팝업 표시 유무" 상태를 파일로 저장하는 방법이 있었습니다.

다만 파일로 저장하려고 하니 위와 같은 팝업이 떴는데요. 업데이트를 위해 파일 접근이 필요합니다라고 풀어내는 것도 구구절절하고, 유저에게 좋지 않은 경험이라 도입하진 않았습니다. (그리고 개발하는 중에 이 feature가 필요가 없어져서 자동으로 해결되었습니다.)
CPU 최적화
Shift의 경우에는 CPU 사용량에 매우 민감합니다. CPU 사용량이 높으면 voice conversion에 문제가 발생하여 지지직거리는 소음이 발생하기 때문입니다. 이는 Shift 서비스를 이용해 버튜버 방송, 게임, 콘텐츠를 만드는 경우에 매우 치명적입니다. 따라서 CPU 사용량에 훨씬 더 예민하게 개발해야 합니다.
어떤 최적화 작업이 있었는지 소개해 보겠습니다.
배너 최적화

Shift는 최상단에 배너를 보여줍니다. 배너는 5초마다 다음 배너로 넘깁니다.
이 위치는 최상단이라 유저가 가장 먼저 보는 곳이고
각종 마케팅 홍보 및 공지 사항이 있었기 때문에
CPU 오버헤드가 발생하면 안 되는, 성능에 민감한 곳입니다.

이 배너는 Swiper라는 외부 라이브러리를 사용하고 있었는데요,
Swiper에서 제공하는 autoplay라는 props로 쉽게 배너를 자동으로 넘길 수 있었습니다.
그래서 이 기능을 사용했는데요, 이럴 수가..!

CPU 사용률이 High를 찍어버립니다.
그리고 특이하게 리렌더링이 발생하는 시점인 배너를 넘기는 순간뿐만 아니라 항상 High를 찍습니다.
다른 웹 환경에서는 이런 이슈가 없었어요.
그래서 Shift를 개발할 때는 꼭 CPU 사용량을 체크하는 습관을 가지게 되었답니다.
해결 방법은,
1. swiper의 autoplay가 아니라 setInterval로 배너를 넘기게 했어요.
2. 특이하게도 배너가 화면에서 안 보일 때 배너를 넘기면 CPU 사용률이 올라가더라고요. Intersection Observer를 이용해 배너가 노출되지 않으면 배너를 주기적으로 넘기는 기능을 일시 정지하도록 했습니다.
Voice conversion 사용 중 표시 개선
기존 Shift 1.0은 conversion이 발생하면 캐릭터 이미지 가장자리에 움직이는 애니메이션이 있었습니다.
유저는 Voice Conversion 이 문제없이 수행 중이라는 걸 알아야 하기 때문에 꽤 중요한 기능이었습니다.
이 애니메이션은 여기 링크에서 확인하실 수 있어요.
https://youtu.be/vIQxGuc4AlI?si=9tjLEQ2JbLkNNlQd
웹뷰로도 동일하게 구현해보려고 했는데요

아래의 다양한 방법을 시도해 봤으나
- 애니메이션의 주기를 늘리고, 애니메이션을 실행하는 Threshold 값을 추가
- Opacity 변경
- Canvas
- Web worker
- Lottie
여전히 CPU 사용률이 높았습니다.
애니메이션의 변경 주기를 낮추면 CPU 사용률은 개선되었으나 반응성이 저하되어 UX가 좋지 못하였습니다.

0.2초마다 현재 볼륨을 가져오고,
볼륨 크기에 따라 이미지를 교체하는 방식으로 해결하였습니다.
호환성 이슈 해결
앱 버전에 따라 호환성이 깨지는 이슈가 발생합니다.
ex)
- 2.0.3 버전 미만에서는 신규 모델 사용 불가, 2.0.3 버전 이상에서는 신규 모델 사용 가능
- 2.1.2 버전 미만에서는 Check my pitch 기능 사용 불가, 2.1.2 버전 이상에서는 사용 가능
호환성이 깨질 때마다 강제 업데이트를 할 순 없습니다.
어떻게 이 문제를 구현할까요? 총 3가지 방법이 있었습니다.
- Amplify 브랜치 별로 다른 URL 사용 (sub domain) (ex: https://{브랜치명}.{url})
- 버전에 맞는 Bundle 파일 다운로드해서 사용
- FE코드는 항상 최신을 유지하고 버전별로 분기 태우기
이중에 저는 어떤 것을 선택했을까요?
[1안과 2안]
장점: FE코드에서 버전별로 분기하는 로직 필요 없음
단점:
- 이슈 발생 시 핸들링 어려움. (어느 버전에서 에러 발생했는지 일일이 다 확인하여 해당 버전 이후의 모든 코드에 적용해야 함)
- 모든 버전에 공통된 로직 (ex: 마케팅적인 기능이나 디자인)을 적용하고 싶을 때 어려움
[3안]
장점: 방법 1과 2의 단점 해소
단점: 버전별로 분기하는 로직이 많아진다면 FE 코드가 지저분해짐
여러분은 단점이 더 치명적이신 것 같나요?
정답이 없는 문제이지만
저는 3안이 더 효율적이라 생각하여 이 방법을 선택했습니다.
이처럼 웹뷰라는 새로운 환경에서의 개발 경험은 저에게 다른 문제 해결 경험을 하게 해 주었습니다.
특히나 Shift 유저들의 따뜻한 응원으로 보람을 많이 느꼈네요.
읽어주셔서 감사합니다~!
'dev' 카테고리의 다른 글
| AI 시대에서 프론트엔드 개발자는 어떻게 일해야할까? (mdc, mcp 사용 후기) (0) | 2026.01.13 |
|---|---|
| 9년차 개발자로서 정립한 나만의 개발 철학 (3) | 2025.09.07 |
| 토스 2025 makers 컨퍼런스 후기 (11) | 2025.07.27 |
| 프론트엔드 개발자 취업 준비 꿀팁 (이력서, 포폴, 면접, 코딩 테스트, 과제 테스트) (4) | 2025.01.30 |
| 개발자로서 자립할 수 있을까? 솔로프리너 인디해커 강연 후기 (6) | 2024.12.27 |
댓글