데보션앱 소개페이지 바로가기
로그인 선택

신고하기

CLOSE
신고사유 (대표 사유 1개)
상세내용 (선택)
0/200
  • 신고한 게시글은 더 이상 보이지 않습니다.
  • 이용약관과 운영정책에 따라 신고사유에 해당하는지 검토 후 조치됩니다.
  • 허위 신고인 경우, 신고자의 서비스 이용이 제한될 수 있으니 유의하시어 신중하게 신고해 주세요.
(이 회원이 작성한 모든 댓글과 커뮤니티 게시물이 보이지 않고, 알림도 오지 않습니다.)

미리보기

커뮤니티

      1,234

      badge 23.06.15

      글 등록

      카테고리를 선택해주세요.

      DEVOTEE를 활성화 시키면
      지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.

      버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.

      임시저장함에 저장되었습니다. 저장일시 : 2022.5.17 14:29:08

      임시저장함

      제목을 선택하시면 이어서 작성이 가능하며,
      최대 20건까지 저장합니다.
      컨텐츠 유형, 제목, 저장일시, 삭제로 이뤄진 임시저장 목록
      컨텐츠 유형 제목 저장일 삭제

      데보션 블로그 게재 요청

      CLOSE
      • *
      • *

      본인인증

      효율적인 데보션 서비스 이용 및
      고객님의 소중한 개인정보보호를 위해
      본인인증을 진행해주세요. 본인인증 미 진행 시 로그인이 제한됩니다.
      본인인증 실패

      본인인증 로그인에 실패하였습니다.
      회원이 아니시거나 본인인증 등록이
      완료되지 않은 사용자입니다.

      회원정보 연결

      Jetpack Compose로 접근성 최적화하기

      dreamer 25.05.13
      1,419 7 1
      DEVOTEE 요약
      접근성은 모든 사용자가 앱을 편리하게 사용할 수 있도록 설계하는 필수 요소로, 장애인과 고령 사용자를 포함한 모두의 경험을 향상시킬 수 있습니다. Jetpack Compose를 활용하면 semantics 수정자를 통해 UI 요소의 접근성 속성을 제어하고, 포커스 관리, 탐색 순서 최적화, 동적 콘텐츠 알림 등으로 사용자 경험을 개선할 수 있습니다. 접근성은 단순한 기능 추가가 아닌 좋은 앱 설계의 기본으로, 이를 고려한 개발은 더 많은 사용자에게 최적의 경험을 제공할 수 있습니다.
      DEVOTEE 추천 블로그



      접근성 - 선택이 아닌 의무

      스마트폰을 눈을 감고 사용해본 적이 있으십니까? 혹은 한 손만으로 모든 기능을 사용해보셨습니까? 우리가 당연하게 여기는 앱 사용 경험이 누군가에게는 매일의 도전일 수 있습니다.

      한국에는 약 260만 명의 장애인과 빠르게 증가하는 고령 사용자들이 있습니다. 이들도 우리와 동일하게 음식을 주문하고, 택시를 부르고, 친구와 대화하기 위해 앱을 사용합니다.

      그러나 많은 앱들이 이들을 위한 기본적인 접근성조차 갖추지 못하고 있는 실정입니다.

      「장애인차별금지 및 권리구제 등에 관한 법률」에 따라 모바일 앱 개발에서 접근성은 선택이 아닌 필수입니다. 하지만 오늘 이야기하려는 것은 복잡한 법적 기준이나 깊은 기술적 내용이 아닙니다.

      모든 사용자가 앱을 편리하게 사용할 수 있도록 하는 것은 개발자의 책임이자 기회이기도 합니다.

      이 글에서는 개발자나 디자이너가 쉽게 놓칠 수 있는 접근성 요소들을 살펴보고, 간단한 변화만으로도 어떻게 더 많은 사용자에게 더 나은 경험을 제공할 수 있는지 알아보려 합니다.

      그럼 어렵지 않은 몇 가지 고려사항들이 어떻게 앱의 사용성을 향상시킬 수 있는지, 함께 알아보도록 하겠습니다.


      Semantics 수정자를 활용한 접근성 제어

      Jetpack Compose에서는 semantics 수정자가 접근성 구현의 핵심입니다. 이 수정자를 통해 UI 요소의 접근성 속성을 세밀하게 제어할 수 있습니다.

      기본 접근성 속성 설정:

      Text(
          "접근성 데모",
          style = MaterialTheme.typography.h4,
          modifier = Modifier
              .padding(16.dp)
              .semantics {
                  heading()
                  contentDescription = "접근성 데모 메인 화면"
              }
      )

      병합 및 트리 구조 제어:

      복잡한 UI 컴포넌트에서는 여러 요소가 하나의 논리적 단위로 작동해야 하는 경우가 많습니다.

      mergeDescendants 속성을 사용하면 불필요한 포커스 이동을 줄여 사용자 경험을 개선할 수 있습니다:

      @Composable
      fun ProfileItem(
          name: String,
          role: String,
          contentDescription: String
      ) {
          Row(
              ...
                  .semantics(mergeDescendants = true) {
                      this.contentDescription = contentDescription
                  },
          ) {
              Image(
                  painter = painterResource(id = R.drawable.profile_placeholder),
                  contentDescription = null,
                  modifier = Modifier.size(48.dp)
              )
              
              Spacer(modifier = Modifier.width(16.dp))
              
              Column {
                  Text(
                      text = name,
                      modifier = Modifier.semantics {
                          heading()
                      }
                  )
                  Text(text = role, modifier = Modifier)
              }
          }
      }

      복잡한 컴포넌트의 경우, 각 하위 요소마다 TalkBack이 포커스를 이동하면 사용자 경험이 저하될 수 있습니다.

      mergeDescendants=true를 설정하면 여러 요소를 하나의 접근성 노드로 병합하여 사용자 경험을 크게 개선할 수 있습니다.

      접근성 액션 커스터마이징:

      제스쳐에 의해 동작되는 액션은 시각적 사용자에게는 직관적이지만, 스크린 리더 사용자에게는 어려울 수 있습니다.

      아래 예제는 스와이프로 삭제를 하는 동작에 대한 대체 접근성 액션을 제공하는 방법을 보여줍니다:

      SwipeToDismiss(
              state = dismissState,
              modifier = modifier.semantics {
                  customActions = listOf(
                      CustomAccessibilityAction("삭제하기") {
                          isRemoved = true
                          onDelete()
                          true
                      },
                  )
              },
              background = { ... },
              dismissContent = { ... },
              directions = setOf(DismissDirection.StartToEnd, DismissDirection.EndToStart)
          )

      스와이프 제스처와 같은 복잡한 인터랙션은 장애가 있는 사용자가 사용하기 어려울 수 있습니다.

      위와 같이 Custom Action을 제공함으로써 TalkBack 사용자도 해당 기능을 쉽게 이용할 수 있게 됩니다.

      텍스트 표현 제어:

      화면에 표시되는 텍스트와 스크린 리더가 읽어야 할 텍스트가 다른 경우가 있습니다.

      특히 통화, 기호, 약어 등에서 이런 차이가 발생합니다:

      Text(
          text = "₩1,234,567",
          modifier = Modifier.semantics {
              // 스크린리더가 읽을 대체 텍스트 제공
              text = AnnotatedString("1,234,567원")
          }
      )

      이처럼 화면에는 기호로 표시하더라도, 스크린 리더 사용자에게는 더 명확하게 읽히도록 할 수 있습니다.

      요소 유형 정의와 특수 의미론:

      UI 요소의 역할과 상태를 명확히 정의하면 스크린 리더 사용자가 컨텍스트를 더 잘 이해할 수 있습니다:

      // 알림 설정
      Row(
          modifier = Modifier
              .padding(horizontal = 16.dp)
              .semantics(mergeDescendants = true) {
                  role = Role.Switch
                  toggleableState = if (notificationsEnabled) ToggleableState.On else ToggleableState.Off
                  stateDescription = if (notificationsEnabled) "알림 활성화됨" else "알림 비활성화됨"
              },
          verticalAlignment = Alignment.CenterVertically
      ) {
          Text(
              "알림 설정",
              modifier = Modifier.weight(1f)
          )
          Switch(
              checked = notificationsEnabled,
              onCheckedChange = { notificationsEnabled = it }
          )
      }

      이렇게 요소의 역할(Role)과 상태를 명시적으로 정의하면 스크린 리더가 더 정확한 정보를 사용자에게 전달할 수 있습니다.

      프로그래밍 방식 포커스 제어

      때로는 UI 상태가 변경될 때 접근성 포커스를 프로그래밍 방식으로 제어해야 할 필요가 있습니다.

      특히 다이얼로그가 표시되거나 동적 콘텐츠가 로드될 때 유용합니다:

      @Composable
      fun AccessibilityFocusComponent(
          isDialogVisible: Boolean,
          onDismiss: () -> Unit,
          onAction: () -> Unit
      ) {
          // 포커스 요청 객체 생성
          val focusRequester = remember { FocusRequester() }
          
          // 다이얼로그 표시 여부에 따라 포커스 설정
          LaunchedEffect(isDialogVisible) {
              if (isDialogVisible) {
                  // 약간의 지연 후 포커스 요청 (애니메이션 완료 후)
                  delay(300)
                  focusRequester.requestFocus()
              }
          }
          
          if (isDialogVisible) {
              AlertDialog(
                  onDismissRequest = onDismiss,
                  title = {
                      Text(
                          "중요 알림",
                          modifier = Modifier.semantics { heading() }
                      )
                  },
                  text = { Text("이 작업을 진행하시겠습니까?") },
                  confirmButton = {
                      Button(
                          onClick = onAction,
                          modifier = Modifier
                              .focusRequester(focusRequester)
                              .focusable()
                              .onFocusChanged { state ->
                                  if (state.isFocused) {
                                      // 포커스 획득 시 피드백 제공 (예: 햅틱)
                                  }
                              }
                      ) {
                          Text("확인")
                      }
                  },
                  dismissButton = {
                      TextButton(onClick = onDismiss) {
                          Text("취소")
                      }
                  }
              )
          }
      }

      다이얼로그와 같은 중요한 UI 요소가 나타날 때 자동으로 포커스를 이동시키면 스크린 리더 사용자의 방향성을 크게 개선할 수 있습니다.

      상태 변경 시 접근성 알림

      동적으로 변하는 콘텐츠는 시각적 사용자에게는 명확하지만, 스크린 리더 사용자에게는 인지하기 어려울 수 있습니다.

      이런 변화를 명시적으로 알리는 것이 중요합니다:

      var expandedState by remember { mutableStateOf(false) }
      
      Row(
          modifier = Modifier
              .fillMaxWidth()
              .clickable {
                  expandedState = !expandedState
              }
              .padding(16.dp)
              .semantics(mergeDescendants = true) {
                  contentDescription = title
                  // 접근성 상태 설명 추가
                  stateDescription = if (expandedState) "확장됨" else "축소됨"
                  // 라이브 리전으로 설정하여 상태 변경 알림
                  liveRegion = LiveRegionMode.Polite
              },
          horizontalArrangement = Arrangement.SpaceBetween,
          verticalAlignment = Alignment.CenterVertically
      ) {
          Text(
              text = title,
              style = MaterialTheme.typography.h6,
              modifier = Modifier.weight(1f).clearAndSetSemantics { }
          )
      
          Icon(
              imageVector = Icons.Default.KeyboardArrowDown,
              contentDescription = if (expandedState) "접기" else "펼치기",
              modifier = Modifier.rotate(rotationState).clearAndSetSemantics { }
          )
      }


      TalkBack 응답성 향상을 위한 Jetpack Compose 접근성 최적화 추가 TIP

      TalkBack은 안드로이드의 스크린 리더로, 사용자의 접근성 경험을 결정짓는 핵심 요소입니다.

      응답성과 사용성을 향상시키기 위한 추가적인 최적화 Tip 들을 알아보겠습니다.

      전략적 포커스 관리

      접근성 포커스를 효과적으로 관리하는 것은 TalkBack 사용자 경험의 핵심입니다:

      시각장애인에게는 논리적으로 예상되는 곳으로 편하게 포커스가 이동되는 것이 무엇보다 중요한 부분입니다.

      Image(
          painter = painterResource(id = R.drawable.decorative),
          contentDescription = null,  // 장식적 이미지는 contentDescription 없음
          modifier = Modifier.semantics { 
              isTraversalGroup = false  // 탐색 경로에서 제외
          }
      )
      
      
      // 중요 컨트롤에는 명시적 접근성 포커스 제어
      Button(
          onClick = { /* 동작 */ },
          modifier = Modifier.semantics { 
              focused = true  // 명시적으로 포커스 설정
              focusable = true
          }
      ) {
          Text("중요 동작")
      }

      장식적 요소는 접근성 포커스에서 제외하고, 중요한 컨트롤에 포커스를 집중시켜 사용자 경험을 효율적으로 만들 수 있습니다.

      탐색 순서 최적화

      Column {
          Text(
              text = "첫 번째 항목",
              modifier = Modifier.semantics { 
                  traversalIndex = 1f  // 첫 번째로 포커스
              }
          )
          
          Text(
              text = "세 번째 항목",
              modifier = Modifier.semantics { 
                  traversalIndex = 3f  // 세 번째로 포커스
              }
          )
          
          Text(
              text = "두 번째 항목",
              modifier = Modifier.semantics { 
                  traversalIndex = 2f  // 두 번째로 포커스
              }
          )
      }

      traversalIndex 속성을 사용하면 레이아웃과 상관없이 논리적인 탐색 순서를 정의할 수 있습니다.

      이는 레이아웃 순서와 접근성 순서가 다를 때 특히 유용합니다.

      발화 품질 향상

      스크린 리더가 정확하고 의미 있는 정보를 전달하도록 발화 품질을 최적화하는 것이 중요합니다:

      // 복잡한 약어나 발음 최적화
      Text(
          text = "FAQ",
          modifier = Modifier.semantics {
              contentDescription = "자주 묻는 질문"
          }
      )
      
      // 상태 정보 제공
      Checkbox(
          checked = isChecked,
          onCheckedChange = { isChecked = it },
          modifier = Modifier.semantics {
              stateDescription = if (isChecked) "선택됨" else "선택되지 않음"
          }
      )
      
      // 라이브 리전 설정으로 중요 알림 발화
      Text(
          text = "새로운 메시지가 도착했습니다",
          modifier = Modifier.semantics {
              liveRegion = LiveRegionMode.Polite
          }
      )

      약어나 특수 기호는 스크린 리더가 제대로 발음하지 못할 수 있습니다. contentDescription이나 text 속성을 사용해 올바른 발음을 유도할 수 있습니다.

      또한 liveRegion 속성을 통해 동적 콘텐츠에 대한 발화 우선순위를 설정할 수 있습니다.

      비동기 로딩 최적화

      데이터가 비동기적으로 로드될 때 접근성 이벤트 발생을 동기화하는 것이 중요할때도 있습니다.

      @Composable
      fun LoadingExample() {
          val context = LocalContext.current
          val accessibilityManager = context.getSystemService(AccessibilityManager::class.java)
          val coroutineScope = rememberCoroutineScope()
      
          var isDataLoaded by remember { mutableStateOf(false )}
      // 데이터 로딩 후 접근성 알림
          LaunchedEffect(isDataLoaded) {
              if (isDataLoaded && accessibilityManager?.isEnabled == true) {
                  coroutineScope.launch {
                      delay(100) // UI 업데이트 이후 약간의 지연
      
                      sendAccessibilityEvent(
                          context = context,
                          message = "데이터 로드가 완료되었습니다"
                      )
                  }
              }
          }
      
      // 로딩 인디케이터에 접근성 정보 추가
          CircularProgressIndicator(
              modifier = Modifier.progressSemantics()
          )
      }
      
      fun sendAccessibilityEvent(context: Context, message: String) {
          val accessibilityManager = context.getSystemService(AccessibilityManager::class.java)
          if (accessibilityManager?.isEnabled == true) {
              val event = AccessibilityEvent.obtain().apply {
                  eventType = AccessibilityEvent.TYPE_ANNOUNCEMENT
                  className = javaClass.name
                  packageName = context.packageName
                  text.add(message)
              }
              accessibilityManager.sendAccessibilityEvent(event)
          }
      }

      비동기 로딩 상황에서는 데이터가 준비된 후 사용자에게 명시적으로 알려주는 것이 중요합니다. 

      이를 통해 스크린 리더 사용자도 콘텐츠 변화를 놓치지 않을 수 있습니다.


      결론

      접근성은 더 이상 선택이 아닌 필수 요소입니다.

      뛰어난 접근성 구현은 장애가 있는 사용자뿐만 아니라, 모든 사용자의 경험을 향상시킵니다.

      명확한 포커스 관리, 의미 있는 콘텐츠 설명, 논리적인 탐색 순서 등은 보편적 사용성의 원칙과도 일치합니다.

      이 글에서는 Jetpack Compose에서 접근성을 구현하는 방법부터, TalkBack 최적화까지 기술적 접근성에 대해 다루었습니다.

      접근성은 단순한 기능 추가를 넘어, 깊이 있는 엔지니어링이 요구되는 영역입니다.

      성능과 접근성 사이의 균형을 맞추는 작업은 지속적인 도전이지만, 올바른 기법과 전략적 접근을 통해 모든 사용자에게 최적의 경험을 제공할 수 있습니다.

      접근성은 특별한 기능이 아니라, 좋은 앱 설계의 기본입니다.

      개발 과정 전반에 걸쳐 접근성을 고려하고, 이 문서에서 소개한 기법들을 적극적으로 활용한다면 Jetpack Compose 애플리케이션의 접근성을 체계적으로 높일 수 있을 것입니다.

      댓글 0

      DEVOTEE를 활성화 시키면
      지금 작성한 댓글에 AI가 댓글을 달아줍니다.

      dreamer 님의 최신 블로그

      더보기

      DEVOTEE 추천 블로그

      동영상 기고하기