놓치기 쉬운 안드로이드 UI 디테일 살펴보기

Sungyong An
12 min readAug 25, 2020
2020 NAVER TECH CONCERT

지난 주에 대학생/주니어 개발자 분들을 대상으로 하는 2020 NAVER 테크콘서트 온라인 행사가 열렸습니다. 현업의 개발자분들이 내용을 준비하여 진행되었고, 저도 참여할 수 있었는데요. 영상으로만 남기기에 조금 아쉬운 마음이 들어 행사에서 발표한 내용 일부를 글로 옮겨봤습니다.

그럼 시작하겠습니다. 🚀

“안드로이드 앱을 만드려면 JAVA만 알면 되나요?”

처음 개발해보려고 하는 분들이 종종 이런 질문을 하는 경우가 있는데요. Java만 해도 어느정도 개발이 가능합니다만, 일반적으로는 Java 외에도 XML 코드를 작성하고, 경우에 따라서는 C++ 코드를 작성하기도 합니다.

최근에는 대부분 Java 대신 Kotlin 코드를 작성하고 있습니다. 구글에서 공식언어로 선정하기도 했고, Java에 비해 장점이 많아서 쓰고 있죠. 현업 개발자의 경우에 Java/Kotlin 코드를 작성하는 것은 매우 익숙한 일이고, 특수한 경우를 제외하면 코드 참조하는 곳을 따라가기도 쉽습니다.

그런데 사실 안드로이드 UI를 구현할 때는 보통 XML 코드도 많이 작성해야 합니다. 예를 들면, 애니메이션/이미지/레이아웃/메뉴 등등등등 매우 다양한 곳에 쓰이고 있죠.

하지만 작성한 XML이 실제로 어떻게 앱에 사용되는지는 모르는 분이 꽤 있을 것 같습니다. 동작 방식을 이해하지 않고, “해보니 그렇게 동작하더라”와 같은 경험적인 형태로 사용방법을 습득을 하게 되는 경우가 있는 것 같습니다.

그래서 오늘은, XML이 실제로 동작하는 방식을 알아보는 것이 목표입니다.

1. 버튼이 잘 안눌러지는 이유

간단한 UI부터 살펴봅시다. 앱을 개발하다보면, 가끔 버튼이 안 눌러지는 경우가 있는데요. 잘 눌러보면 눌려지긴 하니까, 무심코 지나치는 경우도 있을 수 있습니다. 🙄

이미지 버튼을 구현하다보면 쉽게 발생될 수 있는데요. 개발자 옵션에서 ‘레이아웃 범위표시’ 옵션을 켜보면, 버튼 영역이 충분치 않음을 알 수 있습니다.

디자인 가이드를 적용하려고 추가한 margin이 원인인데요. 이는 padding으로 대체하여 디자인 가이드와 터치 영역 모두를 확보할 수 있습니다. 즉, 디자인 가이드를 맹목적으로 따르지 말고, 사용성을 고려하여 터치 영역을 적절히 확보하는 것이 좋겠죠.

조금 전에 보셨듯이, XML 코드 한 줄에도 큰 차이가 발생할 수 있습니다. 그래서 더더욱 XML 코드가 어떻게 동작하는지 이해하고, 문제가 생겼을 때는 원인을 확인할 수 있어야겠죠. 🙆‍♂

2. 버튼이 안 눌러지는 것처럼 보이는 이유

목록형 UI를 구현하다 보면 클릭 효과가 이미지 아래에 그려져서 보이지 않는 경우가 종종 있습니다. 여백이 충분한 경우에는 상관 없을 수 있지만, 그리드 형태라면 여백이 부족하여 버튼이 안눌리는 것처럼 보일 수 있겠죠. 🤔

<androidx.constraintlayout.widget.ConstraintLayout
android:foreground="?selectableItemBackground">

이럴 때는 foreground 속성을 이용하여 해결할 수도 있습니다.
그런데 이 속성을 사용하면 아래처럼 경고 메세지가 보일 거에요. 🙄

사실 안드로이드는 OS 버전에 따라 특정 속성들을 지원하지 않을 수 있습니다. foreground 속성은 API 23부터 플랫폼에 추가되었기 때문에, 앱의 min sdk 버전이 더 낮다면 다른 대안을 찾아야 합니다. 😐

<FrameLayout
android:foreground="?selectableItemBackground">

그런데 희한하게도 FrameLayout을 사용하면 API 21에서도 foreground 속성이 잘 동작합니다. 도대체 왜 그런걸까요??? 🤔

http://androidxref.com/5.0.0_r2/.../android/widget/FrameLayout.java

AOSP를 살펴보면 정확한 이유를 알 수 있습니다. foreground는 원래 FrameLayout의 속성으로, API 23부터는 View의 속성으로 변경되었습니다. 따라서 API 21에서도 동작할 수 있었던거죠.

여기서 재미있는 점은ConstraintLayout에서도 foreground 속성을 사용할 수 있었다는 부분입니다. 즉, 자신이 정의한 속성만이 아니라 상속하는 상위 View 속성도 함께 사용할 수 있다는 것을 알 수 있습니다. 🙆‍♂

3. 레이아웃 XML은 어떻게 그려지는가?

레이아웃 XML이 어떻게 화면에 그려지게 되는지 궁금하지 않나요? 🤔

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
@Override
public void setContentView(int resId) {
...
LayoutInflater.from(mContext).inflate(resId, ...);
...
}

흔히 많이 작성하는 Activity의 내부 모습입니다. setContentView() API를 호출하면 내부적으로 LayoutInflater가 XML을 다루는 것을 알 수 있습니다.

http://androidxref.com/9.0.0_r3/.../android/view/LayoutInflater.java

LayoutInflater는 레이아웃 XML을 파싱해서 View 구현체로 변환해줍니다.
따라서 XML 코드는 실제 구현체로 변환해주는 단계가 반드시 필요합니다.

한가지만 더 덧붙이면 각 View는 XML에 선언된 순서대로 그려집니다.
여기서 구체적인 draw 내용은 XML을 벗어나는 부분이라 생략합니다.
궁금하신 분들은 직접 코드를 찾아보시는 것도 재미있겠죠? 😅

4. AppCompat과 MDC

이번에는 레이아웃 XML에 정의된 View 속성들을 살펴볼까요?

가운데 보이는 ❤️ 아이콘에 붉은색을 보여주려고 tint 속성을 사용했습니다. 이미지 한 벌을 다양한 곳에 다른 색상으로 쓸 수 있게 해주는 멋진 API죠. 😎 아래 코드에 보이는 것처럼 Color Selector를 tint 속성에 넣을 수 있습니다.

하지만 API 21 미만 단말에서는 런타임 오류가 발생합니다.
원인을 찾아보면 tint 속성에서 발생하는데요. 왜 그럴까요? 🤔

http://androidxref.com/4.4_r1/.../android/widget/ImageView.java

그 이유는 역시 AOSP를 살펴보면 명확하게 알 수 있습니다.
API 21부터는 ColorStateList를 허용하도록 바뀌었지만, 그 이전에는 Color Integer만 허용하고 있습니다. 😮

그렇다면 해결할 수 있는 방법은 없을까요?

혹시 최근 업데이트된 AppCompat 1.2.0 버전을 사용해보신 분이 있다면, android:tint 속성에 이런 오류 메세지가 뜨는 것을 보셨을 수도 있습니다. 최근에 이와 관련하여 Lint Rule이 추가되었는데요.

즉, android:tint 대신 app:tint 속성을 사용하면 됩니다. 💯

아마도 이렇게 생각하실 수 있습니다. 엥? 무슨 차이가 있는거죠? 👀

<declare-styleable name="ImageView">
<attr name="tint" format="color" />
</declare-styleable>
<declare-styleable name="AppCompatImageView">
<attr format="color" name="tint"/>
</declare-styleable>

속성이 정의된 주체를 확인해보면 명확해집니다.
android:tint는 ImageView의 속성이고,
app:tint는 AppCompatImageView의 속성입니다.
참고로 android:가 붙는 속성은 플랫폼에 정의된 속성입니다. 😉

AppCompatImageView는 AppCompat이라고 하는 안드로이드 공식 라이브러리에 포함된 위젯으로, AppCompat은 플랫폼 버전에 따라 차이가 발생하는 부분을 메꿔주는 기능을 합니다. 그래서 AppCompatImageView의 tint 속성은 API Level에 관계없이 ColorStateList를 허용하는 것이죠.

잠깐! ImageView가 어떻게 AppCompatImageView가 된걸까요?

이쯤되면 대략 머리가 아파온다… 🤕

class MainActivity : AppCompatActivity()

비밀(?)은 상속에 숨어 있습니다.

AppCompatActivity를 상속하면 내부적으로 LayoutInflater에 Factory를 설정해주는데요. LayoutInflater로 레이아웃 XML을 View 구현체로 변환할 때, 우선적으로 AppCompatViewInflater를 통하게 됩니다. AppCompatViewInflater 내부에서 “ImageView” 태그는 AppCompatImageView로 생성해줍니다.

ImageView가 어떻게 AppCompatImageView가 된건지 이제 이해되시죠? 👐

이번에는 Button을 살펴봅시다.
앱이 상속하는 테마에 따라 Button의 모양이 바뀌는게 보이시나요?

그 이유는 MDC 테마에서 ViewInflater를 교체하여 사용하기 때문입니다. 그래서 “Button” 태그가 MaterialButton으로 생성됩니다.

정리하면 상속하는 테마에 따라 View 구현체가 달라질 수도 있습니다. AndroidStudio 4.0은 기본 프로젝트를 생성하면 AppCompat을 상속하지만, AndroidStudio 4.1부터는 기본이 MDC 상속으로 바뀌었으니 주의하세요!

📢 여기까지가 세션 내용의 절반입니다!

오늘은 안드로이드를 구성하는 XML 코드에 대해서 살펴봤습니다.
어떤가요. 안드로이드 앱 개발 재미있어 보이지 않나요? 😈

이외에도 여러가지의 내용들이 더 있었습니다.
내용이 재미있으셨다면, 남은 내용은 영상으로 봐주시면 좋을 것 같아요. 🙇 못난 얼굴이 무려 30분이나 등장하는 점은 미리 양해 부탁드립니다. 🤦‍♂
링크는 아래 더보기 🔍 부분을 참고해주세요.

요즘 코로나로 인해 개발자 행사도 많이 줄어든 느낌인데요.
온라인 행사로라도 기술 공유를 할 수 있어서 다행인 것 같습니다.
다들 몸조심하시고, 내년에는 오프라인 행사에서 뵐 수 있기를..! 🙏

--

--