[Android] 레이아웃 범위표시로 앱이 어떤 프레임워크로 만들어졌는지 확인하는 방법

easyhooon·2025년 10월 12일
post-thumbnail

서두

Android 기기에서 개발자 옵션을 활성화할 경우 레이아웃 범위표시를 통해 앱의 레이아웃이 어떻게 구성되어있는지 확인해볼 수 있다.

개발자 옵션빠른 설정 개발자 타일

이거 은근 중독성이 있다. 새로운 앱 설치할 때 마다 레이아웃 범위표시를 켜서 구조를 확인해보는 나를 발견...

이번 글에선 레이아웃 범위표시 기능을 통해 각 앱들이 어떤 프레임워크로 만들어져있는지 확인하는 방법을 공유해보고자 한다.

은근 개발 질문방 단골 질문이라 매번 같은 답변 하기 번거로운 관계로... 블로그로 정리하고자 한다. 이 또한 블로그의 순기능일 듯 하다.

본론

Android - XML

쿠팡

가장 일반적인 형태로 빨간색 테두리로 실제 콘텐츠 영역(optical bounds)을 구분하고, 분홍색으로 margin 영역을 채우며, 파란색으로 각 View의 모서리에 코너 마커(clip bounds)를 표시한다.

Android - Compose

플레이 스토어

XML의 레이아웃 범위표시와 다른 부분으로는 분홍색으로 채워진 margin 영역이 존재하지 않으며, 파란색의 코너 마커도 비교적 적은 빈도로 노출된다. 또한 빨간색 테두리외에 파란색 테두리도 관찰된다.

왜 Compose는 분홍색 영역이 존재하지 않는가

분홍색 영역은 margin 영역을 의미한다.

ViewGroup.java#L4205

protected void onDebugDraw(@NonNull Canvas canvas) {
    Paint paint = getDebugPaint();

    // Draw optical bounds
    // View의 광학적 경계(그림자 등을 제외한 실제 콘텐츠 영역)
    {
        // 빨간색
        paint.setColor(Color.RED);
        // 스타일 STROKE(테두리만)
        paint.setStyle(Paint.Style.STROKE);

        for (int i = 0; i < getChildCount(); i++) {
            View c = getChildAt(i);
            if (c.getVisibility() != View.GONE) {
                Insets insets = c.getOpticalInsets();

                drawRect(canvas, paint,
                        c.getLeft() + insets.left,
                        c.getTop() + insets.top,
                        c.getRight() - insets.right - 1,
                        c.getBottom() - insets.bottom - 1);
            }
        }
    }

    // Draw margins
    // View의 margin 영역
    {
        // 반투명 마젠타/분홍색
        // Alpha 63(약 25% 불투명도, 255기준), Red 255, Green 0 Blue 255
        paint.setColor(Color.argb(63, 255, 0, 255));
        // 스타일 FILL(채워짐)
        paint.setStyle(Paint.Style.FILL);

        onDebugDrawMargins(canvas, paint);
    }

    // Draw clip bounds
    // View의 clip 경계(코너 마커로 표시)
    // 각 모서리에 작은 ㄱ, ㄴ 모양으로 표시
    {
        // 파란색 계열
        paint.setColor(DEBUG_CORNERS_COLOR);
        // 스타일 FILL(채워짐)
        paint.setStyle(Paint.Style.FILL);

        int lineLength = dipsToPixels(DEBUG_CORNERS_SIZE_DIP);
        int lineWidth = dipsToPixels(1);
        for (int i = 0; i < getChildCount(); i++) {
            View c = getChildAt(i);
            if (c.getVisibility() != View.GONE) {
                drawRectCorners(canvas, c.getLeft(), c.getTop(), c.getRight(), c.getBottom(),
                        paint, lineLength, lineWidth);
            }
        }
    }
}

Compose에서는 margin을 사용하지 않고 padding만 사용하기 때문에, XML과 다르게 분홍색으로 표시되는 영역이 보이지 않는다.

왜 Compose에서 코너 마커가 XML에 비해 덜 표시되는가

Compose는 AndroidComposeView라는 하나의 View를 생성하며, 그 안에 모든 Composable들을 렌더링한다.

how does jetpack compose work under the hood

따라서 Compose의 Composable들은 개별 View 객체가 아니기에, 전통적인 View 계층 구조를 띄지 않는다.

파란색 테두리의 정체

XML과 다르게 파란색의 테두리도 보이는데, 이 부분에 대해선 아직 어떤 코드에 의해 그려지는지 확인하지 못해, 추후 그 정체를 밝혀내봐야겠다. 정체를 알고 계신분은 댓글 부탁드립니다...ㅎ

내용 추가

-> 영재님께서 답변을 주셔서 그 정체를 확인할 수 있었고, 관련 코드는 댓글에서 확인할 수 있다.

Flutter

네이버 블로그

레이아웃 범위표시를 켜도 범위가 표시되지 않는다..!

Flutter는 Android의 View 계층 구조를 사용하지 않고, 전체 화면의 모든 UI를 자체 렌더링 엔진(Skia, 3.27 버전 이후로 Impeller)을 통해 캔버스에 직접 그리는 방식을 채택하기때문에, View 경계가 보이지 않는다.

Flutter의 렌더링 과정에 대한 더 자세한 내용은 아래 글을 참고하면 좋을 듯 하다.
🎨 [Flutter] 렌더링 원리와 과정: 화면은 이렇게 그려진다

Flutter로 만들어진 앱인지 확인할 수 있는 또 다른 방법으로는 FlutterShark라는 앱을 설치하여 확인하는 방법이 있다.

단, 이 리스트에는 Flutter를 부분적으로만 사용하는 앱도 포함된다. 따라서 리스트에 있다고 해서 모든 화면이 Flutter로 구성된 것은 아니다. 많이들 오해하시는 부분

네이티브 앱도 Skia 엔진 쓰지 않나?

맞다. 다만 사용하는 방식이 다르다.

Android 에서는 각각의 View를 Skia 엔진을 통해 개별적으로 그리고, Flutter 는 전체 UI를 하나의 캔버스에 그린다.

React Native

디스코드

Supercharging Discord Mobile: Our Journey to a Faster App

Android XML과 유사하게 레이아웃의 범위가 표시된다.

React Native는 런타임에 Bridge를 통해 Javascript 코드를 네이티브 UI 컴포넌트로 변환하기 때문에, 최종적으로 Android View 계층 구조가 생성된다. 따라서 네이티브의 뷰의 경계선이 보이게 된다.

Android XML과 React Native 구분이 가능한가

쉽지 않다. 디버그 빌드에선 기기를 흔들어 Dev Menu를 활성화 할 수 있으나, 프로덕션 빌드나, 스토어 설치 환경의 경우 거의 불가능에 가깝다. APK 파일을 뜯어본다던지...

발견적 추론으론, React Native 앱의 경우 코너 마커가 네이티브보다 더 빈번하게 표시되는 것을 확인할 수 있었는데(코너 마커가 표시될 필요가 없는 부분에 표시된다던지, 코너 마커 4개가 한 지점에 모인 '+' 모양이 많이 확인된다던지)

그 이유를 알아본 결과, React Native는 컴포넌트 구조와 레이아웃 시스템 특성상 중첩된 View 계층이 생성되기 쉽다.

https://github.com/react-navigation/react-navigation/issues/9923

따라서 이 과정에서 추가적인 컨테이너 View들이 생성되기 때문에 더 복잡한 형태의 뷰가 만들어지고, 그에 따라 코너 마커가 표시되는 숫자가 늘어난다.

다만, Android XML 기반의 앱의 화면이 여러 컨테이너로 구성된 중첩된 화면일 경우에도 코너 마커가 상당히 많이 표시되기 때문에, 이런 복잡한 Android XML 화면과 React Native를 구분하기엔 한계가 있다. 위에 쿠팡 앱도 상당히 복잡한 XML 화면으로 구성되어 있다.

WebView

당근 - 동네생활 탭

https://www.youtube.com/watch?v=4UD4EB00AME

Flutter 처럼 각각의 레이아웃의 범위가 표시되지 않고, 화면내 웹뷰 영역이 'X'자 표시가 나타나는 것을 확인할 수 있다.

동네 생활 탭의 TopBar와 앱 전역의 BottomNavigationBar는 Android XML로 구현되어 있다.

View.java#L24997

private void debugDrawFocus(@NonNull Canvas canvas) {
    if (isFocused()) {
        final int cornerSquareSize = dipsToPixels(DEBUG_CORNERS_SIZE_DIP);
        final int l = mScrollX;
        final int r = l + mRight - mLeft;
        final int t = mScrollY;
        final int b = t + mBottom - mTop;

        // X자 그리기
        final Paint paint = getDebugPaint();
        paint.setColor(DEBUG_CORNERS_COLOR);

        // Draw squares in corners.
        // 대각선 X 표시
        paint.setStyle(Paint.Style.FILL);
        canvas.drawRect(l, t, l + cornerSquareSize, t + cornerSquareSize, paint);
        canvas.drawRect(r - cornerSquareSize, t, r, t + cornerSquareSize, paint);
        canvas.drawRect(l, b - cornerSquareSize, l + cornerSquareSize, b, paint);
        canvas.drawRect(r - cornerSquareSize, b - cornerSquareSize, r, b, paint);

        // Draw big X across the view.
        paint.setStyle(Paint.Style.STROKE);
        canvas.drawLine(l, t, r, b, paint);
        canvas.drawLine(l, b, r, t, paint);
    }
}

WebView는 Focusable한 View이고(디폴트로 focusable이 true로 설정),
웹 페이지가 로드되면 자동으로 포커스를 획득하기 때문에, X표시가 화면에 나타나게 된다.

웹뷰 영역이라서 X자로 나타나는 것이 아닌, "이 View가 현재 포커스를 가지고 있다" 는 의미로 이헤하는게 더 적절할 듯 하다. 실제로 네이티브로 구성된 화면의 텍스트필드를 레이아웃 범위표시로 확인해보면 X자로 표시된다.

화면 캡처를 하는 경우에도, 시스템이 잠깐 화면에 포커스를 부여하기 때문에 X자 표시가 잠깐 나왔다가 사라진다.

focusable 옵션을 명시적으로 false로 설정한 경우 Flutter 로 만들어진 화면과 구분이 어려운 문제가 있는데, 실제로 몇몇 높은 확률로 웹뷰로 추정되는 화면이 X자 표시가 나타나지 않는 케이스를 확인할 수 있었다.

결론

Android 기기의 개발자 옵션내 레이아웃 범위표시를 활성화하여, 각 앱이 어떤 프레임워크로 만들어졌는지 확인할 수 있는 방법을 알아보았다.

iOS 기기에서도 마찬가지로 이를 확인할 수 있는 방법이 있는지 궁금해서 수소문을 해보았는데, 개발자 옵션에 관련 기능을 지원하지 않는다고 한다... 아쉬운 부분

이후 각 프레임워크별로 어떠한 과정들을 거쳐 화면 렌더링이 진행되는지 추후 학습을 진행해봐야겠다.

앞으로 레이아웃 범위표시를 통해 관심 있는 앱이 어떤 프레임워크로 만들어졌는지 알고 싶을 때, 이 글이 도움이 되길 바란다.

reference)
https://discord.com/blog/supercharging-discord-mobile-our-journey-to-a-faster-app
https://velog.io/@tygerhwang/Flutter-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%9B%90%EB%A6%AC%EC%99%80-%EA%B3%BC%EC%A0%95-%ED%99%94%EB%A9%B4%EC%9D%80-%EC%9D%B4%EB%A0%87%EA%B2%8C-%EA%B7%B8%EB%A0%A4%EC%A7%84%EB%8B%A4
https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/java/android/view/ViewGroup.java
https://github.com/facebook/yoga
https://www.aubergine.co/insights/jetpack-compose-vs-xml-a-comprehensive-comparison-for-android-ui-development
https://www.youtube.com/watch?v=Q9MtlmmN4Q0
https://github.com/react-navigation/react-navigation/issues/9923
https://api.flutter.dev/flutter/dart-ui/FlutterView-class.html
https://docs.flutter.dev/add-to-app/android/add-flutter-view

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

2개의 댓글

comment-user-thumbnail
2025년 10월 16일

저도 글을 보고 Compose 에서 파란선, 빨간선의 정체가 무엇일까 하고 궁금해져 찾아보았습니다!!
cs.android.com 에서 compose ui 쪽을 확인해보니 파란선은 LayoutModifierNodeCoordinator 에서 빨간선은 InnerNodeCoordinator 에서 owner 의 showLayoutBounds 를 보고 그리고 있더라구요.

해당 코드들을 확인해볼 수 있는 링크 하단에 함께 첨부해두겠습니다!! 좋은 글 감사합니다!

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutModifierNodeCoordinator.kt;drc=b8433d431773a0ae1d4468280be2c183c0a1e89f;l=278

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt;drc=9a42d7506131f22b905dcb60141bd84d942f86af;l=180

1개의 답글