Skip to main content

성능, 접근성, 보안을 고려한 앱 완성도 높이기

Views 9약 9분생성형 AI 활용
Summary

기능이 동작하는 것과 좋은 앱은 다릅니다

앱이 원하는 기능을 수행한다고 해서 곧바로 좋은 앱이 되는 것은 아닙니다. 사용자가 오래 쓰는 앱은 빠르게 반응하고, 다양한 사람이 사용할 수 있으며, 개인정보를 안전하게 다룹니다. 또 사용자의 언어와 지역 설정에 맞게 날짜, 숫자, 통화를 자연스럽게 보여줍니다.

이 장에서는 출시 전 반드시 점검해야 할 품질 요소를 다룹니다. 핵심은 단순합니다.

좋은 앱은 빠르고, 안전하며, 누구나 사용할 수 있어야 합니다.

성능, 접근성, 보안을 고려한 앱 완성도 높이기AI 생성 이미지

Compose 성능은 목록과 상태에서 많이 갈립니다

안드로이드 앱에서 성능 문제가 가장 잘 드러나는 곳은 목록 화면입니다. 학습 관리 앱을 예로 들면 홈 화면의 학습 항목 목록, 검색 결과, 알림 기록 화면이 여기에 해당합니다. 항목이 10개일 때는 문제가 없어 보여도 500개, 1,000개가 되면 스크롤이 끊기거나 화면 전환이 느려질 수 있습니다.

Compose에서는 많은 데이터를 표시할 때 Column보다 LazyColumn을 사용해야 합니다. LazyColumn은 화면에 필요한 항목 중심으로 UI를 구성하므로 긴 목록에 적합합니다. 다만 LazyColumn을 쓴다고 자동으로 모든 성능 문제가 해결되지는 않습니다.

목록 항목에는 가능한 한 안정적인 key를 지정해야 합니다.

LazyColumn {
    items(
        items = tasks,
        key = { task -> task.id }
    ) { task ->
        StudyTaskItem(task = task)
    }
}

key는 Compose가 "이 항목은 이전의 그 항목과 같은 데이터"라고 판단할 수 있게 돕습니다. 항목이 추가, 삭제, 정렬될 때 고유한 id가 없으면 Compose가 불필요하게 많은 항목을 다시 계산할 수 있습니다.

목록에 서로 다른 형태의 항목이 섞여 있다면 contentType도 고려할 수 있습니다.

LazyColumn {
    items(
        items = feedItems,
        key = { item -> item.id },
        contentType = { item -> item.type }
    ) { item ->
        when (item) {
            is FeedItem.Task -> TaskRow(item)
            is FeedItem.Ad -> AdRow(item)
            is FeedItem.Header -> HeaderRow(item)
        }
    }
}

contentType은 같은 종류의 UI를 더 효율적으로 재사용하도록 돕습니다. 모든 목록에 반드시 필요한 것은 아니지만, 헤더·광고·카드·일반 항목처럼 레이아웃이 크게 다른 데이터가 섞인 화면에서는 도움이 됩니다.

recomposition을 줄이는 기본 습관

Compose에서 상태가 바뀌면 관련 UI가 다시 계산됩니다. 이를 recomposition이라고 합니다. 재구성 자체는 문제가 아닙니다. Compose의 정상 동작입니다. 문제는 필요하지 않은 영역까지 너무 자주 다시 계산될 때 발생합니다.

먼저 상태의 범위를 작게 유지해야 합니다. 화면 전체에서 필요하지 않은 상태를 상위 Composable에 두면 작은 입력 변화에도 큰 화면이 다시 계산될 수 있습니다. 검색어, 체크 상태, 펼침 여부처럼 특정 영역에서만 쓰는 값은 가능한 한 그 영역 가까이에 둡니다.

계산 비용이 있는 값은 remember로 감쌀 수 있습니다.

val completedTasks = remember(tasks) {
    tasks.filter { it.isDone }
}

이 코드는 tasks가 바뀔 때만 완료된 항목을 다시 계산합니다. 단순한 계산에는 필요 없지만, 정렬·필터링·그룹화처럼 비용이 있는 작업에는 효과가 있습니다.

자주 변하는 상태에서 UI에 필요한 결과만 뽑아야 할 때는 derivedStateOf를 사용할 수 있습니다.

val showScrollTopButton by remember {
    derivedStateOf {
        listState.firstVisibleItemIndex > 3
    }
}

예를 들어 스크롤 위치는 매우 자주 바뀝니다. 하지만 UI는 "맨 위로 버튼을 보여줄지" 정도만 필요할 수 있습니다. 이때 derivedStateOf를 사용하면 불필요한 재구성 범위를 줄이는 데 도움이 됩니다.

이미지도 성능에 큰 영향을 줍니다. 목록에서 원본 크기 이미지를 그대로 불러오면 메모리 사용량이 커지고 스크롤이 끊길 수 있습니다. 화면에 보이는 크기에 맞는 썸네일을 사용하고, 비동기 이미지 로딩 라이브러리의 캐싱 기능을 활용해야 합니다. 로딩 중 표시할 UI와 실패 시 대체 UI도 함께 준비해야 합니다.

접근성은 선택 기능이 아니라 기본 품질입니다

접근성은 장애가 있는 사용자만을 위한 기능이 아닙니다. 작은 화면에서 한 손으로 조작하는 사용자, 햇빛 아래에서 색을 구분하기 어려운 사용자, 키보드나 스위치 장치로 앱을 조작하는 사용자 모두에게 영향을 줍니다.

Compose 접근성의 출발점은 의미 있는 정보를 보조 기술에 전달하는 것입니다. 대표적인 보조 기술은 TalkBack입니다. TalkBack은 화면을 음성으로 읽어 주고, 사용자가 터치 없이도 UI를 탐색할 수 있게 돕습니다.

아이콘만 있는 버튼에는 contentDescription을 제공해야 합니다.

IconButton(onClick = onDeleteClick) {
    Icon(
        imageVector = Icons.Default.Delete,
        contentDescription = "학습 항목 삭제"
    )
}

하지만 모든 이미지에 설명이 필요한 것은 아닙니다. 단순 장식 이미지는 contentDescription = null로 두어 TalkBack이 읽지 않게 할 수 있습니다. 중요한 기준은 "이 정보가 없어도 사용자가 기능을 이해할 수 있는가"입니다.

화면 구조도 의미 있게 전달해야 합니다. 제목 역할을 하는 텍스트에는 heading 의미를 줄 수 있습니다.

Text(
    text = "오늘의 학습",
    modifier = Modifier.semantics { heading() },
    style = MaterialTheme.typography.titleLarge
)

카드 안의 제목, 설명, 진행률이 각각 따로 읽히면 오히려 불편할 수 있습니다. 하나의 학습 항목으로 이해되어야 한다면 관련 요소를 그룹으로 묶는 것이 좋습니다.

Card(
    modifier = Modifier.semantics(mergeDescendants = true) {}
) {
    Column {
        Text(task.title)
        Text("진행률 ${task.progress}%")
    }
}

접근성 관련 예제로 자주 다루는 주제에는 텍스트 대체, 탐색 순서, 콘텐츠 그룹화, heading/list semantics, 화면과 pane title, 최소 터치 영역, UX 변화 알림, 입력 필드의 키보드 타입과 액션 등이 있습니다.1 이 항목들은 체크리스트처럼 활용하기 좋습니다.

사용자가 알아야 하는 변화는 말해 주어야 합니다

앱 화면이 바뀌었는데 보조 기술 사용자가 그 변화를 알 수 없다면 기능을 사용할 수 없습니다. 예를 들어 저장 버튼을 눌렀을 때 Snackbar만 잠깐 보이고 아무 안내가 없다면 TalkBack 사용자는 저장 성공 여부를 놓칠 수 있습니다.

이런 경우에는 Snackbar, Dialog, 화면 제목, pane title 등을 적절히 사용해 변화가 전달되도록 해야 합니다. 화면 전환이 일어났다면 새 화면의 제목이 명확해야 하고, 모달 바텀시트나 다이얼로그가 열렸다면 현재 사용자의 초점이 어디로 이동했는지도 자연스러워야 합니다.

입력 필드도 접근성의 중요한 부분입니다. TextField에는 시각적으로 보이는 힌트만 두지 말고 의미 있는 label을 제공해야 합니다. 이메일 입력에는 이메일 키보드, 숫자 입력에는 숫자 키보드, 검색 입력에는 검색 액션을 지정합니다.

TextField(
    value = title,
    onValueChange = onTitleChange,
    label = { Text("학습 제목") },
    keyboardOptions = KeyboardOptions(
        imeAction = ImeAction.Next
    )
)

색상 대비와 터치 영역도 반드시 확인해야 합니다. 오류를 빨간색만으로 표시하면 색을 구분하기 어려운 사용자는 문제를 알 수 없습니다. 색과 함께 오류 메시지를 제공해야 합니다.

TextField(
    value = email,
    onValueChange = onEmailChange,
    label = { Text("이메일") },
    isError = emailError != null,
    supportingText = {
        if (emailError != null) Text(emailError)
    }
)

버튼, 체크박스, 아이콘 버튼은 손가락으로 누르기 충분한 크기를 가져야 합니다. 너무 작은 터치 영역은 접근성뿐 아니라 일반 사용성도 떨어뜨립니다.

보안은 나중에 붙이는 장식이 아닙니다

보안은 로그인 화면을 만들 때만 생각하는 주제가 아닙니다. 앱이 데이터를 저장하고, 서버와 통신하고, 로그를 남기고, 백업되고, 스크린샷에 노출되는 모든 과정이 보안과 연결됩니다.

OWASP의 Mobile Application Security 프로젝트는 모바일 앱 보안과 개인정보 보호를 점검하기 위한 기준으로 MASVS, 일반적인 취약점 목록인 MASWE, 테스트 가이드인 MASTG 등을 제공합니다.2 출시 전에는 이런 기준을 큰 체크리스트로 삼아 앱이 민감한 데이터를 어떻게 다루는지 점검할 수 있습니다.

가장 좋은 보안 원칙은 민감한 데이터를 가능한 한 저장하지 않는 것입니다. 저장하지 않은 데이터는 유출될 수 없습니다. 꼭 저장해야 한다면 일반 텍스트 파일이나 평범한 SharedPreferences에 넣지 말고 Android Keystore 기반 암호화 저장소를 사용해야 합니다.

예를 들어 자동 로그인 토큰, 개인 식별 정보, 결제 관련 정보, 건강 정보 등은 특별히 조심해야 합니다. 로그에도 남기면 안 됩니다.

// 피해야 하는 예
Log.d("Login", "token=$accessToken")

디버깅 중에는 편해 보여도 릴리스 앱에서 이런 로그가 남으면 심각한 문제가 될 수 있습니다. 로그에는 토큰, 비밀번호, 이메일, 전화번호, 위치 정보 같은 값을 직접 출력하지 않는 습관이 필요합니다.

백업, 클립보드, 스크린샷도 예상치 못한 노출 경로가 될 수 있습니다. 비밀번호 재설정 코드나 일회용 인증번호를 클립보드에 오래 남기는 것은 위험합니다. 금융, 인증, 민감 정보 화면에서는 스크린샷 방지 정책을 검토할 수 있습니다. 앱 데이터가 자동 백업될 때 민감한 파일이 포함되지 않는지도 확인해야 합니다.

API 키와 토큰은 앱 안에 숨길 수 없습니다

초보자가 자주 하는 실수는 API 키를 앱 코드에 넣고 "난독화했으니 괜찮다"고 생각하는 것입니다. 하지만 앱 바이너리는 사용자의 기기에 설치됩니다. 공격자는 앱 파일을 분석할 수 있습니다. 즉, 앱 안에 들어간 비밀값은 완전히 숨길 수 없다고 봐야 합니다.

중요한 API 키는 서버에서 관리하고, 앱은 서버를 통해 필요한 요청만 수행하는 구조가 안전합니다. 불가피하게 클라이언트에 키가 들어가야 한다면 권한을 최소화하고, 사용 가능한 API와 도메인, 패키지 이름, 인증서 지문 등을 제한해야 합니다. 키가 유출될 가능성에 대비해 키 회전 전략도 필요합니다. 키 회전은 기존 키를 폐기하고 새 키로 교체할 수 있는 운영 절차를 말합니다.

네트워크 통신은 HTTPS를 기본으로 사용해야 합니다. 개발 중 임시로 HTTP를 허용했다면 릴리스 전에 제거해야 합니다. Network Security Config를 사용하면 앱의 네트워크 보안 정책을 명시할 수 있습니다. 예를 들어 특정 도메인에만 cleartext traffic을 허용하거나, 운영 빌드에서는 HTTP 통신을 막을 수 있습니다.

또한 인증서 검증을 우회하는 코드를 넣으면 안 됩니다. 테스트 서버 연결을 위해 모든 인증서를 신뢰하도록 만든 코드가 릴리스에 남으면 중간자 공격에 취약해집니다. 중간자 공격은 앱과 서버 사이의 통신을 제3자가 가로채거나 조작하는 공격입니다.

국제화는 문자열 분리에서 시작합니다

국제화는 앱을 여러 언어와 지역에서 자연스럽게 사용할 수 있도록 준비하는 일입니다. 가장 기본은 화면 문자열을 코드에 직접 쓰지 않는 것입니다.

// 피해야 하는 방식
Text("학습 추가")

문자열은 strings.xml 같은 리소스로 분리합니다.

<string name="add_study_task">학습 추가</string>

Compose에서는 다음처럼 사용할 수 있습니다.

Text(stringResource(R.string.add_study_task))

문장 안에 값이 들어간다면 문자열 연결보다 자리 표시자를 사용합니다.

<string name="completed_count">%1$d개 완료</string>
Text(stringResource(R.string.completed_count, completedCount))

복수형 표현도 언어마다 규칙이 다릅니다. 한국어는 비교적 단순하지만 영어 등 다른 언어는 1개와 여러 개의 표현이 달라집니다. 이런 경우 plurals를 사용해야 합니다.

날짜, 시간, 숫자, 통화는 사용자의 Locale에 맞게 표시해야 합니다. 예를 들어 2026-05-22라는 형식이 모든 사용자에게 자연스러운 것은 아닙니다. 미국, 독일, 한국의 날짜 표기 방식은 다를 수 있습니다. 통화도 마찬가지입니다. 10000원, $10,000.00, 10.000,00 €처럼 지역에 따라 기호, 쉼표, 소수점 위치가 달라집니다.

val formatter = NumberFormat.getCurrencyInstance(Locale.getDefault())
val priceText = formatter.format(price)

국제화는 단순히 번역 파일을 추가하는 일이 아닙니다. 글자가 길어졌을 때 버튼이 잘리지 않는지, 오른쪽에서 왼쪽으로 쓰는 언어에서도 화면이 자연스러운지, 날짜와 숫자가 사용자의 기대에 맞는지 확인하는 과정까지 포함합니다.

출시 전 품질 점검 목록

출시 전에는 다음 질문에 답해 보아야 합니다.

  • 긴 목록에서 스크롤이 부드러운가?

  • LazyColumn 항목에 안정적인 key를 지정했는가?

  • 불필요한 recomposition을 줄이기 위해 상태 범위를 적절히 나누었는가?

  • 큰 이미지를 원본 그대로 목록에 표시하고 있지는 않은가?

  • 아이콘 버튼과 이미지에 적절한 contentDescription이 있는가?

  • 제목, 목록, 그룹, 화면 전환이 TalkBack 사용자에게 이해되는가?

  • 터치 영역은 충분히 큰가?

  • 색상만으로 오류나 상태를 전달하고 있지는 않은가?

  • 토큰, 개인정보, 위치 정보가 로그에 남지 않는가?

  • 민감한 데이터를 꼭 저장해야 한다면 암호화 저장소를 사용하는가?

  • API 키를 앱 코드에 하드코딩하지 않았는가?

  • HTTPS와 인증서 검증 정책이 안전한가?

  • 문자열, 날짜, 숫자, 통화가 사용자의 Locale에 맞게 처리되는가?

이 목록은 한 번 확인하고 끝나는 문서가 아닙니다. 앱에 기능이 추가될 때마다 다시 확인해야 하는 기준입니다.

완성도는 신뢰를 만듭니다

사용자는 앱 내부 구조를 보지 못합니다. 대신 앱이 빠르게 반응하는지, 글자가 읽기 쉬운지, 오류가 친절하게 안내되는지, 개인정보를 안전하게 다루는지로 앱을 판단합니다.

성능, 접근성, 보안, 국제화는 서로 떨어진 주제가 아닙니다. 모두 사용자의 신뢰와 연결됩니다. 빠르지만 개인정보를 허술하게 다루는 앱은 좋은 앱이 아닙니다. 기능은 많지만 TalkBack으로 사용할 수 없는 앱도 좋은 앱이 아닙니다. 한 지역에서만 자연스럽게 보이는 앱은 더 넓은 사용자에게 다가가기 어렵습니다.

출시 직전의 마지막 질문은 이것입니다.

"이 앱을 다양한 사용자가 안심하고 오래 사용할 수 있는가?"

이 질문에 자신 있게 답할 수 있을 때 앱은 단순한 실습 결과물을 넘어 실제 서비스에 가까워집니다.

참고

1Android Compose Accessibility Techniques

2OWASP Mobile Application Security

책 목록

처음부터 출시까지 배우는 안드로이드 앱 개발

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기