🩺 comdoc 규칙 카탈로그

compose-doctor 0.1.0 · 총 24종 · 신뢰도 승격: suspected → ai-suspected → confirmed → measured
갈래
심각도
24개 규칙 표시 중

구문 확정 규칙 — AST 스캐너

suspected12/12

소스 구문만으로 판정한다. 빌드·외부도구 없이 즉시 동작하고, 마스킹·스코프 게이트로 오탐을 차단한다.

lazy-list-missing-keyLazy 리스트 items()에 key 누락warning

판정 기준 — key 없는 items() 는 아이템 삽입/리오더 시 위치 기반 매칭이 어긋나 전체 아이템이 재구성되고 아이템 상태(remember)가 잘못 재사용된다.

수정 옵션

  • items(list, key = { it.id }) 처럼 안정적 고유 id 를 key 로 지정

근거https://developer.android.com/develop/ui/compose/lists#item-keys

lazy-list-index-keyLazy 리스트 key 가 index 기반 (리오더 시 무효)warning

판정 기준 — index 를 key 로 쓰면 key 없는 것과 동일하게 리오더/삽입 시 전 아이템이 밀려 재구성된다 — key 의 목적(아이템 정체성 추적)을 무효화.

수정 옵션

  • index 대신 아이템의 안정적 고유 id 를 key 로 사용

근거https://developer.android.com/develop/ui/compose/lists#item-keys

lazy-list-missing-content-type이종(異種) Lazy 리스트에 contentType 미지정info

판정 기준 — 한 Lazy 스코프에 서로 다른 아이템 종류가 섞였는데 contentType 이 없으면 재사용 풀이 종류를 구분하지 못해 스크롤 시 컴포지션 재사용 효율이 떨어진다. 단일 종류 리스트면 문제 아님(verdict=false).

수정 옵션

  • items(..., contentType = { it.type }) 로 아이템 종류 지정

근거https://developer.android.com/develop/ui/compose/lists#item-keys

unremembered-mutable-stateremember 없이 mutableStateOf 생성 (매 recomposition 재생성)error

판정 기준 — remember 없는 mutableStateOf 는 recomposition 마다 새 상태로 리셋된다 — 상태가 유실되는 사실상 버그.

수정 옵션

  • remember { mutableStateOf(...) } 로 감싸기
  • 상태를 hoisting 해 파라미터로 받기

근거https://developer.android.com/develop/ui/compose/state#state-in-composables

collection-literal-arg인자 위치에서 컬렉션 리터럴 생성 (매 recomposition 새 인스턴스)info

판정 기준 — 인자 위치의 listOf(...) 는 매 recomposition 새 인스턴스라 수신 컴포저블의 인자 비교가 항상 실패한다. 원소가 전부 컴파일타임 상수면 remember 승격, 변수 포함이면 remember(키) 또는 상위 hoisting 을 제안.

수정 옵션

  • remember { listOf(...) } 로 승격(상수 원소)
  • remember(deps) { listOf(...) } (변수 원소)
  • 리터럴을 컴포저블 밖 상수/상위 상태로 hoisting

strong skipping — strong skipping 활성 시 unstable 파라미터 컴포저블도 skippable 이 되지만, 비교가 인스턴스 동일성(===)이라 매 recomposition 새 인스턴스면 여전히 skip 에 실패한다 — 문제의 본질이 'non-skippable'에서 '인스턴스 재생성'으로 이동할 뿐 사라지지 않는다.

근거https://developer.android.com/develop/ui/compose/performance/bestpractices#use-remember

collectasstate-without-lifecyclecollectAsState 대신 collectAsStateWithLifecycle 권장info

판정 기준 — collectAsState 는 백그라운드에서도 Flow 수집을 유지해 리소스를 낭비한다. 성능(recomposition)보다는 수명주기 리소스 문제 — 설명에서 과장하지 말 것.

수정 옵션

  • collectAsStateWithLifecycle() 로 교체 (lifecycle-runtime-compose 의존성)

근거https://developer.android.com/topic/libraries/architecture/compose#streams

constant-key-effectLaunchedEffect/DisposableEffect 상수 key (key 누락 의심)info

판정 기준 — 상수 key(Unit/true) 자체는 '첫 composition 1회만 실행' 의도로 정당할 수 있다. 본문이 파라미터/상태 등 변하는 값을 참조하는데 key 에 없으면 stale 값 캡처 — 그때만 문제로 판정하고, 의도적 1회 실행이면 verdict=false.

수정 옵션

  • 본문이 참조하는 변하는 값을 key 로 추가: LaunchedEffect(value) { ... }
  • 최신 값만 필요하면 rememberUpdatedState(value) 로 캡처

근거https://developer.android.com/develop/ui/compose/side-effects#restarting-effects

heavy-init-in-lazy-itemLazy 아이템에서 무거운 객체를 컴포지션 시점에 생성 (스크롤 jank)warning

판정 기준 — ExoPlayer/MediaCodec/Bitmap 등 무거운 생성이 Lazy 아이템 컴포지션 시점에 있으면 스크롤 중 아이템 등장마다 생성 비용이 프레임을 밀어낸다.

수정 옵션

  • remember { ... } + DisposableEffect 해제로 아이템 수명에 묶기
  • 생성을 화면 진입/실제 재생 시점으로 지연(defer)

근거https://developer.android.com/develop/ui/compose/performance/bestpractices#use-remember

mutable-collection-stateMutableState 에 가변 컬렉션 사용 (변경 감지 불가)warning

판정 기준 — mutableStateOf(mutableListOf(...)) 는 컬렉션 내용을 바꿔도 State 가 변경을 감지하지 못해 recomposition 이 안 일어난다 — 성능이 아니라 갱신 누락 버그. remember 여부와 무관하게 문제.

수정 옵션

  • mutableStateListOf()/mutableStateMapOf() 로 교체
  • 불변 컬렉션 + copy 재할당(state.value = list + item)

근거https://developer.android.com/develop/ui/compose/state#state-in-composables

state-autoboxing원시 타입 mutableStateOf (오토박싱 — 전용 변형 권장)info

판정 기준 — Int/Long/Float/Double/Boolean 리터럴을 mutableStateOf 로 감싸면 매 읽기/쓰기마다 오토박싱된다. 고빈도 갱신 상태(스크롤·애니메이션)일수록 효과 큼 — 저빈도면 과장 금지.

수정 옵션

  • mutableIntStateOf/mutableLongStateOf/mutableFloatStateOf/mutableDoubleStateOf 로 교체

근거https://developer.android.com/develop/ui/compose/state#state-in-composables

expensive-computation-in-composition컴포지션 중 컬렉션 변환 연산 (remember 누락)warning

판정 기준 — items(list.sortedBy { ... }) 처럼 컴포지션 시점에 정렬/필터/매핑을 수행하면 매 recomposition 전체 변환이 재실행된다. 리스트가 크거나 recomposition 이 잦으면 프레임을 밀어낸다.

수정 옵션

  • remember(list) { list.sortedBy { ... } } 로 캐싱
  • 변환을 ViewModel/데이터 계층으로 이동

근거https://developer.android.com/develop/ui/compose/performance/bestpractices#use-remember

composed-modifierModifier.composed 사용 (Modifier.Node 권장)info

판정 기준 — composed {} 는 요소마다 컴포지션을 만들어 재사용이 안 되고 비교 비용이 크다 — 커스텀 modifier 는 Modifier.Node API 권장.

수정 옵션

  • Modifier.Node + ModifierNodeElement 로 마이그레이션

근거https://mrmans0n.github.io/compose-rules/rules/#avoid-modifier-composed

AI 판정 위임 규칙 — type B

ai-suspected8/8

AST 는 구조 앵커(후보)만 잡고, 타입/맥락 판정(이벤트 람다 여부·실제 안정성 등)은 AI 가 수행한다. AI 가 기각한 후보는 리포트의 'AI 기각' 섹션에 사유와 함께 남는다.

backwards-write컴포지션 중 상태 쓰기 의심 (backwards write — 무한 recomposition)error

판정 기준 — 이미 읽은 상태를 컴포지션 본문에서 다시 쓰면 recomposition 무한 루프. 단 **onClick 등 이벤트 람다/effect 블록 안의 쓰기는 정상**이다 — 스니펫에서 쓰기가 람다 안인지 본문 직접 실행인지 판정하고, 람다 안이면 verdict=false.

수정 옵션

  • 상태 쓰기를 이벤트 핸들러/LaunchedEffect 로 이동

근거https://developer.android.com/develop/ui/compose/performance/bestpractices#avoid-backwards

coroutine-launch-in-composition컴포지션 중 코루틴 실행 의심 (recomposition 마다 재실행)error

판정 기준 — 컴포지션 본문에서 scope.launch 를 직접 호출하면 매 recomposition 코루틴이 새로 뜬다. **이벤트 람다(onClick 등) 안의 launch 는 정상** — 스니펫에서 호출 위치가 본문 직접 실행일 때만 verdict=true.

수정 옵션

  • 1회성 작업은 LaunchedEffect(key) 로 이동
  • 이벤트 반응이면 이벤트 람다 안으로 이동

근거https://developer.android.com/develop/ui/compose/side-effects#restarting-effects

stateflow-value-read컴포지션에서 StateFlow.value 직접 읽기 의심 (구독 없는 스냅샷)warning

판정 기준 — 컴포지션에서 StateFlow.value 를 직접 읽으면 값이 바뀌어도 recomposition 이 트리거되지 않는다(갱신 누락). 수신 타입이 StateFlow 계열일 때만 문제 — MutableState.value 나 일반 프로퍼티면 verdict=false.

수정 옵션

  • collectAsStateWithLifecycle() 로 구독 전환

근거https://developer.android.com/topic/libraries/architecture/compose#streams

derived-state-candidate고빈도 상태 파생 계산 (derivedStateOf 후보)info

판정 기준 — 스크롤 인덱스처럼 프레임마다 변하는 상태의 비교식을 본문에서 직접 계산하면 매 프레임 recomposition 된다. 파생 결과의 변화 빈도가 원본보다 훨씬 낮을 때만 derivedStateOf 가 이득 — 아니면 verdict=false(과잉 적용도 비용).

수정 옵션

  • val show by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } }

근거https://developer.android.com/develop/ui/compose/performance/bestpractices#use-derivedstateof

defer-state-read상태 읽기를 컴포지션 단계에서 수행 (람다 modifier 로 지연 권장)info

판정 기준 — Modifier.offset(x,y)/background(color) 의 인자가 자주 변하는 상태에서 파생되면 값 변경마다 컴포지션부터 다시 돈다. 람다 버전은 layout/draw 단계에서만 읽어 컴포지션을 건너뛴다. 인자가 상수/저빈도 값이면 verdict=false.

수정 옵션

  • Modifier.offset { IntOffset(0, provider()) } 로 전환
  • Modifier.drawBehind { drawRect(color) } 로 전환

근거https://developer.android.com/develop/ui/compose/performance/bestpractices#defer-reads

unstable-collection-param컴포저블 파라미터에 불안정 컬렉션 타입(List/Set/Map) 노출 가능warning

판정 기준 — 읽기전용 List/Set/Map 인터페이스는 컴파일러가 unstable 로 추론한다. ImmutableList/PersistentList(kotlinx.collections.immutable)·@Immutable 래퍼면 stable — 타입 표기가 그중 하나면 verdict=false.

수정 옵션

  • kotlinx-collections-immutable 의 ImmutableList/ImmutableSet/ImmutableMap 으로 교체
  • @Immutable 래퍼 클래스로 감싸기
  • 외부 모듈 타입이면 stability configuration file 에 등록

strong skipping — strong skipping 활성 시 unstable 파라미터 컴포저블도 skippable 이 되지만, 비교가 인스턴스 동일성(===)이라 매 recomposition 새 인스턴스면 여전히 skip 에 실패한다 — 문제의 본질이 'non-skippable'에서 '인스턴스 재생성'으로 이동할 뿐 사라지지 않는다.

근거https://developer.android.com/develop/ui/compose/performance/stability/fix#immutable-collections

unstable-model-paramvar 프로퍼티를 가진 모델 클래스를 컴포저블 파라미터로 사용 가능warning

판정 기준 — var 프로퍼티가 하나라도 있으면 그 클래스는 unstable. 모든 프로퍼티가 val + stable 타입이면 stable. Compose 가 var 변경을 감지하지 못해 화면이 갱신 안 되는 버그로도 이어진다.

수정 옵션

  • var → val + copy() 불변 모델로 전환
  • 실제로 안정적이면 @Immutable/@Stable 표기(계약 책임은 개발자)
  • compose 컴파일러 없는 모듈의 클래스면 stability configuration file 등록

strong skipping — strong skipping 활성 시 unstable 파라미터 컴포저블도 skippable 이 되지만, 비교가 인스턴스 동일성(===)이라 매 recomposition 새 인스턴스면 여전히 skip 에 실패한다 — 문제의 본질이 'non-skippable'에서 '인스턴스 재생성'으로 이동할 뿐 사라지지 않는다.

근거https://developer.android.com/develop/ui/compose/performance/stability/fix

viewmodel-forwardingViewModel/State 를 하위 컴포저블로 전달 (불안정 전파 가능)info

판정 기준 — ViewModel 통째 전달은 하위를 unstable 로 만들고 재사용/프리뷰를 막는다. 단 최상위 화면(Screen) 컴포저블이 VM 을 받는 것은 관례 — 하위 컴포넌트로 '다시 전달(forwarding)'하는 경우만 문제로 판정.

수정 옵션

  • 하위에는 필요한 값과 이벤트 람다만 전달 (state hoisting)
  • uiState 는 최상위에서 collect 하고 데이터 클래스로 내려보내기

근거https://mrmans0n.github.io/compose-rules/rules/#hoist-all-the-things

컴파일러 리포트 규칙 — tier2

confirmed3/3

Compose 컴파일러가 확정한 사실(추정 아님). AST 후보와 정확 매칭되면 해당 후보를 confirmed 로 승격한다.

non-skippable-composable컴파일러 리포트가 확인한 non-skippable 컴포저블 (unstable 파라미터 외 원인)warning

판정 기준 — restartable 인데 skippable 이 아닌 컴포저블 — unstable 파라미터가 원인이면 unstable-param finding 이 따로 나오므로, 이 finding 은 그 외 원인(stability 미상 파라미터 등)일 때만 산출된다. 스니펫에서 원인 후보를 짚어 설명.

수정 옵션

  • stability 미상 타입에 @Immutable/@Stable 표기 또는 stability configuration file 등록
  • 파라미터 비교 비용이 크면 @NonRestartableComposable 검토

strong skipping — strong skipping 활성 프로젝트에선 non-skippable 자체가 드물다 — 남아 있다면 @NonSkippableComposable 표기이거나 특이 케이스.

근거https://developer.android.com/develop/ui/compose/performance/stability/diagnose

unstable-class컴파일러 리포트가 확인한 불안정 클래스warning

판정 기준 — 컴파일러가 unstable 로 확정한 클래스. 메시지의 원인 필드(var 또는 unstable 타입)를 짚어, 이 클래스를 파라미터로 받는 모든 컴포저블에 영향이 전파됨을 설명.

수정 옵션

  • 원인 필드를 val + stable 타입으로 전환
  • 컬렉션 필드는 kotlinx-collections-immutable 로 교체
  • 실제로 안정적이면 @Immutable/@Stable 표기(계약 책임은 개발자)

strong skipping — strong skipping 활성 시 unstable 파라미터 컴포저블도 skippable 이 되지만, 비교가 인스턴스 동일성(===)이라 매 recomposition 새 인스턴스면 여전히 skip 에 실패한다 — 문제의 본질이 'non-skippable'에서 '인스턴스 재생성'으로 이동할 뿐 사라지지 않는다.

근거https://developer.android.com/develop/ui/compose/performance/stability/fix

unstable-param컴파일러 리포트가 확인한 불안정 파라미터warning

판정 기준 — 컴파일러가 unstable 로 확정한 파라미터(추정 아님). 타입이 왜 unstable 인지(읽기전용 컬렉션·var 프로퍼티·외부 모듈 클래스)를 스니펫에서 짚어 설명.

수정 옵션

  • kotlinx-collections-immutable 컬렉션으로 교체
  • @Immutable/@Stable 표기 또는 불변 모델 전환
  • 외부 모듈 타입이면 stability configuration file 등록

strong skipping — strong skipping 활성 시 unstable 파라미터 컴포저블도 skippable 이 되지만, 비교가 인스턴스 동일성(===)이라 매 recomposition 새 인스턴스면 여전히 skip 에 실패한다 — 문제의 본질이 'non-skippable'에서 '인스턴스 재생성'으로 이동할 뿐 사라지지 않는다.

근거https://developer.android.com/develop/ui/compose/performance/stability/diagnose

런타임 실측 규칙 — tier3

measured1/1

단말 perfetto 트레이스에서 실측. 컴포저블 이름이 정확 매칭되면 정적 finding 을 measured 로 승격한다.

runtime-jank런타임 트레이스에서 실측된 메인스레드 jank 원인 슬라이스warning

판정 기준 — perfetto 실측으로 잡힌 메인스레드 원인 슬라이스(tier3). 슬라이스명의 의미(Recomposer:recompose=재구성, compose:lazy:prefetch:measure=Lazy 프리페치 측정, animation=애니메이션 프레임, AndroidOwner:measureAndLayout=측정/배치, Compose:onForgotten=dispose 리소스 해제)와 통상 원인·완화책을 설명. fix_code 는 원인 코드가 특정될 때만 제시하고 불확실하면 생략.

수정 옵션

  • recompose 원인이면 위 stability 규칙들의 수정 옵션 적용
  • lazy prefetch/측정이면 아이템 경량화·contentType·고정 크기(Modifier.height) 검토
  • 애니메이션이면 프레임당 작업량 축소·graphicsLayer 활용

근거https://developer.android.com/topic/performance/vitals/render

runtime-jank 원인 카테고리

tier3 실측 finding 은 원인 슬라이스 종류에 따라 카테고리가 붙는다 (메시지 접두 + json jank_category).

카테고리표기완화 방향
recompose재구성stability 규칙(unstable 파라미터/비-remember 생성) 수정으로 재구성 축소
lazy-prefetchLazy 측정아이템 경량화 + contentType + 고정 크기(Modifier.height)로 측정 비용 축소
measure-layout측정/배치레이아웃 중첩 축소·intrinsic 측정 회피·고정 크기 지정
animation애니메이션프레임당 작업량 축소·graphicsLayer 로 draw 단계 이동
dispose해제onForgotten 해제 비용 — 무거운 리소스 해제를 백그라운드로 이동
other기타