Android 개발을 시작하면 가장 먼저 마주치는 단위가 dp와 sp이다. 대부분의 개발자가 "dp는 뷰 크기에, sp는 텍스트에 사용한다"는 정도로 알고 있지만, 왜 이렇게 구분하는지, 그리고 sp를 사용할 때 발생하는 레이아웃 깨짐 문제는 어떻게 해결해야 하는지까지 깊이 이해하고 있는 경우는 드물다. 이 글에서는 dp와 sp의 근본적인 차이점부터 실무에서 마주치는 문제 해결법까지 상세히 다뤄보고자 한다.
Android 기기는 다양한 화면 크기와 해상도를 가지고 있다. 만약 버튼 너비를 100px로 고정한다면, 저해상도 기기에서는 버튼이 크게 보이고 고해상도 기기에서는 작게 보이는 문제가 발생한다.
예를 들어 같은 100px이라도 160dpi 화면에서는 약 1.6cm이지만, 480dpi 화면에서는 약 0.5cm에 불과하다. 사용자 경험의 일관성을 위해 밀도 독립적인 단위가 필요한 이유이다.
dp(density-independent pixel)는 화면 밀도에 관계없이 일관된 물리적 크기를 유지하도록 설계된 단위이다. 기준점은 160dpi 화면으로, 이 화면에서 1dp는 정확히 1px과 같다.
실제 픽셀 = dp × (화면 dpi / 160)
따라서 320dpi 화면에서 1dp는 2px로 변환되고, 480dpi 화면에서 1dp는 3px로 변환된다. 결과적으로 어떤 기기에서든 100dp 버튼은 거의 동일한 물리적 크기로 표시된다.
dp는 오직 화면 밀도만 고려하여 크기를 계산한다. 사용자의 시스템 설정과 무관하게 항상 동일한 물리적 크기를 유지한다.
// dp를 px로 변환하는 공식
fun dpToPx(dp: Float, context: Context): Float {
return dp * context.resources.displayMetrics.density
}
주로 뷰의 크기, 마진, 패딩 등 레이아웃 관련 수치에 사용된다.
sp는 dp의 특성에 더해 사용자의 글꼴 크기 설정(Font Scale)을 추가로 반영한다.
// sp를 px로 변환하는 공식
fun spToPx(sp: Float, context: Context): Float {
return sp * context.resources.displayMetrics.scaledDensity
}
scaledDensity는 density × fontScale로 계산된다. 사용자가 시스템 설정에서 글꼴 크기를 "크게" 또는 "아주 크게"로 설정하면 fontScale 값이 증가하고, 이에 따라 sp 단위의 텍스트도 함께 커진다.
160dpi 화면에서 사용자가 글꼴 크기를 1.3배로 설정한 경우를 살펴보자.
16dp → 16px (밀도만 적용)
16sp → 16 × 1.0 × 1.3 = 20.8px (밀도 + 글꼴 배율 적용)
320dpi 화면에서 동일한 설정이라면 다음과 같다.
16dp → 32px
16sp → 16 × 2.0 × 1.3 = 41.6px
시력이 좋지 않은 사용자, 고령 사용자, 또는 단순히 큰 글씨를 선호하는 사용자는 시스템 설정에서 글꼴 크기를 조절한다. sp 단위를 사용하면 이러한 사용자 설정을 자연스럽게 반영할 수 있다.
Google Play 스토어 정책과 접근성 가이드라인에서도 텍스트에 sp 단위 사용을 권장하고 있다. 접근성을 고려하지 않은 앱은 사용자층을 제한할 뿐 아니라 법적 이슈가 될 수도 있다.
텍스트 크기에 dp를 사용하면 사용자의 글꼴 크기 설정이 무시된다. 이는 접근성 위반일 뿐 아니라 사용자 경험을 해치는 행위이다.
<!-- 권장하지 않음 -->
<TextView
android:textSize="16dp" />
<!-- 권장 -->
<TextView
android:textSize="16sp" />
sp의 접근성 지원은 분명 장점이지만, 실무에서는 골치 아픈 문제를 야기하기도 한다. 사용자가 글꼴 크기를 크게 설정하면 텍스트가 예상보다 커지면서 레이아웃이 깨지는 현상이 발생한다.
텍스트 잘림 현상: 고정 높이 컨테이너 안의 텍스트가 잘려서 보이지 않는다.
레이아웃 겹침: 커진 텍스트가 인접한 뷰와 겹치거나 밀어낸다.
버튼 텍스트 넘침: 버튼 안의 텍스트가 버튼 영역을 벗어난다.
한 줄 텍스트의 줄바꿈: 의도치 않게 여러 줄로 표시되면서 디자인이 무너진다.
가장 기본적인 해결책은 텍스트를 담는 컨테이너의 높이를 고정하지 않는 것이다.
<!-- 문제가 발생하는 코드 -->
<TextView
android:layout_width="match_parent"
android:layout_height="48dp"
android:textSize="16sp" />
<!-- 권장하는 코드 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:textSize="16sp" />
minHeight를 사용하면 기본적인 터치 영역은 확보하면서 텍스트가 커질 경우 자연스럽게 높이가 늘어난다.
ConstraintLayout을 사용하면 텍스트 크기 변화에 유연하게 대응할 수 있다.
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="18sp"
app:layout_constraintEnd_toStartOf="@id/button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
텍스트 영역의 width를 0dp(match_constraint)로 설정하면 버튼 영역을 침범하지 않으면서 가용 공간 내에서 자동으로 줄바꿈된다.
제한된 공간에 텍스트를 맞춰야 할 때는 autoSizeTextType을 활용할 수 있다.
<TextView
android:layout_width="match_parent"
android:layout_height="48dp"
android:autoSizeTextType="uniform"
android:autoSizeMinTextSize="10sp"
android:autoSizeMaxTextSize="18sp"
android:autoSizeStepGranularity="1sp"
android:maxLines="1" />
이 설정을 사용하면 텍스트가 컨테이너를 벗어날 경우 자동으로 글꼴 크기가 줄어든다. 단, 접근성 측면에서 최소 크기(autoSizeMinTextSize)를 너무 작게 설정하면 안 된다.
공간이 제한적이고 텍스트 전체 표시가 필수가 아닌 경우 말줄임 처리를 적용할 수 있다.
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="2"
android:ellipsize="end"
android:textSize="16sp" />
다만 중요한 정보가 잘리지 않도록 주의해야 한다.
디자인상 절대로 크기가 변하면 안 되는 특수한 경우에는 sp 대신 dp를 사용하거나, 프로그래밍 방식으로 고정 크기를 적용할 수 있다.
// 특정 TextView에서 font scale 무시
fun TextView.setTextSizeIgnoringFontScale(sizeInSp: Float) {
setTextSize(TypedValue.COMPLEX_UNIT_PX, sizeInSp * resources.displayMetrics.density)
}
그러나 이 방법은 접근성을 해치므로 정말 필요한 경우에만 제한적으로 사용해야 한다. 탭 바의 아이콘 라벨이나 고정 크기가 필수인 배지 등 극히 일부 UI 요소에만 적용하는 것이 좋다.
가장 중요한 것은 다양한 글꼴 크기 설정에서 앱을 테스트하는 것이다.
// 테스트 코드에서 Font Scale 변경
@Test
fun testLargeFontScale() {
val config = Configuration(context.resources.configuration)
config.fontScale = 1.5f // 큰 글꼴
val scaledContext = context.createConfigurationContext(config)
// 테스트 수행
}
에뮬레이터나 실제 기기에서 설정 → 디스플레이 → 글꼴 크기에서 "가장 크게" 설정 후 앱의 모든 화면을 점검하는 습관을 들여야 한다.
dp와 sp는 단순한 단위 차이가 아니라 접근성과 사용자 경험에 대한 철학이 담겨 있다. sp를 사용함으로써 발생하는 레이아웃 문제는 분명 번거롭지만, 이는 더 많은 사용자를 포용하기 위해 감수해야 할 비용이다.
핵심은 sp 사용을 회피하는 것이 아니라, sp를 사용하면서도 깨지지 않는 유연한 레이아웃을 설계하는 것이다. wrap_content, minHeight, autoSizeTextType, ConstraintLayout의 조합으로 대부분의 문제를 해결할 수 있다.
결국 좋은 Android 개발자란 다양한 기기와 사용자 설정에서도 일관된 경험을 제공할 수 있는 개발자이다. dp와 sp의 차이를 정확히 이해하고, 접근성을 고려한 레이아웃을 설계하는 것이 그 첫걸음이 될 것이다.