사전 contentDescription
인터페이스

contentDescription

gabury1고친 사람 github-actions[bot]

contentDescription 은 Android 앱 화면의 아이콘과 그림에 소리로 읽힐 설명을 붙여 줍니다. 화면을 못 보는 사용자는 이 설명을 듣고 그 부품이 무엇인지 압니다. 설명은 화면에 보이지 않습니다. 화면을 소리로 읽어 주는 프로그램만 이 설명을 읽습니다.

쉽고 빠른 이해

contentDescription 은 앱의 부품에 귀로 듣는 이름을 붙입니다. 공유 아이콘만 그려진 버튼에 「공유」라는 설명을 다는 식입니다.

화면을 못 보는 사용자는 아이콘 모양을 알 수 없습니다. 설명이 없으면 무엇을 하는 버튼인지 모릅니다.

  1. 개발자가 버튼에 「공유」라는 설명을 적습니다
  2. 사용자가 손가락으로 화면을 쓸어 그 버튼에 닿습니다
  3. 화면을 소리로 읽어 주는 프로그램이 「공유, 버튼」이라고 읽습니다

설명이 있으면 이 프로그램은 부품에 적힌 글자 대신 설명을 읽습니다. 그런데 설명은 눈에 안 보입니다. 화면 글자를 바꾸고 설명을 안 바꿔도 만든 사람이 알아채기 어렵습니다. 그래서 글자가 이미 적힌 부품에는 붙이지 않습니다.

상세

공유 아이콘 하나만 든 버튼을 예로 끝까지 씁니다. 먼저 이 버튼이 화면을 못 보는 사용자에게 어떻게 전해지는지 봅니다. 그다음 contentDescription 을 붙이는 방법과 읽을 말이 정해지는 순서를 따라갑니다.

뒤쪽에서는 붙이면 안 되는 부품과 설명을 비워 두는 그림, 값을 적는 법을 봅니다. 끝으로 빠뜨렸는지 확인하는 법과 웹의 같은 수단을 보고 언제 쓰는지 가립니다.

아이콘만 든 버튼

스크린 리더는 화면의 글자와 부품을 소리로 읽어 주는 프로그램입니다. 눈이 안 보이는 사용자가 이것으로 앱을 씁니다. Android 에 기본으로 들어 있는 스크린 리더가 TalkBack 입니다.

TalkBack 을 켜면 사용자는 화면을 손가락으로 쓸어 부품 사이를 옮겨 다닙니다. 부품에 닿을 때마다 TalkBack 이 그 부품의 이름과 종류를 읽습니다. 「저장, 버튼」처럼 이름을 먼저 읽습니다. 종류는 그 뒤에 읽습니다.

버튼의 이름은 대개 버튼에 적힌 글자에서 옵니다. 「저장」이라고 적힌 버튼은 「저장, 버튼」으로 읽힙니다. 그런데 공유 아이콘만 그려진 버튼에는 읽을 글자가 없습니다.

이런 버튼은 이름 없이 종류만 읽힙니다. 사용자는 이것이 공유인지 삭제인지 알 수 없습니다. contentDescription 은 이 빈 이름을 채우는 값입니다. 버튼에 「공유」를 적어 두면 「공유, 버튼」으로 읽힙니다.

View 와 Compose 에서 붙이는 법

Android 앱의 화면은 두 방식으로 만듭니다. 오래된 방식은 View 라는 화면 부품을 XML(Extensible Markup Language) 파일에 늘어놓습니다. 새 방식인 Jetpack Compose 는 Kotlin 함수로 화면을 그립니다. contentDescription 은 두 방식 모두에 있습니다.

View 방식에서는 XML 의 부품 태그에 속성으로 적습니다. 아래는 공유 아이콘을 띄우는 ImageButton 입니다.

XML
<ImageButton
    android:src="@drawable/ic_share"
    android:contentDescription="@string/share" />

android:src 는 띄울 아이콘 그림입니다. android:contentDescription 이 읽힐 설명입니다. 값의 @string/share 는 문자열 리소스에서 share 라는 이름의 글을 꺼내 온다는 뜻입니다. 문자열 리소스는 화면에 나올 글을 코드 밖 파일에 모아 둔 것입니다. 왜 이렇게 하는지는 아래 「문자열 리소스로 적는 까닭」에서 봅니다.

같은 값을 코드에서 넣을 수도 있습니다. View 에는 setContentDescription() 메서드가 있습니다. Kotlin 에서는 이 메서드를 속성에 값을 넣듯 부릅니다.

Kotlin
shareButton.contentDescription = "공유"

shareButton 은 위 버튼을 가리키는 변수입니다. 화면 상태에 따라 설명을 바꿀 때 이 방법을 씁니다. 재생 버튼이 눌린 뒤 「일시 정지」로 바뀌는 경우가 그렇습니다.

Compose 에서는 그림을 띄우는 함수 Icon 과 Image 가 contentDescription 을 인자로 받습니다. 이 인자는 생략할 수 없어서 그림을 넣을 때마다 적어야 합니다. 그림을 넣는 개발자가 설명을 잊지 않게 하려는 것입니다.

Kotlin
Icon(
    Icons.Filled.Share,
    contentDescription = "공유"
)

Icons.Filled.Share 는 Compose 가 기본으로 주는 공유 아이콘입니다. 문자열 리소스를 안 쓰고 「공유」를 직접 적은 것은 읽기 쉽게 줄인 것입니다.

Compose 는 화면 부품마다 시맨틱스라는 정보 묶음을 따로 들고 있습니다. 부품의 이름과 종류처럼 TalkBack 이 읽을 정보가 이 묶음에 담깁니다. TalkBack 과 테스트 코드가 이 묶음을 읽습니다.

Icon 을 안 쓰고 그래프를 직접 그리는 부품에는 이 묶음에 설명을 손으로 넣어야 합니다. 이때 Modifier 를 씁니다. Modifier 는 Compose 부품에 크기 · 여백 · 클릭 처리 같은 성질을 덧붙이는 값입니다. 대부분의 부품 함수가 이 값을 modifier 라는 인자로 받습니다.

Kotlin
Modifier.semantics {
    contentDescription = "걸음 수 그래프"
}

Modifier.semantics { … } 는 중괄호 안의 값을 그 부품의 시맨틱스에 넣는 Modifier 입니다. 이것을 그래프 부품의 modifier 인자로 넘기면 TalkBack 이 그 부품을 「걸음 수 그래프」로 읽습니다.

붙이는 방법 넷을 화면 방식별로 나란히 놓으면 이렇습니다.

화면 방식 붙이는 곳 적는 꼴
View · XML 부품 태그 android:contentDescription="…"
View · 코드 부품 객체 shareButton.contentDescription = "…"
Compose Icon · Image 의 인자 contentDescription = "…"
Compose 그 밖의 부품 Modifier.semantics { … }

읽을 말이 정해지는 순서

TalkBack 은 부품의 이름을 정할 때 contentDescription 을 먼저 봅니다. 값이 있으면 그 값을 읽습니다. 부품에 적힌 글자는 읽지 않습니다. 값이 없을 때만 적힌 글자로 내려갑니다.

flowchart TD
    A["contentDescription 이 있나"] -->|있다| B["그 값을 읽는다"]
    A -->|없다| C["부품에 적힌 글자가 있나"]
    C -->|있다| D["그 글자를 읽는다"]
    C -->|없다| E["종류만 읽는다"]

글자가 적힌 부품에 붙이면 어떻게 되는지 봅니다. 아래 title 은 글자를 띄우는 부품입니다. 각 줄 오른쪽 주석은 그 줄까지 실행했을 때 TalkBack 이 읽는 말입니다.

Kotlin
title.text = "저장"               // 저장
title.contentDescription = "등록" // 등록

첫 줄만 있으면 화면에 보이는 「저장」이 읽힙니다. 둘째 줄을 더하면 화면에는 여전히 「저장」이 보입니다. TalkBack 은 contentDescription 이 앞서므로 「등록」을 읽습니다.

글자가 적힌 부품에는 붙이지 않는다

화면에 글자가 적힌 버튼은 그 글자가 이미 이름입니다. 여기에 contentDescription 을 붙이면 앞의 예처럼 글자를 덮어씁니다. 덮어쓴 값은 눈에 안 보이니 화면 글자와 어긋나도 알아채기 어렵습니다.

화면 글자를 「등록」에서 「저장」으로 바꾸고 설명을 안 바꾸면 앞의 예가 됩니다. 보는 사람에게는 「저장」, 듣는 사람에게는 「등록」인 버튼이 생깁니다.

음성 제어를 쓰는 사용자도 막힐 수 있습니다. 음성 제어는 손 대신 말로 화면을 누르게 하는 기능입니다. 사용자는 화면에 보이는 글자를 말합니다. 이름이 그 글자와 다르면 명령이 안 먹을 수 있습니다.

글자를 넣는 입력 칸도 따로 짚어 둡니다. Android 의 입력 칸 부품인 EditText 에는 contentDescription 대신 android:hint 로 힌트를 적습니다. 힌트는 칸이 비어 있을 때 흐리게 보이는 안내 글입니다. 눈으로 보는 사람과 듣는 사람이 같은 안내를 받습니다.

꾸밈 그림은 설명을 비운다

모든 그림에 설명이 필요한 것은 아닙니다. 배경 무늬나 글 옆에 붙은 꾸밈 그림은 뜻을 전하지 않습니다. 이런 그림까지 읽으면 사용자는 쓸모없는 말을 듣느라 원하는 부품에 늦게 닿습니다.

Compose 에서는 contentDescription 에 null 을 넣습니다. 그러면 TalkBack 이 그 그림을 건너뜁니다.

Kotlin
Image(
    painterResource(R.drawable.bg),
    contentDescription = null
)

R.drawable.bg 는 앱에 넣어 둔 배경 그림 파일을 가리킵니다. 위 코드는 그 그림을 띄우되 설명은 없다고 밝힙니다.

View 방식에서 그림만 띄우는 부품은 ImageView 입니다. ImageButton 과 달리 누를 수 없습니다. 그림을 보여 주기만 합니다.

꾸밈 그림이면 ImageView 에 android:contentDescription="@null" 을 적습니다. @null 은 Compose 의 null 과 같은 뜻입니다. 설명을 일부러 비웠다고 밝힙니다. 그러면 TalkBack 이 그 그림을 건너뜁니다.

android:importantForAccessibility="no" 는 같은 일을 더 직접 하는 속성입니다. 설명 값과 상관없이 TalkBack 이 그 부품을 건너뛰게 합니다. 두 방법 모두 결과는 같습니다. 그림이 TalkBack 에 읽히지 않습니다.

문자열 리소스로 적는 까닭

문자열 리소스는 언어마다 파일을 하나씩 둡니다. 한국어 파일에는 share 가 「공유」로, 영어 파일에는 「Share」로 적힙니다. 앱은 기기 언어에 맞는 파일에서 글을 꺼냅니다.

설명 글도 화면 글자처럼 번역되어야 합니다. 코드에 「공유」를 직접 박으면 영어로 쓰는 사용자도 한국어 「공유」를 듣습니다. 문자열 리소스로 적어야 그 사용자는 「Share」를 듣습니다.

Compose 에서는 stringResource(R.string.share) 로 같은 글을 꺼냅니다. 앞의 Icon 예에서 「공유」 대신 이것을 넣으면 됩니다.

값을 적는 법

설명에는 그 부품이 무엇인지나 누르면 무엇을 하는지를 짧게 적습니다. 「공유」 · 「뒤로 가기」 · 「검색」처럼 한두 낱말이면 됩니다.

종류를 뜻하는 말은 넣지 않습니다. TalkBack 이 이름 뒤에 종류를 붙여 읽기 때문입니다. 「공유 버튼」이라고 적으면 「공유 버튼, 버튼」으로 읽힙니다. 「아이콘」 · 「이미지」 같은 말도 같은 까닭으로 뺍니다.

목록 안에서 같은 버튼이 되풀이되면 무엇에 붙은 버튼인지 넣습니다. 사진 목록의 줄마다 삭제 버튼이 있으면 다섯 개 모두 「삭제」로 읽힙니다. 「여름 사진 삭제」처럼 그 줄의 이름을 넣어야 구분됩니다.

버튼 안에 든 아이콘

Compose 에서는 아이콘을 버튼 함수 안에 넣는 일이 많습니다. 이때 아이콘의 설명이 버튼의 이름이 됩니다. Compose 가 버튼 안 부품들의 시맨틱스를 버튼 하나로 합쳐서 TalkBack 에 넘기기 때문입니다.

아래 첫 줄 오른쪽 주석은 TalkBack 이 이 버튼에서 읽는 말입니다.

Kotlin
IconButton(onClick = share) { // 공유, 버튼
    Icon(Icons.Filled.Share, "공유")
}

onClick 에 넘긴 share 는 누르면 실행할 공유 동작입니다. 앞의 shareButton 처럼 버튼을 가리키는 변수가 아닙니다. Icon 의 둘째 인자 「공유」가 contentDescription 입니다. TalkBack 은 버튼과 아이콘을 따로 읽지 않고 한 번에 「공유, 버튼」으로 읽습니다. 그래서 IconButton 에 설명을 또 붙이지 않습니다.

빠뜨렸는지 확인하기

설명은 화면에 안 보이니 눈으로 확인할 수 없습니다. 그래서 도구가 대신 찾아 줍니다.

Android Lint 는 앱을 실행하지 않고 코드만 읽어 흔한 실수를 찾는 검사 도구입니다. 설명이 없는 ImageView 와 ImageButton 에 경고를 띄웁니다.

꾸밈 그림에 경고가 뜨면 앞에서 본 "@null" 을 적습니다. Lint 는 이것을 일부러 비운 설명으로 보고 경고를 띄우지 않습니다.

Accessibility Scanner 는 기기에서 앱 화면을 훑어 접근성 문제를 알려 주는 앱입니다. 이름이 없는 부품도 이 앱이 표시해 줍니다.

Compose 테스트 코드는 설명으로 부품을 찾을 수 있습니다. composeRule 은 테스트에서 Compose 화면을 띄우고 조작하는 도우미 객체입니다. 아래 코드는 설명이 「공유」인 부품을 찾아 누릅니다.

Kotlin
composeRule
    .onNodeWithContentDescription("공유")
    .performClick()

설명이 빠졌거나 값이 다르면 부품을 못 찾아 테스트가 실패합니다. TalkBack 이 못 알아볼 버튼을 테스트가 먼저 잡아 주는 셈입니다.

웹의 같은 수단

웹 페이지에도 같은 일을 하는 속성이 있습니다. 웹 페이지는 HTML(HyperText Markup Language)로 씁니다. 아래 둘이 HTML 태그에 붙는 속성입니다.

수단 플랫폼 붙이는 곳
contentDescription Android 아이콘 · 그림 · 글자 없는 부품
alt 속성 웹 <img> 그림 태그
aria-label 웹 버튼 · 링크처럼 글자 없는 부품

셋 다 대체 텍스트를 적는 수단입니다. 대체 텍스트는 그림을 볼 수 없는 사람에게 그 그림의 뜻을 글로 전하는 것입니다. 셋 모두 평소에는 화면에 안 보입니다. 스크린 리더만 이 글을 읽습니다.

쓰는 때와 안 쓰는 때

상황마다 할 일을 아래 표로 가립니다. 위에서부터 보고 맞는 줄에서 멈춥니다.

상황 하는 일
부품에 이름이 될 글자가 적혀 있다 그 글자를 쓴다. contentDescription 을 안 붙인다
글자를 넣는 입력 칸 android:hint 로 힌트를 적는다
뜻을 전하지 않는 꾸밈 그림 Compose 는 null, View 는 "@null" 을 적는다
목록마다 되풀이되는 버튼 누르면 하는 일에 그 줄의 이름을 넣어 적는다
아이콘만 든 버튼 누르면 하는 일을 contentDescription 에 적는다
뜻을 전하는 그림 · 그래프 그림이 전하는 내용을 contentDescription 에 적는다

관련 항목

contentDescription 이 속하는 상위 분류

접근성 · 모바일 접근성 · Android 접근성 · Android · 대체 텍스트

contentDescription 을 읽어 전하는 보조 기술

보조 기술 · 스크린 리더 · TalkBack · 음성 제어 · Voice Access · 스위치 액세스 · 접근성 서비스

contentDescription 을 받는 화면 부품

뷰 · ImageView · ImageButton · IconButton · 아이콘 버튼 · EditText

contentDescription 을 붙이는 화면 개발 도구

Jetpack Compose · Compose 시맨틱스 · Modifier · XML 레이아웃 · XML · Kotlin · 문자열 리소스

contentDescription 과 함께 쓰는 Android 접근성 속성

importantForAccessibility · 힌트 · labelFor · stateDescription · 접근성 포커스

contentDescription 과 같은 일을 하는 웹 접근성 수단

alt 속성 · aria-label · aria-labelledby · ARIA · 접근 가능한 이름 · HTML

설명이 빠졌는지 찾는 도구

Android Lint · Accessibility Scanner · Compose UI 테스트 · Espresso · 접근성 테스트

다른 이름: content description · android:contentDescription · setContentDescription · 콘텐츠 설명