Compose HotSwan "v2" Beta 사용 후기

easyhooon·2026년 7월 6일
post-thumbnail

서두

지난 3월 Compose HotSwan v1 후기를 쓰며 @Preview에 기대던 UI 개발 흐름이 달라질 수도 있겠다고 느꼈다. 실제 기기에서 변경 결과를 바로 확인하는 경험은 인상적이었지만 구조를 크게 바꾸면 전체 빌드와 앱 재실행으로 넘어가 작업 흐름이 끊기는 아쉬움도 있었다.

Compose HotSwan은 Jetpack Compose 코드의 변경 결과를 실행 중인 실제 기기에 반영하는 Android Studio 플러그인이다. 설치 방법과 기본 기능, v1의 동작 방식은 이전 후기에서 확인할 수 있다.

그 뒤 v2 Beta가 공개됐고 v1에서 아쉬웠던 fallback의 경계가 얼마나 밀려났는지 직접 확인해 보고 싶어 다시 글을 쓰게 됐다. 실제 프로젝트에 적용해 구조 변경과 LaunchedEffect·uiState를 실험하고 테스트 중 마주친 HotSwan과 Metro 문제를 추적한 과정까지 정리했다.

이 글은 이전 후기에서 사용한 v1.0.13을 기준으로 비교한다. 이후 v1 안정판에도 Composable 재배치와 교차 파일 함수 추가 등 여러 기능이 보강됐다. v1 전체가 v1.0.13과 같았다고 보기는 어렵다.

본론

v2 Beta는 아직 Stable 채널에 올라오지 않아 Android Studio의 Plugins 검색이나 일반 업데이트로 설치할 수 없다. JetBrains Marketplace의 Beta 버전 페이지에서 IDE 플러그인 파일을 직접 내려받아야 한다.

다운로드한 파일은 Settings > Plugins > 톱니바퀴 아이콘 > Install Plugin from Disk에서 설치한다. IDE 플러그인과 Gradle 플러그인 버전은 똑같이 맞춰야 하며, 설치 전 공식 Releases에서 최신 버전을 확인하는 편이 안전하다.

버전 확인
HotSwan의 공식 최소 요구사항은 Kotlin 2.3.0+지만 이번 v2·Metro 테스트는 Kotlin 2.4.0+를 기준으로 진행했다. Metro를 함께 쓴다면 Kotlin 2.4 계열을 지원하는 1.2.1+가 필요하다. 다만 이 글에서 다룬 stale graph callsite 버그는 1.3.1에서 수정됐으므로 Metro 1.3.1+ 사용을 권장한다.

v1과 달라진 부분

v1.0.13에서 저장은 이미 있는 화면의 값을 조율하는 동작에 가까웠다. v2에서는 화면의 뼈대를 바꾸는 수정까지 같은 흐름에 들어왔다.

내가 체감한 가장 큰 차이는 fallback 동작이었다. v1.0.13도 변경 결과 자체는 결국 반영됐지만 구조 변경에서는 전체 빌드와 재설치로 넘어가며 앱이 다시 실행되곤 했다. 화면은 원하는 대로 바뀌었어도 그 순간만큼은 Hot Reload라고 부르기 애매했고 작업 흐름도 한 번씩 끊겼다.

이번 v2 Beta에서 시도한 변경 범위에서는 앱이 재실행된 적이 한 번도 없었다. Composable 추가·삭제나 레이아웃 재배치도 실행 중인 화면에 바로 반영됐다. 지원 범위가 늘었다는 설명보다 fallback으로 끊기던 흐름이 사라진 데서 발전을 가장 크게 느꼈다.

변경 내용v1.0.13v2 Beta
색상·여백·텍스트·크기 변경지원지원
Modifier와 스타일 변경지원지원
Composable 추가·제거제한적지원
레이아웃 순서 변경제한적지원
조건 분기와 화면 로직 변경제한적지원
화면 전체를 새 코드로 교체미지원지원
상태와 내비게이션 유지지원지원

단순 리터럴을 고치면 컴파일을 건너뛰고 실행 중인 앱에 값만 패치한다. 공식 글은 색상, 문자열, 숫자 같은 수정이 50ms 미만에 반영된다고 설명한다. 구조나 로직을 바꿀 때는 변경된 코드만 컴파일하며 이 경로는 대략 1초 정도다.

속도보다 수정 범위가 더 크게 다가왔다. Composable을 넣거나 빼고 ColumnRow로 바꾸고 ifwhen 분기를 고쳐도 앱을 다시 실행할 필요가 없다. 공식 v2 데모의 화면 전체 교체도 약 3초 만에 반영됐다.

상태 보존 방식은 매번 같지 않다. HotSwan은 영향받은 영역만 다시 그리는 targeted recomposition부터 시도한다. 여기서는 remember, 스크롤 위치, 내비게이션 스택이 그대로 남는다.

처리하기 어려운 변경이면 composition reset, Activity restart 순으로 넘어간다. composition reset에서는 rememberSaveable, ViewModel, 내비게이션을 유지하지만 일반 remember 값은 사라질 수 있다. 어떤 변경에서도 상태가 무조건 보존되는 것은 아니다.

전체 빌드가 필요한 변경도 남아 있다. 함수 파라미터 추가·삭제, 클래스 계층 변경, inline 함수 수정이 대표적이다. 이 목록까지 외울 필요는 없다. HotSwan이 구조를 먼저 검사하고 필요하면 증분 빌드와 재설치로 전환한다.

실기기에서 단계별로 확인한 결과

설명만 읽는 것과 실제 프로젝트에서 확인하는 건 달랐다. 기준 APK를 한 번 설치한 뒤 Home 화면을 띄워 둔 채 기본 UI부터 컴포넌트 구조, ViewModel 로직, 사이드 이펙트 순으로 변경했다.

검증하는 동안 앱 PID는 15419, APK 설치 시각은 17:11:48로 계속 유지됐다. 모든 유효 reload가 restartRequired=false로 끝났고 앱이 다시 실행되거나 재설치되는 장면은 한 번도 없었다.

단계변경 내용HotSwan 로그실기기에서 확인한 결과
기본 UI텍스트 스타일, 구분선 크기와 색상 변경reloadedCount=1820글자 크기와 구분선이 즉시 변경됨
컴포넌트 추가새 배지 Composable과 Preview 추가reloadedCount=12실행 중인 화면에 배지가 바로 나타남
uiState 데이터ViewModel의 데이터 가공 로직 변경reloadedCount=1새로고침 후 변경된 닉네임이 화면에 반영됨
사이드 이펙트LaunchedEffect와 Toast 호출 추가reloadedCount=13실행 중인 실제 기기에 Toast가 노출됨
effect key 변경revision 값을 1 → 2로 변경리터럴 1건 fast path기존 effect가 다시 실행되며 Toast가 재노출됨
화면 색상배경과 주요 카드의 색상을 고대비로 변경reloadedCount=17, 11화면 전체의 색상 변화가 즉시 반영됨

여기서 reloadedCount는 reload를 실행한 횟수가 아니라 그때 적용된 클래스 수다. 첫 변경에서는 1,820개가 보고됐고 이후부터는 변경 범위에 따라 12개, 1개, 13개로 줄었다.

전체 로그 89줄도 함께 확인했다. 모두 INFO였고 WARN, ERROR, 재설치나 프로세스 재시작을 뜻하는 로그는 없었다. 파일 저장을 중복 감지해 0 class(es)로 끝난 기록은 있었지만 앞선 적용 결과를 취소한 실패는 아니었다.

UI 변경을 넘어 사이드 이펙트까지

화면이 바뀌는 것만 확인하려고 시작했는데 더 흥미로웠던 건 LaunchedEffect였다. 실행 중인 Home 화면에 effect와 Toast 호출을 새로 추가하자 실제 기기에 Toast가 바로 나타났다.

effect가 우연히 한 번 실행된 것인지도 다시 확인했다. 명시적인 revision 값을 key에 넣고 1에서 2로 바꾸자 기존 effect가 재실행되며 Toast가 다시 노출됐다. 이 변경은 클래스 컴파일이 아닌 리터럴 1건의 fast path로 처리됐다.

ViewModel의 uiState가 갱신되자 기존 StateFlow를 구독하던 화면은 Loading 분기로 바뀌었다. 텍스트나 색상만 다시 그린 게 아니다. effect 실행, 상태 전달, 화면 반영까지 이어지는 과정을 한자리에서 확인했다.

Home 화면에서는 ViewModel의 데이터 가공 로직도 실행 중에 교체했다. 기존 StateFlow 값이 저절로 다시 계산되지는 않았지만 당겨서 새로고침을 호출하자 새 로직을 거친 닉네임이 화면에 반영됐다. 상태를 유지한 채 다음 호출부터 변경된 로직이 사용되는 것도 확인할 수 있었다.

특히 진입 조건이 까다로운 화면에서 편했다. Toast나 Loading처럼 잠깐 나타나는 결과를 보려고 앱을 재실행한 뒤 같은 화면까지 다시 들어갈 필요가 없었다. 만들어 둔 상태는 그대로 두고 로직만 바꿔 볼 수 있었다.

직접 부딪혀 본 트러블슈팅

베타를 실제 프로젝트에 붙이니 문서만 읽을 때는 보이지 않던 문제도 만났다. LaunchedEffect 안에서 delay(300) 이후 FocusRequester.requestFocus()를 호출하는 화면에서 ViewRootImpl$CalledFromWrongThreadException이 발생했다.

로그를 확인해 보니 UI를 건드리는 requestFocus()가 main이 아닌 kotlinx.coroutines.DefaultExecutor에서 실행되고 있었다. 같은 코드는 HotSwan 1.3.7에서 정상 동작했고 2.0.0-beta01에서만 재현됐다. skydoves 님께 재현 조건과 로그를 직접 전달했고 수정된 v2 빌드에서는 문제가 다시 나타나지 않았다.

문제를 추적하다 HotSwan과 별개인 Metro 쪽 원인도 하나 발견했다. @AssistedInject ViewModel 생성자의 non-assisted dependency를 바꾼 뒤, 새 Factory signature와 기존 graph callsite가 어긋나 NoSuchMethodError가 발생했다.

HotSwan과 Metro는 모두 Kotlin compiler plugin이지만 같은 방식으로 코드를 다루지는 않았다. skydoves 님의 설명에 따르면 HotSwan은 FIR이 아닌 IR-backend에서 동작하며 문제에 등장한 Metro의 $MetroFactorygraph_$Impl은 건드리지 않았다.

처음에는 Metro의 결과가 HotSwan backend를 거치며 증상이 꼬였을 가능성도 의심했다. 하지만 HotSwan을 완전히 제거해도 같은 오류가 재현됐다. 두 플러그인의 충돌이 아니라 Metro와 Kotlin/Gradle의 incremental compilation 문제로 범위가 좁혀졌다.

최소 재현 프로젝트를 만들고 bytecode를 비교해 보니 graph callsite가 갱신되지 않고 이전 Factory signature를 호출하고 있었다. 내용을 정리해 Metro #2531로 제보했다.

Zac Sweers 님은 다음 날 재현 테스트를 추가하고 문제를 수정했다. Metro 1.3.1 릴리스에도 “stale graph callsites” 수정이 명시됐다. HotSwan을 시험하다 다른 compiler plugin의 버그까지 발견하고 해결된 셈이다.

이 과정이 번거롭지 않았다면 거짓말이다. 그래도 재현 조건과 로그를 전달하자 피드백이 빠르게 왔고 실제 수정까지 확인했다. Beta 버전을 쓴다는 건 새 기능을 먼저 만나는 동시에 아직 드러나지 않은 경계를 함께 찾는 일이기도 했다.

더 나아가 v2의 차별점

수정 범위가 넓어지자 MCP의 쓰임도 달라졌다. v1에도 MCP 서버는 있었지만 v2에서는 AI가 다룰 수 있는 변경이 값 조정에서 화면 구조와 로직으로 넓어졌다.

HotSwan MCP를 연결하면 AI가 reload를 실행하고 실제 기기 화면을 캡처한 다음 여러 변형까지 비교한다. AI가 답을 대신 고르기보다는 선택지를 펼쳐 놓고 개발자가 실제 데이터와 상태가 담긴 화면을 보며 고르는 흐름이다.

UI 작업에서는 이 차이가 더 선명하다. 애니메이션의 duration과 dampingRatio, 테마 팔레트와 간격, LaunchedEffect의 key와 body는 코드만 봐서는 판단하기 어렵다. 직접 눈으로 봐야 하는 작업일수록 피드백 주기가 짧아야 한다.

v2를 단순히 ‘더 빠른 Hot Reload’라고 부르기에는 부족하다. Native Jetpack Compose와 Kotlin, 기존 Android 라이브러리를 그대로 쓰면서 코드 작성과 실제 기기 검증 사이의 간격을 줄여준다.

결론

이전 v1.0.13이 만들어진 화면을 빠르게 다듬는 도구였다면, v2 Beta는 실행 중인 앱의 맥락을 지킨 채 화면 구조를 다시 설계하는 도구에 가까워졌다. 값 하나에서 화면 전체까지, 한 번에 다루는 작업의 크기가 커졌다.

Beta라는 점은 감안해야 한다. 실제 프로젝트에서는 coroutine continuation이나 다른 compiler plugin의 incremental compilation처럼 예상하지 못한 곳에서 문제가 드러났다. 모든 변경이 Hot Reload로 끝나는 것도 아니다.

그래도 수정한 UI뿐 아니라 Toast, Loading 상태처럼 로직의 결과까지 실행 중인 기기에서 확인하는 경험은 기대 이상이었다. 화면 진입 과정이 길거나 상태 재현 비용이 큰 프로젝트일수록 더 유용할 듯하다.

이번 단계별 테스트에서는 그 차이를 로그로도 확인했다. 기본 UI부터 Composable 추가, ViewModel 로직과 LaunchedEffect까지 바꾸는 동안 앱 PID와 설치 시각이 유지됐다. 이전 후기에서 한 번씩 흐름을 끊던 앱 재실행이 이번에는 없었다.

v1을 썼을 때는 @Preview를 대신할 수 있겠다고 생각했다. v2를 직접 써보니 범위는 UI 확인에서 끝나지 않았다. 화면을 바꾸는 Hot Reload를 넘어 실행 중인 앱에서 UI·사이드 이펙트·상태 전이까지 연속해서 검증하는 실시간 개발 피드백 도구에 가까워졌다.

Reference

profile
실력은 고통의 총합이다. Android Developer

0개의 댓글