모두를 위한 앱 개발 스토리

2019년, 배달의민족 앱서비스팀은 ready… set… 마침내… GO! Accessibili—–ty! 했던 한 해였습니다. 서비스 초기부터 접근성 개선을 위해 조금씩 작업했고, 그 노하우가 쌓이고 구성원들의 열정이 더해져 올해는 이전보다 추진력 있게 접근성을 개선하고자 노력했습니다.


진행 과정만으로 의미 있는 일이지만, 그 중에도 WOOWA!😍 (brothers?) 감탄사가 나올 결과물도 있었는데요. 2020년 1월 30일 구글 플레이 스토어에 배달의민족이 접근성 개선 사례로 Featured 되었습니다. 🎉🎉🎉🎉🎉


해당 영상은 전세계에 우리 회사가 어떤 회사인지 알리고, 배달의민족의 앱서비스를 담당하는 저희 팀이 구글코리아와의 긴밀한 협업을 기반으로 접근성 진단이나 워크샵 등과 같은 다양한 방법을 통해 꾸준히 앱의 접근성을 개선하려 노력한 모습을 담아낼 수 있었습니다.

누구나 사용할 수 있는 앱에 대한 지칭 중 하나가 바로 ‘접근성(Accessibility)’

구글의 미션인 to organize the world’s information and make it universally accessible and useful에서도 알 수 있는 것 처럼 무언가를 universally accessible 하게 만든다는 것은 다양한 사람들이 제한이나 어려움 없이 서비스를 사용하길 바라는 마음이고, 서비스를 담당하는 사람이라면 간절히 바라는 바일 것 같습니다.

배달의민족은 구글이 만든 플랫폼에 올리는 앱이기도 하고, 국민 배달 앱이기도 하면서 먹는 것과 관련된 니즈는 boundaries 없이 모두가 원하는 부분이기 때문에 접근성을 보장하는 것에 대한 노력이 필요했지요.


접근성 개선에 대해 가장 잘 알려진 기술은 스크린 리더나 화면 확대같은 시각적 지원기능입니다. 모바일 기기들은 대부분의 정보를 시각적 요소로 전달하고 있는데, 시각적 어려움이 있으신 분들은 그 정보를 접할 방법이 없으니 시각적 요소를 청각적 요소를 전환해 전달하는 것이지요. 이런 지원이 없으면 정보의 습득이 힘든 상황이 될 수 있다보니, 접근성 하면 시각적 지원 기능이 먼저 회자되고 있는 것 같습니다.


오늘은 안드로이드 배달의민족이 접근성 개선을 진행할 때 중점을 두었던
대체텍스트 적용과 포커스 단위에 대한 노하우와 경험을 공유드리고자 합니다.
노하우는 Android Developers Accessiblity Guilde와 팀원들의 경험과 응용력을 기반으로 만들어졌습니다.
(오늘은?.. 다음에 또 다른내용으로 찾아오겠다는 예고…? 🙈)


시작은 대체텍스트




Android Studio의 Inspect Code 기능을 사용해보신 분이라면 익숙하신 화면일 텐데요.

안드로이드 code inspection를 보면 Android - Lint - Accessiblity내용이 기본적으로 들어가 있습니다. Android Lint Checks에서 볼 수 있는 것처럼 Lint checks 에선 Accessibility 관련해서 총 5가지의 inspection result를 주고있으며 그중 3가지가 대체텍스트 관련된 내용입니다.

이처럼 접근성을 충족시키고자 할 때 무엇보다 가장 필요한 것은 화면을 자유롭게 보실 수 없는 분들 앱의 내용을 쉽게 파악할 수 있도록 도와드리는 것입니다. 위에 말씀드렸던 것 처럼 많은 정보들이 시각적 요소들을 통해서 전달되곤 하는데 그런 정보들을 시각적으로 접하지 못하는 분들을 위해 다른 형태로 제공하는 것이 접근성의 가장 기본적인 단계라고 할 수 있고, 그렇기 때문에 Android - Lint check도 이 부분을 가장 기본적으로 제공하지 않나 추측해봅니다.


기본적인 대체텍스트

<ImageView
    android:id="@+id/userPhotoImageView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:contentDescription="@string/cd_profile_image_change"
    android:src="@drawable/bg_user_thumbnail_pressed_selector" />
userPhotoImageView.setContentDescription(R.string.cd_profile_image_change)


대체텍스트는 XML에서도 설정할 수 있고 코드 내 뷰에 접근해 programmatically 하게도 추가할 수 있죠. 뷰에 대한 추가적인 로직이 없는 단순한 경우에 XML을 통한 설정을 주로 하게 됩니다.

대체텍스트를 붙여줄 때 기획자나 서비스 운영자의 도움이 있다면 골치가 덜 아플테지만, 실제로는 그렇게 진행되기 쉽지 않습니다. 바쁘게 과제를 진행하고 그에 맞게 매 Feature가 진행될때 대체텍스트까지 함께 정의하는… 날이 언젠가 올테지만요 🙂
그래서 contentDescription을 넣는 일은 개발자의 순발력과 표현력으로 완성될 때도 있습니다. 그 때 사용할 수 있는 팁들은 다음과 같습니다.

1. 눈으로 읽히는 모든 정보는 음성으로 읽어줄 수 있는 대체텍스트를 가지고 있어야 합니다.

2.Button과 Click Listener가 붙은 ImageView는 시스템에서 OO”버튼”이라고 읽어주기 때문에
ContentsDescription에 그야말로 description만 적절히 내용을 넣어주면 됩니다.

3.그 외 뷰 속성을 클릭했을 때 무언가 액션이 이뤄지는 경우라면
명시적으로 “버튼”이나 추가적으로 설명할 수 있는 문구를 추가합니다.


예를 들면 TextView에 밑줄이 그어져있고, 누르면 무언가 동작을 하는 경우 텍스트 내용 자체만 읽어주면 해당 뷰를 눌러 무언가 작동될 것이라는 것에 대한 유추가 어렵지요. 그런 경우 보통 내용 + “버튼”으로 만든 대체텍스트를 만들어주면 누르면 실행되는 구나~ 라고 인지하실 수 있게 제공할 수 있습니다.





커스텀 뷰로 만든 버튼과 같은 동작형태를 지닌 뷰에 경우에도 같은 예입니다.

배달의민족 메인 화면 상단에는 주소 설정하는 기능이 있는데 화면에 보이는대로 강남구청만 읽어준다면 주소 설정 기능에 대해 전달할 수 없으니 정보 손실이 일어나게 됩니다. “강남구청 버튼”으로 읽어준다면 실행해서 주소 변경을 할 수 있다는 내용까지 간접적으로 전달할 수 있습니다.

실행된다고 문구 뒤에 버튼이라고 읽어주는 것만으로… 그 기능을 정말 알 수 있겠어? 라는 고민이 시작될 수 있는데요.
이런 고민이 드신다면 AE가 되신겁니다! Accessibility Expert! 🙂


더 상세하게 안내할 수 있어요



배너뷰에 대해 기본 action label을 읽는 Google PlayStore App의 모습입니다. 위와 같은 안내를 API 22부터 제공하는 AccessibilityNodeInfoCompat을 이용하면 추가 설명을 쉽게 전할 수 있습니다. 배달의민족 안드로이드 코드에선 접근성 Util 클래스에 TalkBack과 같은 스크린리더가 켜져있는 지 확인하거나 추가 설명을 붙이는 것과 같은 다양한 메소드들을 모아 사용하고 있습니다.

fun setAccessibilityClickActionHintScript(view: View, hintScript: String?) {
    if (isTalkbackOn) {
        view.setAccessibilityDelegate(object : View.AccessibilityDelegate() {
            override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) {
                super.onInitializeAccessibilityNodeInfo(host, info)
                val infoCompat = AccessibilityNodeInfoCompat.wrap(info)
                val clickAction = AccessibilityActionCompat(AccessibilityNodeInfoCompat.ACTION_CLICK, hintScript)
                infoCompat.addAction(clickAction)
            }
        })
    }
}

기본적으로 눌러서 실행 가능한것들에 대해 “활성화하려면 두 번 탭하세요”라고 읽어주고 있는데요. 아래 코드를 통해 “활성화하려면” 이라는 hintScript를 원하는 내용으로 변경하는 것이죠. 배달의민족을 예로 들면 장바구니에서 맨 윗부분에 가게 이름이 나오게 되는데 가게이름 버튼이라고만 읽어주면 그 상황에서 어떻게 된다는건지 개인마다 유추하는 기능이 다를 수 있습니다. 그래서 “가게 화면으로 이동하려면 두번 탭하세요”라는 문구를 통해 정확히 가게 화면으로 이동한다는 것을 전달하고 있습니다.

메인의 현재 주소 + 버튼에 대해서도 좀 더 설명을 덧붙이면 좋지 않겠어 라는 생각과 내가 배달받을 주소에 버튼이라고 읽어주면 그것으로 충분하지 라는 생각이 충돌하게 되는데요. 접근성 대체텍스트를 작성하며 더욱 자세한 내용의 대체텍스트와 간결한 대체텍스트 사이에서 갈등하게 되는일은 꽤나 많답니다. 이런 경우엔 내가 설정한 주소에 버튼이라고 붙으면 주소 설정과 관련된 내용이라는 것이 충분히 전달될 것이다 라는 생각으로 간결한 대체텍스트를 설정했었던 기억입니다 🙂




각 화면에 대한 추가설명 뿐 아니라 일어난 일에 대한 추가설명은 꽤나 중요하고 꼭 필요한 편인데요.

써브웨이 잠실역에서 점심시키려고 하는데 누군가가 옆에서 나도 똑같은거 하나 더! 했을 때… 간단히 수량추가를 하곤 하죠. 이런 경우 수량 추가 버튼을 눌렀을 때… 로직적으로 수량만 증가한 것과 수량이 추가되었다고 사용자에게 피드백을 주는 것은 꽤나 다른 결과를 가져옵니다. 시각장애 사용자가 수량이 추가된지 모르는 상황으로 그대로 주문해버리는 상황이 발생할 수도 있습니다.

시각적 어려움이 없다면 숫자가 2로 바뀌었기 때문에 바로 파악할 수 있지만, 시각적 어려움이 있는 분들은 이게 추가돼서 2가 된 건지 혹시 추가가 두 번 눌려 3이 된 건 아닌지 확인하려면 일일이 또 해당 위치까지 이동해 텍스트가 2인지 확인해야 하는 과정이 필요하게 됩니다. 하지만 시각적 변화에 대해 안내해준다면 그와 같은 불편함은 줄어들게 됩니다.


fun sendAccessibilityEvent(text: String?) {
    if (isTalkbackOn) {
        val e = AccessibilityEvent.obtain()
        e.eventType = AccessibilityEvent.TYPE_ANNOUNCEMENT
        e.className = context.javaClass.getName()
        e.packageName = context.packageName
        e.text.add(text)
        manager.sendAccessibilityEvent(e)
    }
}

AccessiblityEvent를 announce를 설정한 뒤 AccessibilityManager에 event를 전송하면 유저의 인풋에 대한 결과물이나 시각적 요소에 대한 변화에 대한 피드백을 전달할 수 있습니다. 더 나아가서 복잡한 기능에 대해서도 설명할 때에 사용해 볼 수도 있겠습니다.

모든 뷰가 화면에 보이는 것 처럼 읽어줄 준비를 마쳤다면 큰 산 하나 넘으셨습니다! 👏 이제 잘 넣어준 대체텍스트가 잘 전달되는지 포커스 이동에 대한 검증이 필요합니다. 내가 넣어준 대체텍스트가 이상한 순서에 읽히면 시각장애 유저 입장에서는 갑자기 왜 거기로 포커스가 이동해서 이 내용을 읽어주는건지 엉뚱하게 느껴질 수 있겠지요.

포커스 관리



시각적 어려움이 있는 분들께 스크린 리더가 읽어주는 포커스는 눈의 시선 이동과 같이 정보를 파악하는 흐름이기 때문에 중요하고, 포커스가 가지 않거나 튀어버리는 현상이 나타나지 않는지 확인하는 것이 필요합니다.

한 손가락으로 화면을 오른쪽으로 쓸면 다음 영역으로 왼쪽으로 쓸면 이전 영역으로 화면을 읽어주는 포커스가 이동하게 되는데요. 맨 위에 있던 포커스가 갑자기 왼쪽 하단과 같이 논리적 흐름이 맞지 않게 이동하게 될 때가 있습니다. 또한 너무 자잘한 영역을 잡고 끊어서 읽어주거나 너무 많은 내용을 한꺼번에 읽어주어 난감할 때가 있을 수 있습니다.

널리가 제공하는 앱 접근성 체크리스트 에 따르면 포커스는 왼쪽에서 오른쪽, 위에서 아래, 제목에서 내용이라는 순서대로 이동하기를 권장합니다. 기본적인 논리적 흐름에 기반한 순서이기 때문에 이 흐름이 깨진다면 앱을 사용에 혼동을 주기 쉽습니다.

포커스를 관리하는 방법 중 하나는 View api에 있는 View setAccessibilityTraversalAfter/Before 를 이용해서 포커스 이동 순서를 지정할 수 있습니다.

하지만 실제로 이렇게 모든 뷰의 이동 흐름 순서를 지정하는 경우는 흔하지 않은 것 같습니다. 배달의민족 안드로이드 앱 코드에서도 해당 메소드는 사용하지 않고 있고, OS가 알아서 순서를 잘 지켜주기 때문에 특별히 필요한 경우가 없었던 것 같습니다.

실제로 필요했던 경우는 포커스가 읽어주는 단위의 조정과 바뀐 뷰로 포커스를 잘 옮겨주는 과정이었습니다.

포커스 단위 조정


위 사진을 통해 가장 자주 쓰일 만한 부분은 하나의 포커스의 단위를 크게 만드는 예를 상상해볼 수 있습니다.

위와 같이 몇가지의 정보들을 한 리스트 아이템에 담은 형태들을 자주 사용하실텐데요. 배달의민족 메뉴 뷰는 메뉴 이름 따로, 옵션 따로, 가격 따로 읽어주는 식이었습니다. 해당 내용 구글코리아와 진행했던 접근성 컨설팅을 통해 인지할 수 있었고, 현재는 포커스가 하나로 묶여져 읽도록 개선했습니다.


하나로 묶기 위해 주로 사용하는 방법은 위와 같이 XML에서 parent view에 importantForAccessibility=yes로 설정해주고 child view에 no로 설정하는 것입니다. 이렇게 되면 스크린 리더가 자식 뷰들에게는 포커스를 보내지 않기 때문에 한 번에 읽어주게 되는것이지요. 과거에 focusble=true/false로 사용할 때도 있었지만 스크린리더를 사용하지 않을때에도 발생할 수 있는 side effect가 종종 발생하기 때문에importantForAccessiblity사용이 좀 더 안정적으로 쓸 수 있는 것 같습니다.

뷰를 따로따로 그리고 있으니 스크린 리더가 읽을때 뷰 한 땀 한 땀 읽을 것을 고려하진 못한 부분이었고, 듣는 입장에선 일일이 쓸어 념겨야 각각의 정보를 들을 수 있으니 번거로운 일이 될 수 있으므로 개발자의 정성이 필요한 부분입니다.

덮히는 Fragment

스크린 리더 사용자의 사용성을 생각하기 쉽지 않은 또 다른 부분이 있는데요. 바로 화면 위에 덮히는 Fragment입니다. 무언가를 실행해 위에 Fragment가 띄워져있을 경우 포커스가 위에 띄워진 Fragment로 가야 할 때 열릴 뷰의 XML에 focusbleInTouchMode를 설정해두거나 코드에서 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)를 호출해 해결될 때도 있었는데요.

아무리 다양한 View 메소드들을 이용해도 계속 이전 뷰에 포커스가 머물러 있는 경우가 있습니다.




위와 같이 실행시켜 새로운 화면을 띄워 정보를 커스텀하게 보여주고자 할 때 fragment를 사용할 수 있는데, 그 때 스크린 리더에서도 포커스가 잘 이동해줄 수 있도록 저희팀에서는 아래와 같은 코드로 관리하고 있습니다.

fun updateAccessibilityOnLifeCycle(lifecycleOwner: LifecycleOwner, hiddenViews: List<View>) {
    if (isTalkbackOn) {
        lifecycleOwner.lifecycle
                .addObserver(UpdateAccessibilityLifecycleObserver(hiddenViews))
    }
}


private class UpdateAccessibilityLifecycleObserver(private val views: List<View>) : LifecycleObserver {
    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    fun disableAccessibility() {
        for (view in views) {
            view.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
        }
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    fun enableAccessibility() {
        for (view in views) {
            view.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
        }
    }
}

fragment위에 fragment를 add 하기 전에 updateAccessibilityOnLifeCycle을 호출하면 하단 fragment의 view들에 대한 importantForAccessibility를 NO_HIDE_DESCENDANTS를 세팅해서 상단 fragment 뷰들에게 포커스가 가도록 해주며, 상단 fragment의 lifecycle이 stop되면 다시 하단 fragment view들에게 importantForAccessibility를 YES로 해줌으로 다시 포커스가 정상적으로 작동하도록 처리하고 있습니다.



그렇다면 실제 접근성 기능이 필요한 유저분들은 어떻게 사용하고 계실까요?




Google I/O나 여러 매체에서 전해지는 내용들을 보면 접근성을 높여줄 방법들이 다양하고, 개발자가 그것에 대한 작업을 해주면 시각장애가 있으신 분들도 앱을 사용할 수 있다고 하는데… 대체 정말 어떻게 사용하시기에 개발자 작업이 필요한걸까요? 아래 영상을 보시면 궁금증이 조금 해소되실꺼예요.





지난 가을 5명의 시각장애가 있으신 대학생 유저 5분을 사무실로 모실 기회가 있었고, 그 분들의 다양한 의견과 실제적인 사용법을 보고 배우고 싶었기에 위와 같은 유저와의 만남을 진행했습니다. 저희 사내에서는 꽤나 임팩트 있었던 기회였었고 실제 유저가 사용하는 모습을 보니 왜 필요한지 어떻게 해야할 지 확 와닿았다는 의견이 많았었는데.. 어떠셨나요? 접근성이 중요한 이유는 사용이 불편한 수준을 넘어서, 기능 자체를 이용할 수 있을까 없을까의 문제를 판가름하는 요소이기 때문이라는 이야기 덕분에 접근성이란 것에 대한 새로운 관점을 가질 수 있었던 것 같습니다.

Android의 스크린 리더인 Talkback과 비슷한 기능의 iOS의 Voiceove 설명을 통해 전맹 유저의 사용법도 경험할 수 있었는데요. 스크린리더 자체의 기본적인 사용법은 동일하고, 실제로 적지 않은 전맹 유저분들 또한 안드로이드를 애용해주고 계시고 있답니다. 저시력사용자를 위한 화면 개선은 이미 앱이 출시되어 있다면 변경될 디자인과 UX에 대한 보다 다양한 고민이 필요하지만, 전맹사용자를 위해 개선하는 것은 접근성에 대한 이해와 개발자의 코드를 통해 빠른 시간안에 실천이 가능합니다.


모두를 위한 발걸음



요즘 저희 팀은 대체텍스트를 어떻게 넣을 것인가 함께 고민하기도 하고, 개발과제 작업할 때 접근성 작업 또한 같이 진행하고 있습니다. 공유드렸던 코드들은 각 안드로이드 팀원들의 아이디어로 구현되어 있을 정도로 접근성에 대한 다양한 고민이 녹아있지요. 점점 접근성 개선이 특별한 일이 아닌 개발과정의 당연하고 해야하는 일처럼 스며드는 것이 느껴집니다. 또한 더욱 다양한 접근성을 채우기 위해 노력하고 있답니다.

여러분은 어떠신가요? 접근성을 오늘 처음 알게된 분들도 계실 수도 있지만, 많은 분들은 여러 경로를 통해 들어왔지만 어떻게 해야 할 지 모르거나 엄두가 안나서 혹은 상황이 따라주지 않아 시작해보지 못하셨으리라 예상합니다. 기술은 누구나 모두가 동일한 정보를 얻고 삶을 살아갈 수 있는 훌륭한 도구가 되어주고, 접근성은 그 목적을 충실히 수행하는 기술이라고 할 수 있습니다.

2020년에는 모두를 위한 앱서비스에 동참해 보시는 건 어떨까요? 👍




References
https://developer.android.com/guide/topics/ui/accessibility
https://android-developers.googleblog.com/2012/04/accessibility-are-you-serving-all-your.html https://venturebeat.com/2019/10/11/probeat-googles-accessibility-first-tech-helps-everyone https://www.cnet.com/news/for-people-with-disabilities-accessibility-techs-still-not-all-it-could-be/ https://nuli.navercorp.com/sharing/a11y/nmcag/checklist