devlog of ShinJe Kim

[Android] DeepLink(딥링크) 알아보기

|

딥링크(Deep Link)란?

홈페이지가 아닌 특정 콘텐츠(페이지)로 바로 연결되는 링크를 딥링크라고 합니다. 예를 들어보겠습니다. 안드로이드 개발 문서의 홈페이지는 아래의 주소입니다.

https://developer.android.com

하지만 제가 코틀린 스타일 가이드에대한 페이지로 바로 이동하는 링크를 걸고 싶다면 아래의 주소와 같이 해당 페이지로 바로 이동할 수 있는 딥링크를 사용하는 것입니다.

https://developer.android.com/kotlin/style-guide

딥링크는 일반적으로 아래와 같이 스키마(scheme)호스트(host) 및 경로(path)의 두 부분으로 나뉘어져 있습니다.

{scheme}://{host_path}

딥링크라는 개념은 모바일과 웹의 구분없이 사용되는 개념입니다. 다만 웹의 특성상 링크가 공개되어있어 대부분 딥링크를 사용하기 때문에 특별히 딥링크라고 부르지 않는 것같습니다. 웹처럼 링크가 공개되어있지 않은 모바일에서 이러한 딥링크를 구현하려면 어떻게 해야 할까요?

또 하나의 문제가 있습니다. 모바일 앱이 이미 설치되어있는 경우에는 바로 해당 컨텐츠로 이동하겠지만, 아직 설치되지 않은 경우에는 어떻게 처리해야 할까요?

이러한 문제를 해결하기 위해 나온 방법들을 아래에서 하나씩 살펴보겠습니다.

모바일 딥링크의 종류

딥링크를 구글창에 검색해보면 앱링크, 다이나믹 링크 등 여러가지 관련 개념들이 많이 나옵니다. 한 번 제대로 짚고 넘어가지 않으면 계속 혼란스러울 것 같아 이번 기회에 각 방법들의 정확한 개념과 동작 방식, 그리고 문제점에 대해 정리해보려 합니다.

지금부터 언급하는 것들은 모두 딥링크라는 개념을 구현하기 위한 방식들입니다. 즉, 모두 딥링크에 속하는 개념입니다.

초기의 딥링크에서는 앱이 설치되어 있으면 딥링크를 통해 해당 컨텐츠로 정상적으로 이동할 수 있었습니다. 하지만 앱이 설치되어있지 않은 경우에는 에러 페이지나 대체 페이지를 보여줘야만 했습니다.

traditional-deep-link.png

이러한 문제를 해결하기 위해 나온 것이 아래의 deferred deep links입니다.

2. Deferred deep links(지연된 딥링크)

deferred(지연된) 딥링크는 앱을 다운받지 않은 사용자에게도 해당 링크의 컨텐츠로 바로 이동할 수 있도록 해줍니다. 웹에서 딥링크를 클릭했을 때 이미 앱을 다운받은 사용자의 경우에는 기존의 딥링크와 동일하게 바로 해당 컨텐츠로 이동합니다.

만약 앱이 설치되지 않은 경우라면 구글 플레이스토어나 앱스토어로 리다이렉트하여 이동한 뒤, 앱 다운로드가 완료되면 바로 해당 컨텐츠로 이동할 수 있도록 합니다. 위에서 언급했던 초기의 딥링크의 문제점을 개선한 것이라고 볼 수 있습니다.

deferred-deep-links.png

저는 지금 안드로이드 개발을 공부하고 있기때문에 여기서는 안드로이드 관련 딥링크에서만 설명하겠습니다. ios에서는 universal links(유니버셜 링크)라는 것을 지원하는 것 같습니다.

안드로이드 공식 문서를 보면 Deep Links(딥링크)라는 것이 있고 App Links(앱링크)라는 것이 있습니다. 친절하게도 이 두가지의 차이점에 대한 내용이 공식 문서에 있어 설명해보려합니다.

안드로이드에서의 Deep Links(딥링크)는 사용자가 안드로이드앱에서 특정 활동을 직접 지정할 수 있도록 하는 intent filter(인텐트 필터)입니다. 딥링크를 사용하면 사용자가 링크를 클릭했을 때 disambiguation dialog(명확성 대화상자라는 번역이 어색한 것 같아 영어로 쓰겠습니다)가 열리고, 사용자가 지정된 URL을 처리할 수 있는 여러 앱 중 하나를 선택할 수 있습니다.

예를 들어 아래의 그림1은 사용자가 지도 링크를 클릭한 후 지도앱 또는 크롬에서 링크를 열 것인지 묻는 disambiguation dialog를 보여줍니다.

그림1

그림1. The disambiguation dialog

App Links(앱링크)는 웹 사이트의 URL을 기반으로 하는 딥링크입니다. 따라서 앱링크 방식으로 된 딥링크를 클릭하면 disambiguation dialog가 나타나는 것이 아니라 앱이 즉시 열립니다. 만약 앱이 설치되어있지 않다면 해당 웹페이지로 이동합니다.

아래의 표에서 안드로이드에서의 딥링크와 앱링크 두 가지 방식의 차이점을 확인할 수 있습니다.

  Deep Links App Links
Intent URL scheme http, https, 커스텀 스키마 http와 https만 가능
Intent action 모두 가능 android.intent.action.VIEW만 가능
Intent category 모두 가능 android.intent.category.BROWSABLEandroid.intent.category.DEFAULT만 가능
Link verification 필요없음 Digital Asset Links 필요
User experience 사용자가 어떤 앱을 사용할 것인지 disambiguation dialog에서 선택할 수 있음 dialog없음. 해당 앱이 바로 열리거나 웹사이트로 이동함
Compatibility 모든 안드로이드 버전에서 가능 안드로이드 6.0 이상에서 가능

4. Dynamic Links(Firebase)

구글 파이어베이스에서도 다이나믹 링크(dynamic links)라는 서비스로 딥링크를 지원해줍니다.

안드로이드나 ios에서 직접 딥링크를 구현할떄와 파이어베이스의 다이나믹 링크를 사용할 때 어떤 점이 다른지 비교해보았습니다.

deep links 직접 구현 Firebase dynamic links
안드로이드/ios 링크를 따로 만들어야함 안드로이드/ios를 하나의 링크로 사용 가능
사용자에게 보이는 링크가 2개 사용자에게 보이는 링크가 1개
앱 설치여부를 확인하는 기능을 개발자가 직접 구현해야함 파이어베이스에서 해줌
개발자만이 링크를 생성할 수 있음 마케터나 기획자가 원하는 방식으로 직접 할 수 있음

지금까지 정리한바로는 다이나믹 링크가 가장 편리하고 유용해보입니다. 딥링크는 주로 외부에서 앱으로의 유입을 유도하기 위해 사용합니다. 때문에 광고를 집행하거나 이벤트 등을 하여 앱으로 들어오는 것을 유도할 때 많이 사용하는것 같은데, 링크를 매번 개발자가 생성하는 것이 아니라 기획자나 마케터가 직접 쉽게 생성할 수 있다는 것은 큰 장점이라고 생각합니다. 또한 파이어베이스에서 사용자의 유입을 추적하기에도 용이하지 않을까라는 생각이 듭니다. 제가 아직 사용해본적이 없기에 틀린 부분이 있다면 피드백 주시면 감사하겠습니다. 다이나믹 링크의 단점 혹은 문제점이 무엇인지 궁금하여 구글링을 해보았지만 제가 못찾는건지 찾을 수가 없었습니다.혹시 아시는 분은 알려주시면 감사하겠습니다.

다음 글에서는 파이어베이스 다이나믹 링크로 딥링크를 직접 구현해보며 다이나믹 링크의 특징에 대해 알아보겠습니다.

참고 자료

[Android] ConstraintLayout 톺아보기 (안드로이드 공식 문서 번역)

|

ConstraintLayout

제약 레이아웃은 android.view.ViewGroup에 속한 레이아웃이며 위젯의 위치(position)와 크기(size)를 지정할 수 있게 해줍니다.

현재 사용할 수 있는 제약(constraints)은 아래와 같습니다.

위의 개념들을 차례대로 살펴보겠습니다. 아래의 설명에서 대상 뷰/위젯이란 제약을 지정하는 뷰/위젯을 의미합니다.

Relative positioning

Relative positioning(상대 위치 지정)은 제약 레이아웃에서 가장 기초적인 개념입니다. 대상 위젯의 위치를 지정할 때, 다른 위젯으로부터의 상대적인 위치에 지정하는 것입니다. 예를 들면 아래 그림처럼 버튼B라는 위젯의 위치를 지정하기 위해서 버튼A의 오른쪽으로 버튼B를 포지셔닝 할 수 있습니다.

relative-positioning-example

위의 레이아웃의 코드는 아래와 같습니다. 다른 부분은 나중에 차차 살펴보고 일단 app:layout_constraint...라고 되어있는 부분만 살펴보도록 하겠습니다. app:layout_constraint...라고 되어 있는 부분을 보면 모두 app:layout_constraint... = "parent" 혹은 app:layout_constraint... = "@+id/아이디"로 parent 혹은 id가 할당되어 있는 것을 보실 수 있습니다. 이처럼 Relative positioning을 하기 위해서는 기준으로 잡을 수 있는 parent나 다른 위젯의 id가 항상 필요합니다.

이 예시에서는 버튼B의 시작점(start)을 버튼A의 끝(end)에 연결하였습니다. 이는 버튼B 코드의 app:layout_constraintStart_toEndOf="@+id/buttonA에 해당됩니다. 위젯을 화면에 띄우기 위해서는 상하좌우의 위치 제약(constraint)을 모두 지정해주어야 하는데, 이 예제에서는 모두 임의로 parent를 기준으로 하였습니다.

<Button
    android:id="@+id/buttonA"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="80dp"
    android:layout_marginLeft="80dp"
    android:layout_marginTop="328dp"
    android:text="버튼A"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

<Button
    android:id="@+id/buttonB"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="72dp"
    android:layout_marginLeft="72dp"
    android:layout_marginTop="328dp"
    android:text="버튼B"
    app:layout_constraintStart_toEndOf="@+id/buttonA"
    app:layout_constraintTop_toTopOf="parent" />

상대 위치 지정은 아래와 같이 수직/수평의 모든 방향으로 지정할 수 있습니다.

  • Horizontal Axis(수평): left, right, start and end sides(좌, 우, 시작과 끝면)
  • Vertical Axis(수직): top, bottom sides and text baseline(위, 아래, 텍스트 베이스라인)

relative-positioning-constraints

Horizontal Axis를 기준으로 위치를 지정할 때 아래의 1번과 2번 코드 모두 동일한 결과를 만들어냅니다. 어떤 것을 사용하는 것이 좋을까요?

1번
<Button ...
    app:layout_constraintLeft_toRightOf="@+id/buttonA" />
2번
<Button ...
    app:layout_constraintStart_toEndOf="@+id/buttonA" />

2번 코드처럼 start와 end를 사용하는 것이 권장된다고 합니다. 많은 국가는 LTR(Left to Right) 언어, 즉 왼쪽에서 오른쪽 방향으로 쓰고 읽는 언어를 사용하지만 아랍 등의 국가에서는 RTL(Right to Left) 언어를 사용합니다. 따라서 레이아웃 리소스가 LTR과 RTL을 모두 지원할 수 있도록 left/right 대신 start/end를 사용하는 것이 바람직하다고 합니다.

Relative positioning에서 사용 가능한 제약은 아래와 같습니다.

- layout_constraintLeft_toLeftOf
- layout_constraintLeft_toRightOf
- layout_constraintRight_toLeftOf
- layout_constraintRight_toRightOf
- layout_constraintTop_toTopOf
- layout_constraintTop_toBottomOf
- layout_constraintBottom_toTopOf
- layout_constraintBottom_toBottomOf
- layout_constraintBaseline_toBaselineOf
- layout_constraintStart_toEndOf
- layout_constraintStart_toStartOf
- layout_constraintEnd_toStartOf
- layout_constraintEnd_toEndOf

Margins

마진(margin)은 대상 위젯의 테두리 바깥의 여백 면적을 의미하며, 0이상으로만 지정될 수 있습니다. 마진의 크기를 지정하면 기존에 지정되어있던 제약(constraint)에 따라 기준면(source side)과 마진이 지정된 위젯 사이에 공간이 생깁니다. Relative positioning부분의 그림을 보시면 버튼A와 버튼B사이에 72dp만큼의 간격이 있습니다. 이는 코드에서 아래 부분에 해당합니다.

<Button ...
    android:layout_marginStart="72dp"
    android:layout_marginLeft="72dp"
    android:layout_marginTop="328dp" />

하나씩 살펴보면, 버튼B의 Start는 버튼A로부터 72dp만큼의 margin이 있습니다. 아래의 코드도 사실은 같은 의미로, 버튼B의 Left는 버튼A로부터 72dp만큼의 margin을 지정한 것입니다. RTL 지원을 생각하면 marginStart만 사용하는 것이 바람직하지만 API17 이전의 버전을 지원하기 위해서는 marginStart/marginEnd를 사용할 때에 marginLeft/Right도 같이 입력해야 에러가 발생하지 않습니다. 마지막으로, 버튼B의 Top은 parent로부터 328dp만큼의 margin이 있습니다.

margins에서 사용 가능한 제약은 아래와 같습니다.

- android:layout_marginStart
- android:layout_marginEnd
- android:layout_marginLeft
- android:layout_marginTop
- android:layout_marginRight
- android:layout_marginBottom

Margins when connected to a GONE widget

대상 위젯이 View.GONE에 속한 GONE 위젯이라면 기본 마진과는 다른 gone 속성을 사용한 마진을 지정할 수 있습니다. GONE 위젯에 사용 가능한 제약은 아래와 같습니다.

- layout_goneMarginStart
- layout_goneMarginEnd
- layout_goneMarginLeft
- layout_goneMarginTop
- layout_goneMarginRight
- layout_goneMarginBottom

gone margin에 대한 설명은 GONE 위젯에 대한 설명이 전제되어야하므로 아래의 Visibility behavior 부분에서 설명하도록 하겠습니다.

Centering positioning and bias

ConstraintLayout의 장점은 불가능해보이는 제약을 지정했을 때 알맞게 조절해준다는 것입니다. 우선 아래의 예시 코드를 보겠습니다.

<Button ...
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent" />

위의 코드같은 경우에는 버튼의 너비가 상위 레이아웃인 ConstraintLayout의 너비와 일치해야만 가능합니다. 이럴 때 안드로이드에서는 아래와 같이 대상 위젯을 parent의 정중앙에 위치시킵니다. 이는 수평/수직 제약 모두 해당됩니다.

centerint-positioning.png

Bias

대상 위젯의 양 측으로 제약을 줄 때 기본적으로는 정중앙에 위치하도록 포지셔닝됩니다. 이를 변경하고 싶다면 bias 속성을 사용하여 다르게 포지셔닝을 할 수 있습니다. bias를 사용하면 스크린의 크기 변경에 유연하게 대응할 수 있습니다.

- layout_constraintHorizontal_bias
- layout_constraintVertical_bias

bias-example.png

예를 들어 위의 그림처럼 왼쪽으로 치우치게 포지셔닝을 하고 싶다면 layout_constraintHorizontal_bias를 0.5이하로 설정하면 됩니다. 저는 아래 코드와 같이 0.2로 설정해보았습니다.

<Button ...
    app:layout_constraintHorizontal_bias="0.2" />

Circular Positioning

Circular Positioning(원형 위치 지정)이란 대상 위젯의 중간점으로부터 다른 위젯의 중간점까지의 각도와 거리를 지정하여 제약을 주는 것을 의미합니다. 즉 아래의 그림처럼 위젯의 중간점을 중심으로 원을 그렸을 때, 반지름의 길이만큼 떨어진 원 둘레의 한 지점에 다른 위젯을 포지셔닝 하는 것을 말합니다.

- layout_constraintCircle : 다른 위젯의 id를 지정합니다.
- layout_constraintCircleRadius : 대상 위젯에서 다른 위젯의 중간점까지의 거리를 지정합니다. 즉 대상 위젯을 둘러싼 원의 반지름을 지정하는 것을 의미합니다. 
- layout_constraintCircleAngle : 다른 위젯이 어떤 각도에 위치할지를 지정합니다.(범위: 0~360도)

아래의 왼쪽과 같이 버튼B의 제약을 지정하면, 오른쪽의 그림과 같은 결과가 나옵니다.

circular-positioning

코드로 자세히 보겠습니다. 버튼A의 id인 buttonA를 constraintCircle로 지정하였습니다. 각도는 60도로 하였고, 버튼A의 중점으로부터 100dp만큼의 거리에 버튼B의 중점이 위치하도록 하였습니다.

<Button ...
    app:layout_constraintCircle="@id/buttonA"
    app:layout_constraintCircleAngle="60"
    app:layout_constraintCircleRadius="100dp" />

Visibility behavior

View.GONE에 속한 GONE 위젯은 뷰에서 보여지지 않으며 자리를 차지하지도 않습니다. 하지만 대상 위젯에 적용된 면적은 그대로 유지됩니다. 이렇게만 보면 무슨 말인지 이해가 가지 않습니다. 지금부터 이것이 어떤 의미인지 찬찬히 설명해보겠습니다.

우선, GONE 위젯은 다음과 같은 속성을 가집니다.

- 레이아웃에서 GONE 위젯의 면적은 0으로 간주됩니다.
- 만약 GONE 위젯이 다른 위젯과 연결된 제약을 가지고 있다면 그 제약은 유지되지만, GONE 위젯의 마진은 0으로 유지됩니다. 

예를 들어 보겠습니다. 아래 왼쪽의 그림에서 버튼B는 버튼A의 end를 기준으로 80dp의 마진을 두고 있습니다. 그리고 버튼A는 parent의 start를 기준으로 70dp의 마진을 두고 있습니다. 여기서 버튼A의 visibility를 gone으로 처리하면 버튼A의 면적과 마진이 모두 0으로 간주됩니다. 따라서 오른쪽 그림처럼 버튼A는 보이지 않게 되고 버튼B만 보이게 되며, 버튼 A가 없어진 면적만큼 버튼B의 위치가 이동한 것을 보실 수 있습니다.

gone.png

이 때의 코드는 아래와 같습니다. 코드를 살펴보면, 버튼B에 걸려있는 버튼A에 대한 제약과 속성은 그대로 남아있는 것을 확인할 수 있습니다. 그리고 버튼A에 지정된 제약과 속성 또한 그대로 유지되어있습니다. 하지만 버튼A의 visibilitygone으로 지정했기때문에 레이아웃에서 버튼A가 차지하는 면적과 마진은 0으로 간주되고, 그래서 위의 그림과 같은 뷰를 만들 수 있게 되는 것입니다.

<Button
    android:id="@+id/buttonA"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="70dp"
    android:layout_marginLeft="70dp"
    android:layout_marginTop="320dp"
    android:text="버튼A"
    android:visibility="gone"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

<Button
    android:id="@+id/buttonB"
    ...
    android:text="버튼B"
    app:layout_constraintStart_toEndOf="@+id/buttonA"
    app:layout_constraintTop_toTopOf="parent" />

이러한 GONE 제약은 일시적으로 위젯을 사라지게 하고싶지만 레이아웃은 그대로 유지해야 할 때 유용하게 사용할 수 있습니다. 그런데 여기서 하나의 문제가 발생합니다. 버튼A를 GONE 위젯으로 만들어버리면 버튼B의 위치가 달라집니다. 버튼B의 위치도 그대로 유지하고싶다면 어떻게 해야 할까요? 이 떄 사용하는 것이 위에서 언급한 gone margin입니다.

Dimension constraints

아래는 dimension과 관련된 제약들입니다. dimension은 치수/면적/크기 등으로 번역될 수 있는데, 문맥에 맞게 혼용하여 사용하도록 하겠습니다.

Minimum dimensions on ConstraintLayout

아래의 제약을 사용하여 제약 레이아웃의 최솟값과 최대값을 지정할 수 있습니다.

- android:minWidth : 레이아웃의 최소 너비를 지정합니다
- android:minHeight : 레이아웃의 최소 높이를 지정합니다
- android:maxWidth : 레이아웃의 최대 너비를 지정합니다
- android:maxHeight : 레이아웃의 최대 높이를 지정합니다

제약 레이아웃의 크기가 WRAP_CONTENT로 지정되어있을 때 위의 속성을 사용할 수 있습니다.

Widgets dimension constraints

위젯의 크기는 아래와 같이 android:layout_width, android:layout_height의 속성을 사용하여 3가지의 방법으로 지정할 수 있습니다.

1. 특정 크기로 지정하는 방법
2. WRAP_CONTENT를 사용하는 방법
3. MATCH_CONSTRAINT를 사용하는 방법(width/height를 0dp로 지정하는 것을 의미합니다)

1번 방법은 말 그대로 대상 위젯의 width와 height의 크기를 직접 지정하거나 Dimension 리소스를 통해 지정하는 것을 의미합니다. 직접 크기를 지정할때에는 아래와 같이 할 수 있습니다.

<!--직접 값을 입력하여 크기를 지정하는 방법-->
<Button ...
    android:layout_width="100dp"
    android:layout_height="100dp" />

그럼 Dimension 리소스를 통해 지정한다는 것은 무엇일까요? 안드로이드에서는 이미지나 문자열과 같은 애플리케이선의 리소스를 독립적으로 관리할 수 있도록 지원합니다. dimension과 관련된 속성의 경우 YourProject/res/values/dimens.xml 경로의 파일에서 관리할 수 있습니다.(이 경로는 규칙이므로 꼭 지켜주셔야 적용이 됩니다.) 리소스에 관한 것이 더 궁금하시다면 안드로이드 공식문서에서 확인하실 수 있습니다.

주의: MATCH_PARENT 속성은 제약 레이아웃에 속한 위젯에는 사용하지 않는 것이 권장됩니다. 대신 parentleft/right 혹은 top/bottom 제약을 지정하여 MATCH_CONSTRAINT 속성을 사용할 수 있습니다.

WRAP_CONTENT : enforcing constraints

WRAP_CONTENT는 대상 위젯 내부의 컨텐츠 크기에 위젯의 크기를 자동으로 맞춥니다. 이 때 아래의 속성을 사용하여 대상 위젯의 크기에 제한을 둘 수 있습니다.

- app:layout_constrainedWidth=”true|false”
- app:layout_constrainedHeight=”true|false”

아래의 예시를 한 번 보겠습니다. TextView의 width를 wrap_content로 지정해놓았습니다. 그런데 TextView의 content 길이가 너무 길어 TextView의 width가 길어졌고, 그로 인해 화면에서 Button이 보이지 않습니다. 이 때 TextView에 app:layout_constrainedWidth=”true” 속성을 추가하면 width에 제한을 주면 위젯 안의 content 길이가 길어지더라도 위와 같이 레이아웃이 유지됩니다.

constraint-width.png

MATCH_CONSTRAINT dimensions

위젯의 크기를 MATCH_CONSTRAINT로 설정하면 기본적으로 해당 위젯이 차지할 수 있는 모든 면적을 다 차지하게 됩니다. 이 때 아래의 속성을 사용하여 위젯의 크기를 변경할 수 있습니다.

- layout_constraintWidth_min
- layout_constraintHeight_min
- layout_constraintWidth_max
- layout_constraintHeight_max
- layout_constraintWidth_percent
- layout_constraintHeight_percent
Min and Max

min과 max로 지정되는 값은 dp의 단위로 직접 크기를 지정할 수도 있고, WRAP_CONTENT와 동일한 결과를 내는 wrap을 사용하여 지정할 수도 있습니다.

Percent dimension

퍼센트를 사용하기 위해서는 아래의 설정이 필요합니다.

- 대상 위젯의 크기(dimension)는 MATCH_CONSTRAINT(0dp)로 지정되어야 합니다.
- app:layout_constraintWidth_default="percent" 혹은
  app:layout_constraintHeight_default="percent"를 사용하여 기본값을 퍼센트로 설정하여야 합니다.
- layout_constraintWidth_percent 혹은 layout_constraintHeight_percent 속성을 0과 1 사이의 값으로 지정해야 합니다.

Ratio

위젯의 치수(dimension) 대한 비율을 이용하여 다른 부분의 치수를 지정할 수도 있습니다. 비율을 사용하기 위해서는 최소한 하나의 치수(dimension)를 0dp(MATCH_CONSTRAINT)로 지정해야만 합니다. 그리고 layout_constraintDimensionRatio를 사용하여 비율을 지정하면 됩니다. 비율을 입력하는 방식에는 아래와 같이 두 가지의 방식이 있습니다.

- app:layout_constraintDimensionRatio="1:1" (width:height로 표현하는 방법)
- app:layout_constraintDimensionRatio="1.0" (width와 height의 비율을 float값으로 표현하는 방법)

아래의 코드에서는 width를 wrap_content로 지정하고 height를 0dp로 지정한 뒤 1:1의 비율을 입력하였습니다. 결과로는 버튼의 content 크기에 맞춰진 정사각형 버튼이 만들어집니다.

<Button ...
    android:layout_width="wrap_content"
    android:layout_height="0dp"
    app:layout_constraintDimensionRatio="1:1" />

width와 height이 모두 MATCH_CONSTRAINT(0dp)로 지정되어 있을 때에도 비율을 지정할 수 있습니다. 이 경우에는 아래와 같이 W, 혹은 H,를 비율 앞에 추가하여 어떤 쪽에 제약을 줄 것인지를 지정할 수 있습니다. 이 때 주의해야 할 점은 width와 height를 모두 0dp로 지정하는 것이기때문에 start/end, top/bottom에 제약이 걸려있어야 제대로 테스트할 수 있습니다.

- app:layout_constraintDimensionRatio = "H, x:y" (width를 constraint에 맞춰 설정한 뒤 비율에 따라 높이를 결정합니다)
- app:layout_constraintDimensionRatio = "W, x:y" (height를 constraint에 맞춰 설정한 뒤 비율에 따라 높이를 결정합니다)

아래의 예시를 통해 조금 더 자세히 설명해보겠습니다.

<Button ...
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintDimensionRatio="W, 1:3" />

우선 width와 height를 모두 0dp로 지정하였습니다. 마지막 라인의 app:layout_constraintDimensionRatio="W, 1:3"의 의미는 height를 먼저 constraint에 맞춰 3으로 설정한 후, width를 1로 설정하겠다는 것입니다. 위의 코드를 입력하면 아래와 같은 결과가 나옵니다. (코드에서는 생략했지만 start/end, top/bottom 제약을 모두 parent로 설정하였습니다.)

dimenstion-ratio-width

그럼 아래와 같이 H, 1:3으로 지정하면 어떻게 될까요?

<Button ...
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintDimensionRatio="H, 1:3" />

마지막 라인의 app:layout_constraintDimensionRatio="H, 1:3" 부분에 의해 width가 먼저 parent에 맞춰 1로 설정이 되고, 그 다음 height가 그에 대한 3의 비율로 설정됩니다. 따라서 아래 그림과 같이 위젯이 화면을 벗어나게됩니다.

dimenstion-ratio-height

Chains

체인을 사용하면 하나의 수평/수직 축(axis)을 기준으로 여러개의 위젯들을 그룹화하여 움직일 수 있습니다. 하나의 축을 기준으로 체인을 지정하더라도 다른 축에 대한 제약은 독립적으로 적용할 수 있습니다. 즉, 수평 체인을 지정하더라도 수직으로 별개의 제약을 지정할 수 있습니다.

Creating a chain

아래와 같이 위젯들이 양방향으로 연결되어있는 것을 가장 기본적인 형태의 체인으로 볼 수 있습니다.

chain

Chain heads

체인은 가장 첫 번째에 있는 요소인 헤드(head)의 속성에 의해 제어됩니다. 수평 체인에서는 가장 왼쪽에 있는 위젯이 헤드가 되고, 수직 체인에서는 가장 위쪽에 있는 위젯이 헤드가 됩니다.

chain

Margins in chains

체인 연결에 마진을 지정할수도 있습니다. spread chain의 경우 할당된 공간에서 마진의 크기만큼 여백이 줄어듭니다. 아래에서 자세히 설명해보겠습니다.

Chain Style

체인에 스타일을 주고 싶을 때에는 체인의 첫번째 요소를 layout_constraintHorizontal_chainStyle 혹은 layout_constraintVertical_chainStyle의 속성으로 지정하면 됩니다. 디폴트값은 CHAIN_SPREAD입니다.

- CHAIN_SPREAD : 기본 스타일입니다. 빈 공간이 양측으로 균등하게 분산됩니다. 
- Weighted chain : 각 요소들의 가중치에 따라 추가 공간을 확보하게 됩니다. layout_constraintHorizontal_weight 혹은 layout_constraintVertical_weight 속성을 이용하여 지정할 수 있습니다. 
- CHAIN_SPREAD_INSIDE : 체인의 안쪽으로만 간격을 두어 분산시킵니다. 
- CHAIN_PACKED : 체인의 각 요소들을 한 곳에 묶습니다. 자식 요소의 수직/수평 bias 속성은 CHAIN_PACKED로 묶인 요소들의 위치에 영향을 주게됩니다. 

chains-styles

Margins and chains

체인에서 마진은 합산되는 성질이 있습니다. 예를 들어, 수평 체인에서 한 요소의 오른쪽 마진을 10dp로 설정하고, 그 다음 요소의 왼쪽 마진을 5dp로 설정하면 두 요소간의 마진은 총 15dp가 됩니다. 체인의 요소들을 포지셔닝하기 위해 남은 공간을 계산할 때, 요소와 그 요소의 마진이 차지하는 공간을 합산하여 하나로 간주합니다.

Virtual Helpers objects

앞서 설명했던 내장 함수 이외에도 ConstraintLayout에 포함된 helper를 이용하여 레이아웃을 조정할 수 있습니다. Guideline 객체를 사용하면 ConstraintLayout을 기준으로 하는 수평/수직의 가이드라인을 만들 수 있으며, BarrierGroup 속성도 사용할 수 있습니다. 가이드라인(Guideline)과 배리어(Barrier)는 렌더링되어 뷰에 나타나는 요소는 아닙니다.(View.GONE의 gone 뷰로 표시됩니다) 주로 가이드라인에 위젯을 붙여 위젯의 위치를 쉽게 변경하기 위해 사용합니다.

Guideline

가이드라인(Guideline)은 수평/수직 모두 적용할 수 있습니다. 수평 가이드라인(horizontal guidelines)의 height는 0이고 width는 parent인 ConstraintLayout에 맞춰집니다. 수직 가이드라인(vertical guidelines)의 width는 0이고 height는 parent인 ConstraintLayout에 맞춰집니다.

가이드라인의 위치를 지정하기 위해서 아래의 세 가지 방법을 사용할 수 있습니다.

- layout_constraintGuide_begin : 레이아웃의 left 또는 top에서부터의 고정 거리를 지정합니다.
- layout_constraintGuide_end : 레이아웃의 right 혹은 bottom에서부터의 고정 거리를 지정합니다.
- layout_constraintGuide_percent : 레이아웃에서의 width 혹은 height의 퍼센트를 지정합니다. 

Barrier

배리어(Barrier)는 복수의 위젯을 참조하여 가상의 장벽(일종의 가이드라인)을 만듭니다. 배리어 제약이 지정된 위젯들은 위치가 변경될 때 배리어에 맞게 변경됩니다. 이 때 배리어와 가장 가까이 있는 위젯(공식문서에서는 “the most extreme widget on the specified side”라고 표현하고 있습니다.)을 기준으로 장벽을 만듭니다. 아래의 그림 1번을 보시면 ButtonA가 배리어와 가장 가까이 있을 때에는 ButtonA를 기준으로 배리어가 형성됩니다. 하지만 2번 그림처럼 ButtonB가 배리어 방향의 가장 극단으로 옮겨지면 ButtonB를 기준으로 배리어가 형성됩니다.

barrier.png

Group

그룹(Group) 헬퍼를 통해 복수의 위젯을 그룹화하여 아래와 같이 visibility 속성(visible | invisible | gone)을 동시에 제어할 수 있습니다.

<androidx.constraintlayout.widget.Group
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" 
    android:visibility="invisible"
    app:constraint_referenced_ids="button, button2, button3" />

위 코드의 android:visibility 부분에서 visibility 속성을 지정할 수 있으며, app:constraint_referenced_ids에서 그룹으로 지정하고자 하는 위젯의 아이디를 입력할 수 있습니다.

Optimizer

Constraint Layout 1.1버전부터는 레이아웃의 속도를 높이기 위해 최적화 방법을 제공합니다. ConstraintLayout 요소에 app:layout_optimizationLevel 태그를 추가하여 아래와 같이 최적화 레벨을 지정할 수 있습니다.

- none : 최적화 사용 안함
- standard : 디폴트 레벨
- direct : 고정된 요소에 연결된 제약 조건에 관련된 최적화
- barrier : barrier 제약 조건 관련 최적화
- chain : 체인 제약 조건 최적화(아직 테스트중인 기능)
- dimensions : 크기(dimensions) 측정 최적화(아직 테스트중인 기능)

참고자료

[TIL] 2019-08-06 (화)

|

Today I Learned

  • 앱 테스트시에는 해상도 다른 스마트폰 최소 3종류 이상 테스트한다.
  • match_parent는 constraintLayout 내의 위젯/레이아웃에는 최대한 쓰지 않는다. 제약을 줄 때에는 match_constraint(0dp)를 기본적으로 주는 것이 맞다.
  • 버튼 위젯을 사용할 때 그림자를 주의하자. background를 커스텀 파일로 변경하더라도 디폴트로 그림자가 들어가게 된다. 그림자를 없애려면 xml파일의 버튼 위젯 태그 안에 style="@style/Widget.AppCompat.Button.Borderless"를 추가하면 된다.
  • 위젯/레이아웃 간격이 원하는 대로 나오지 않을 때에는 임시로 background 색상을 지정하면 각 위젯/레이아웃이 차지하는 영역을 한 눈에 볼 수 있다.
  • textView간의 간격이 원하는 대로 나오지 않아 2시간 정도 고생했던 것 같다. 원인은 font에 포함된 여백이었다. background 색상을 임시로 지정하여 영역을 파악했다면 금방 찾을 수 있었을텐데 그 생각을 하지 못해서 시간을 많이 소모했다.
  • font에 포함된 여백을 제거하는 방법은 xml에서 해당 위젯 태그 안에 android:includeFontPadding="false"를 추가하면 된다. textView를 사용할때에는 항상 font 자체의 여백과 lineHeight를 조심하자.
  • git에서 tig를 사용하면 기초적인 명령어만으로도 요약된 커밋로그, 커밋 내용 등을 다양한 뷰를 이용하여 볼 수 있다. tig 개발자의 블로그강성진님의 tig 매뉴얼 번역를 참고하자.

[TIL] 2019-08-05 (월)

|

Today I Learned

  • 앱 레이아웃을 만들 때 최대한 레이아웃간의 중첩을 줄이자
  • 미션을 받았을 때 기존의 코드를 보고 어떻게 구성되어있는지를 꼭 파악하자
  • 에러가 났을 떄에는 Logcat을 확인

To Do

  • 마보 스토어 화면 완성
  • 마보 콘텐츠 리스트 정리
  • 딥링크 자료 정리 & 샘플 앱 만들기
  • 2019-08-01 채팅 앱 수업(RecyclerView/context/ViewType/Layout Manager) 개념 정리 & 앱 만들기

개인적으로 할 것

  • CostraintLayout 정리한 것에 부족한 설명 추가(Optimizer, 예시 없는것 등등)
  • TextView의 gravity 개념 정리

[TIL] 2019-08-02 (금)

|

Today I Learned

  • 안드로이드의 ConstraintLayout에 대하여 공부하고 마크다운으로 정리했다. 아직은 지식들이 파편화되어 머릿속에서 돌아다니는 느낌이다. 정리한 것을 보고 외우는 과정을 통해 구조화시킬 수 있을 것 같다.
  • ScrollView는 단 하나의 직계 child만을 가질 수 있다.
  • ScrollView에 RecyclerView를 넣으면 안된다. RecyclerView는 화면 크기만큼의 뷰만 생성하여 화면에 보여지는 뷰를 계속 재사용하는 것에 그 의미가 있다. 그런데 ScrollView에 RecyclerView를 넣게 되면 데이터 개수만큼의 뷰가 모두 새로 생성된다. 공식 문서에서는 절대 ScrollView 내부에 RecyclerView나 ListView를 넣지 말라고 안내하고 있다.
  • 모바일 앱 혹은 프론트 개발을 할 때에는 기기별 해상도를 지원하기 위해 이미지 리소스 파일을 해상도별로(mdpi, hdpi 등등) 관리해야한다. 안드로이드에서는 리소스(res) 디렉토리의 drawable로 관리할 수 있다.
  • 뷰의 중첩은 앱의 성능을 떨어트리는 주요 요소이다. 따라서 모바일 앱 개발을 할 때에는 뷰의 중첩을 줄이는 것이 중요하다. (요즘은 스마트폰의 성능이 좋아져서 뷰 중첩으로 인한 성능저하가 예전보다 많이 줄었다고 한다.)

To Do

  • 마보 스토어 화면 완성
  • 딥링크 자료 정리
  • 딥링크 샘플 앱 만들기
  • CostraintLayout 정리한 것에 부족한 설명 추가(Optimizer, 예시 없는것 등등)
  • TextView의 gravity 개념 정리
  • 2019-08-01 채팅 앱 수업(RecyclerView/context/ViewType/Layout Manager) 개념 정리 & 앱 만들기