Manifest Android Interview 책을 읽고 Practical Questions 에 대한 답변을 작성해보고, 카테고리 내에 특정 개념에 대한 딥다이브 및 스터디 시간에 얘기 나누면 좋을 내용들을 적어보는 글입니다.
답변 정리는 LLM의 도움을 받았습니다.
Q) 33. View Lifecycle(뷰 생명주기)에 대해 설명하라
Q1) 이미지 로딩이나 애니메이션 설정과 같은 비용이 많이 드는 초기화 작업이 필요한 커스텀 View를 만들고 있다. View 생명주기의 어느 시점에서 이러한 리소스를 초기화해야 하며, 메모리 누수를 방지하기 위해 어떻게 적절한 정리 작업을 보장할 수 있는가?
리소스 초기화 시점:
onAttachedToWindow(): View가 Window에 연결될 때 호출. 이때 이미지 로딩, 애니메이션 설정 등 비용이 큰 작업을 수행하는 것이 적절. 이 시점에서는 View가 실제로 화면에 표시될 준비가 되었기 때문.
메모리 누수 방지 방법:
class CustomView : View {
private var animator: ValueAnimator? = null
override fun onAttachedToWindow() {
super.onAttachedToWindow()
// 리소스 초기화
animator = ValueAnimator.ofFloat(0f, 1f).apply {
duration = 1000
start()
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
// 정리 작업
animator?.cancel()
animator = null
}
}
Q2) 동적으로 생성되는 View들로 구성된 복잡한 UI를 가진 애플리케이션에서 성능 문제가 발생하고 있다. 반응성을 유지하면서 렌더링 효율성을 개선하기 위해 onMeasure()와 onLayout() 메서드를 어떻게 최적화할 수 있는가?
onMeasure() 최적화:
측정 결과를 캐싱하여 동일한 조건에서 재계산 방지
MeasureSpec을 적절히 활용하여 불필요한 측정 방지
복잡한 계산을 백그라운드에서 미리 수행
onLayout() 최적화:
자식 View의 위치가 변경되지 않았다면 layout() 호출 생략
requestLayout() 호출을 최소화
ViewGroup에서 clipChildren=false 사용 시 성능 저하 주의
Q) 34. View와 ViewGroup의 차이점은 무엇인가?
View:
모든 UI 구성 요소의 기본 클래스
직접적인 사용자 상호작용과 렌더링 담당
Button, TextView, ImageView 등이 View를 상속
ViewGroup:
View를 상속받아 다른 View들을 포함할 수 있는 컨테이너
자식 View들의 레이아웃과 측정을 관리
LinearLayout, RelativeLayout, ConstraintLayout 등이 ViewGroup을 상속
Q1) View 생명주기에서 requestLayout(), invalidate(), postInvalidate()가 어떻게 작동하는지 설명하고, 각각을 언제 사용해야 하는지 설명하라.
requestLayout():
View의 크기나 위치가 변경되었을 때 사용
측정(measure) → 레이아웃(layout) → 그리기(draw) 전체 과정을 다시 수행
부모 ViewGroup까지 전파되어 전체 레이아웃 재계산
invalidate():
View의 내용(외관)만 변경되었을 때 사용
onDraw()만 다시 호출하여 그리기만 재수행
반드시 메인(UI) 스레드에서만 호출 가능
postInvalidate():
invalidate()의 스레드 안전 버전
백그라운드 스레드에서 호출 가능
내부적으로 Handler를 통해 메인 스레드로 메시지 전달
Q2) View 생명주기는 Activity 생명주기와 어떻게 다르며, 효율적인 UI 렌더링을 위해 두 가지 모두를 이해하는 것이 왜 중요한가?
Activity 생명주기:
onCreate → onStart → onResume → onPause → onStop → onDestroy
View 생명주기:
Constructor → onFinishInflate → onAttachedToWindow → onMeasure → onLayout → onDraw → onDetachedFromWindow
주요 차이점:
Activity가 재생성되면 View도 모두 새로 생성됨
Configuration Change 시 Activity는 onSaveInstanceState/onRestoreInstanceState로 상태 복원하지만, View는 별도 처리 필요
View는 Activity 내에서 동적으로 추가/제거될 수 있어 독립적인 생명주기를 가짐
두 생명주기를 모두 이해해야 하는 이유:
메모리 효율성: 불필요한 리소스 사용 방지
배터리 최적화: 백그라운드에서 불필요한 작업 중단
성능 향상: 보이지 않는 View의 렌더링 작업 최소화
상태 관리: Configuration Change 시 올바른 상태 복원
사용자 경험: 부드러운 애니메이션과 반응성 제공
결론: Activity와 View 생명주기를 모두 고려해야 앱이 메모리 효율적이고 성능이 좋으며 배터리를 절약하는 최적화된 UI를 제공할 수 있음
Q) 35. ViewStub을 사용해 본 적 있는가? UI 성능을 어떻게 최적화하는가?
ViewStub은 지연 로딩(Lazy Loading)을 위한 특수한 View
Q) ViewStub이 인플레이트되면 View 계층에 어떤 변화가 발생하며, 메모리 사용량과 성능에 어떤 영향을 미치는가?
작동 원리:
초기에는 0x0 크기로 메모리만 점유
inflate() 호출 시 실제 레이아웃으로 교체
한 번 인플레이트되면 ViewStub 자체는 제거됨
성능 최적화:
<ViewStub
android:id="@+id/viewStub"
android:layout="@layout/expensive_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
사용 예시: 조건부로 표시되는 복잡한 UI (프로필 상세 정보, 광고 배너 등)
Q) 36. 커스텀 View를 어떻게 구현하는가?
커스텀 View 구현 단계
기본 구조
class CustomView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
// 1. 페인트 및 리소스 초기화
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private var customColor = Color.BLUE
private var customText = "Default"
init {
// 2. 커스텀 속성 적용
setupAttributes(attrs)
setupPaint()
}
// 3. 크기 측정
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val desiredWidth = 200
val desiredHeight = 100
val width = resolveSize(desiredWidth, widthMeasureSpec)
val height = resolveSize(desiredHeight, heightMeasureSpec)
setMeasuredDimension(width, height)
}
// 4. 레이아웃 배치
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
// 자식 View가 있을 경우 위치 계산
}
// 5. 그리기 구현
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 배경 그리기
paint.color = customColor
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
// 텍스트 그리기
paint.color = Color.WHITE
paint.textSize = 40f
canvas.drawText(customText, 50f, height / 2f, paint)
}
// 6. 터치 이벤트 처리
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// 터치 시작
performClick()
return true
}
MotionEvent.ACTION_UP -> {
// 터치 끝
return true
}
}
return super.onTouchEvent(event)
}
override fun performClick(): Boolean {
super.performClick()
// 클릭 이벤트 처리
return true
}
}
Q) XML 레이아웃에서 하위 버전 호환성을 보장하면서 커스텀 View에 커스텀 속성을 효율적으로 적용하는 방법은 무엇인가?
attrs.xml에서 커스텀 속성 정의:
<declare-styleable name="CustomView">
<attr name="customColor" format="color" />
<attr name="customText" format="string" />
</declare-styleable>
하위 호환성 보장:
class CustomView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
init {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomView)
// 기본값 제공으로 하위 호환성 보장
val customColor = typedArray.getColor(R.styleable.CustomView_customColor, Color.BLACK)
val customText = typedArray.getString(R.styleable.CustomView_customText) ?: "Default"
typedArray.recycle()
}
}
Q) 37. Canvas란 무엇이며, 어떻게 활용하는가?
Canvas는 Android에서 2D 그래픽을 그리기 위한 API.
주요 메서드:
drawRect(): 사각형 그리기
drawCircle(): 원 그리기
drawPath(): 복잡한 경로 그리기
drawText(): 텍스트 그리기
drawBitmap(): 이미지 그리기
Q) AndroidX 라이브러리에서 지원하지 않는 복잡한 도형이나 UI 요소를 렌더링하기 위한 커스텀 View를 어떻게 만드는가? 어떤 Canvas 메서드와 API를 사용할 것인가?
복잡한 도형 렌더링 Canvas API:
핵심 메서드
override fun onDraw(canvas: Canvas) {
// 1. Path - 복잡한 도형
val path = Path().apply {
moveTo(100f, 100f)
cubicTo(150f, 50f, 250f, 150f, 300f, 100f) // 베지어 곡선
arcTo(RectF(200f, 200f, 400f, 400f), 0f, 180f, false) // 원호
}
// 2. Matrix - 변환
canvas.save()
canvas.rotate(45f, centerX, centerY)
canvas.scale(1.5f, 1.5f, centerX, centerY)
// 3. Shader - 그라데이션
paint.shader = LinearGradient(0f, 0f, width.toFloat(), height.toFloat(),
Color.RED, Color.BLUE, Shader.TileMode.CLAMP)
// 4. 고급 기능
canvas.clipPath(path) // 클리핑
canvas.drawPath(path, paint)
canvas.restore()
}
주요 API:
Path: moveTo(), cubicTo(), arcTo(), addCircle()
Canvas: save(), restore(), rotate(), scale(), clipPath()
Paint: setShader(), setXfermode(), setPathEffect()
Matrix: postRotate(), postScale(), concat()
Q) 38. View 시스템에서 Invalidation이란 무엇인가?
Invalidation은 View를 다시 그리도록 요청하는 과정
Q1) invalidate() 메서드는 어떻게 작동하며, postInvalidate()와 어떤 차이가 있는가? 각각이 적절한 실제 사용 사례를 제시하라.
invalidate():
메인 스레드에서만 호출 가능
즉시 무효화 플래그 설정
다음 드로잉 사이클에서 onDraw() 호출
postInvalidate():
모든 스레드에서 호출 가능
Handler를 통해 메인 스레드로 메시지 전달
네트워크 콜백, 백그라운드 작업 완료 시 사용
실제 사용 사례:
// UI 이벤트 처리 시
button.setOnClickListener {
customView.invalidate() // 메인 스레드에서 직접 호출
}
// 백그라운드 작업 완료 시
Thread {
// 백그라운드 작업
customView.postInvalidate() // 백그라운드에서 안전하게 호출
}.start()
Q2) 백그라운드 스레드에서 UI 요소를 업데이트해야 하는 경우, 메인 스레드에서 다시 그리기 작업이 안전하게 수행되도록 어떻게 보장하겠는가?
// Handler 사용
Handler(Looper.getMainLooper()).post {
view.invalidate()
}
// Activity의 runOnUiThread
activity.runOnUiThread {
view.invalidate()
}
// postInvalidate 사용
view.postInvalidate()
Q) 39. ConstraintLayout이란?
Q1) ConstraintLayout은 중첩된 LinearLayout과 RelativeLayout에 비해 어떻게 성능을 향상시키는가? ConstraintLayout을 사용하는 것이 더 효율적인 시나리오를 제시하라.
성능 향상 원리:
기존 중첩 레이아웃의 문제:
<!-- 성능이 좋지 않은 중첩 구조 -->
<LinearLayout>
<RelativeLayout>
<LinearLayout>
<TextView />
<Button />
</LinearLayout>
</RelativeLayout>
</LinearLayout>
ConstraintLayout의 장점:
플랫 계층구조: 모든 View를 한 레벨에 배치
측정 패스 최소화: 복잡한 레이아웃도 한 번의 측정으로 해결
메모리 효율성: ViewGroup 객체 수 감소
Q2) ConstraintLayout에서 match_constraint (0dp) 동작이 어떻게 작동하는지 설명하라. wrap_content와 match_parent와 어떤 차이가 있으며, 어떤 상황에서 사용해야하는가?
<TextView
android:layout_width="0dp" <!-- match_constraint -->
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
차이점:
wrap_content: 콘텐츠 크기에 맞춤
match_parent: 부모 크기에 맞춤 (ConstraintLayout에서는 사용 비권장)
0dp (match_constraint): 제약 조건에 따라 크기 결정
사용 상황: 반응형 UI, 비율 기반 레이아웃, 여러 View 간 공간 분할
Q) 40. 언제 TextureView를 대신 SurfaceView를 사용해야 하는가?
SurfaceView 사용 권장 상황
// 게임, 비디오 플레이어, 카메라 프리뷰
class GameSurfaceView : SurfaceView, SurfaceHolder.Callback {
// 60fps 이상의 부드러운 렌더링
// 하드웨어 가속 최적화
}
SurfaceView: 별도 Surface로 메모리 효율적
TextureView: View 계층에 포함되어 메모리 오버헤드 발생
// SurfaceView - 하드웨어 최적화
// TextureView - 추가 GPU 메모리 복사 발생
TextureView 사용 권장 상황
// 회전, 스케일, 애니메이션
textureView.rotation = 45f
textureView.scaleX = 1.5f
textureView.alpha = 0.8f // SurfaceView에서 불가능
// ScrollView 내부, RecyclerView 아이템
// View 계층에서 자연스러운 동작 필요
Q1) SurfaceView의 생명주기를 적절히 관리하여 효율적인 리소스 관리를 보장하고 메모리 누수를 방지하는 방법은 무엇인가?
SurfaceView 생명주기 관리 및 리소스 최적화:
class CustomSurfaceView : SurfaceView, SurfaceHolder.Callback {
private var renderingThread: Thread? = null
private var isRendering = false
init {
holder.addCallback(this)
}
override fun surfaceCreated(holder: SurfaceHolder) {
// 리소스 초기화
isRendering = true
startRenderingThread()
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
// 안전한 스레드 종료
isRendering = false
renderingThread?.join() // 스레드 완료 대기
releaseResources()
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
// 크기 변경 시 렌더링 파라미터 업데이트
}
private fun startRenderingThread() {
// 새로운 백그라운드 스레드 생성
renderingThread = Thread {
while (isRendering) {
val canvas = holder.lockCanvas()
try {
// 렌더링 작업
draw(canvas)
} finally {
holder.unlockCanvasAndPost(canvas) // 반드시 unlock
}
}
}.apply { start() }
}
private fun releaseResources() {
// Paint, Bitmap 등 리소스 해제
renderingThread = null
}
}
핵심 관리 포인트:
스레드 안전 종료: join()으로 스레드 완료 대기
Canvas 해제: unlockCanvasAndPost() 반드시 호출
플래그 관리: isRendering으로 렌더링 루프 제어
리소스 해제: surfaceDestroyed()에서 모든 리소스 정리
메모리 누수 방지:
렌더링 스레드 적절한 종료
Canvas lock/unlock 쌍 맞추기
Paint, Bitmap 객체 해제
SurfaceHolder.Callback 등록 해제
SurfaceView의 핵심 장점:
메인 스레드 블로킹 방지: UI 스레드에서 무거운 렌더링 작업을 하지 않음
60fps 렌더링: 백그라운드에서 부드러운 프레임 렌더링 가능
성능 최적화: 메인 스레드는 UI 이벤트 처리에만 집중
Q2) 회전 및 스케일링과 같은 변환이 포함된 실시간 카메라 미리보기를 표시해야 하는 요구사항이 있을 때, SurfaceView와 TextureView 중 어떤 컴포넌트를 선택하겠는가? 구현 시 고려사항과 함께 선택한 이유를 설명하라.
TextureView 선택 이유:
변환 지원: 회전, 스케일링, 투명도 조절 가능
View 시스템 통합: 일반 View처럼 애니메이션 적용 가능
하드웨어 가속: GPU를 통한 효율적인 렌더링
SurfaceView 한계:
View 변환 불가 (항상 사각형)
Z-order 문제 (항상 최상위 또는 최하위)
애니메이션 제한
Q) 41. RecyclerView는 내부적으로 어떻게 작동하는가?
// LayoutManager: 아이템 배치 관리
// Adapter: 데이터와 ViewHolder 연결
// ViewHolder: View 재사용 단위
// RecycledViewPool: ViewHolder 캐시 저장소
스크롤 ↓
1. 화면에서 사라진 ViewHolder → Scrap으로 이동
2. 새로 필요한 ViewHolder → Pool에서 재사용 또는 새로 생성
3. onBindViewHolder()로 데이터 바인딩
4. 화면에 표시
// 4단계 캐시 시스템
// 1. Scrap: 레이아웃 중 임시 저장
// 2. Cache: 최근 사용된 ViewHolder (기본 2개)
// 3. ViewCacheExtension: 커스텀 캐시
// 4. RecycledViewPool: 타입별 ViewHolder 풀
Q1) RecyclerView의 ViewHolder 패턴은 ListView에 비해 어떻게 성능을 향상시키는가?
ListView의 문제점:
// 매번 findViewById 호출 (비효율적)
public View getView(int position, View convertView, ViewGroup parent) {
TextView textView = convertView.findViewById(R.id.textView); // 매번 호출
textView.setText(data.get(position));
return convertView;
}
ViewHolder 패턴:
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val textView: TextView = itemView.findViewById(R.id.textView) // 한 번만 호출
fun bind(data: String) {
textView.text = data
}
}
Q2) RecyclerView에서 ViewHolder의 생명주기를 생성부터 재활용까지 설명하라.
onCreateViewHolder(): ViewHolder 객체 생성
onBindViewHolder(): 데이터 바인딩
화면 표시: 사용자에게 보임
스크롤 아웃: 화면에서 사라짐
재활용 풀 이동: RecycledViewPool에 저장
재사용: 새로운 데이터로 재바인딩
Q3) RecycledViewPool이란 무엇이며, 뷰 아이템 렌더링을 최적화하기 위해 어떻게 사용할 수 있는가?
1. 여러 RecyclerView 간 ViewHolder 공유
val sharedPool = RecyclerView.RecycledViewPool()
recyclerView1.setRecycledViewPool(sharedPool)
recyclerView2.setRecycledViewPool(sharedPool)
// ViewHolder 캐시 크기 조정
sharedPool.setMaxRecycledViews(VIEW_TYPE_HEADER, 5)
sharedPool.setMaxRecycledViews(VIEW_TYPE_NORMAL, 20)
// 세로 스크롤 내부의 가로 스크롤 RecyclerView
class ParentAdapter : RecyclerView.Adapter<ParentViewHolder>() {
private val childPool = RecyclerView.RecycledViewPool()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ParentViewHolder {
val holder = ParentViewHolder(view)
holder.childRecyclerView.setRecycledViewPool(childPool) // Pool 공유
return holder
}
}
// Pool 없이: 각 RecyclerView마다 ViewHolder 생성
// Pool 공유: ViewHolder 재사용으로 메모리 절약 + 생성 비용 감소
// 측정 가능한 개선
// - 메모리 사용량: 30-50% 감소
// - 스크롤 성능: ViewHolder 생성 지연 제거
// - 배터리: GC 빈도 감소로 전력 절약
핵심 이점:
메모리 효율성: ViewHolder 재사용으로 객체 생성 최소화
성능 향상: 스크롤 시 ViewHolder 생성 지연 제거
배터리 절약: GC 압박 감소로 전력 소모 최적화
Q) 42. Dp와 Sp의 차이점은 무엇인가?
Dp (Density-independent Pixels):
기기의 화면 밀도와 무관하게 동일한 물리적 크기 보장
UI 요소 크기, 마진, 패딩에 사용
사용자 설정에 영향받지 않음
Sp (Scale-independent Pixels):
Dp와 유사하지만 사용자의 글꼴 크기 설정을 반영
텍스트 크기에만 사용 권장
접근성을 위해 중요
Q) 텍스트 크기에 sp를 사용할 때 발생할 수 있는 레이아웃 깨짐 문제는 무엇이며, 이를 어떻게 방지할 수 있는가?
레이아웃 깨짐 방지:
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="16sp"
android:maxLines="2"
android:ellipsize="end"
app:autoSizeTextType="uniform"
app:autoSizeMinTextSize="12sp"
app:autoSizeMaxTextSize="16sp" />
Q) 43. 나인 패치(Nine-Patch) 이미지는 어떤 용도로 사용되는가?
Nine-Patch는 크기 조절 가능한 PNG 이미지
Q) Nine-Patch 이미지는 일반 PNG 이미지와 어떻게 다르며, 어떤 시나리오에서 Nine-Patch 이미지를 사용해야 하는가?
구조:
모서리: 고정 영역 (늘어나지 않음)
가장자리: 1픽셀 검은 선으로 늘어날 영역 표시
중앙: 반복되어 늘어나는 영역
사용 시나리오:
채팅 말풍선 (크기 가변, 모서리 둥글게 유지)
버튼 배경 (텍스트 길이에 따라 크기 조절)
다이얼로그 배경
그림자 효과
Q) 44. Drawable이란 무엇이며, UI 개발에서 어떻게 사용되는가?
Drawable은 그릴 수 있는 모든 것의 추상화
Q) Drawable만을 사용하여 사용자 상호작용에 따라 모양과 색상이 변하는 동적 배경을 가진 버튼을 어떻게 만들 수 있는가?
동적 배경 구현:
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#FF6B6B" />
<corners android:radius="8dp" />
</shape>
</item>
<item android:state_enabled="false">
<shape android:shape="oval">
<solid android:color="#CCCCCC" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="#4ECDC4" />
<corners android:radius="16dp" />
</shape>
</item>
</selector>
Q) 45. 안드로이드에서 Bitmap이란 무엇이며, 대용량 Bitmap을 효율적으로 처리하려면 어떻게 해야하는가?
Bitmap은 픽셀 데이터를 메모리에 저장하는 객체
Q) 큰 Bitmap을 메모리에 로드할 때의 위험성은 무엇이며, 이를 어떻게 방지할 수 있는가?
위험성:
OutOfMemoryError: 큰 이미지는 엄청난 메모리 사용
메모리 누수: 적절히 해제하지 않으면 GC되지 않음
UI 블로킹: 메인 스레드에서 로딩 시 ANR 발생
방지 방법:
// 이미지 크기 미리 확인
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeFile(imagePath, options)
// 적절한 샘플링
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)
options.inJustDecodeBounds = false
val bitmap = BitmapFactory.decodeFile(imagePath, options)
// 사용 후 해제
bitmap.recycle()
Q) 46. 안드로이드에서 애니메이션을 어떻게 구현하는가?
// XML 정의
// res/anim/fade_in.xml
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:fromAlpha="0.0"
android:toAlpha="1.0"
android:duration="300" />
// 코드에서 사용
val animation = AnimationUtils.loadAnimation(context, R.anim.fade_in)
view.startAnimation(animation)
// ObjectAnimator - 단일 속성
ObjectAnimator.ofFloat(view, "alpha", 0f, 1f).apply {
duration = 300
start()
}
// ValueAnimator - 커스텀 값
ValueAnimator.ofFloat(0f, 100f).apply {
addUpdateListener { animator ->
val value = animator.animatedValue as Float
// 커스텀 로직
}
start()
}
// Compose에서 애니메이션
@Composable
fun AnimatedButton() {
var isPressed by remember { mutableStateOf(false) }
val scale by animateFloatAsState(
targetValue = if (isPressed) 0.95f else 1f,
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy)
)
Button(
modifier = Modifier.scale(scale),
onClick = { isPressed = !isPressed }
) { Text("Animate") }
}
// Frame Animation
val animationDrawable = imageView.drawable as AnimationDrawable
animationDrawable.start()
// Vector Drawable Animation
val animatedVector = ContextCompat.getDrawable(context, R.drawable.animated_vector)
imageView.setImageDrawable(animatedVector)
Q1) 버튼을 클릭했을 때 확장 및 축소되는 부드러운 애니메이션을 효율적으로 수행되도록 어떻게 구현하겠는가?
버튼 확장/축소 애니메이션:
fun animateButton(button: View) {
val scaleDown = ObjectAnimator.ofPropertyValuesHolder(
button,
PropertyValuesHolder.ofFloat("scaleX", 0.95f),
PropertyValuesHolder.ofFloat("scaleY", 0.95f)
).apply {
duration = 100
interpolator = DecelerateInterpolator()
}
val scaleUp = ObjectAnimator.ofPropertyValuesHolder(
button,
PropertyValuesHolder.ofFloat("scaleX", 1.0f),
PropertyValuesHolder.ofFloat("scaleY", 1.0f)
).apply {
duration = 100
interpolator = AccelerateInterpolator()
}
AnimatorSet().apply {
playSequentially(scaleDown, scaleUp)
start()
}
}
Q2) 전통적인 View 애니메이션 대신 MotionLayout을 언제 사용하며, 그 장점은 무엇인가?
전통적 애니메이션 한계:
여러 View의 복잡한 상호작용 어려움
키프레임 기반 애니메이션 복잡
제스처와 애니메이션 연동 어려움
MotionLayout 장점:
선언적 애니메이션 정의
키프레임과 제스처 지원
복잡한 전환 효과 쉽게 구현
Android Studio 편집기 지원
Q) 47. Window란 무엇인가?
Window는 화면에 표시되는 최상위 UI 컨테이너
일반적인 Window 구성:
Status Bar Window: 상태바 (시간, 배터리 등)
Navigation Bar Window: 내비게이션 바 (뒤로가기, 홈, 최근 앱)
Application Window: 실제 앱 콘텐츠
추가 Window:
Dialog Window (다이얼로그 표시 시)
Toast Window (토스트 메시지 시)
System Alert Window (시스템 오버레이)
Q) 간단한 레이아웃을 가진 Activity가 표시될 때 화면에 몇 개의 Window가 존재하며, 그것들은 무엇인가?
일반적으로 3개의 Window가 존재:
// 화면 최상단 - 시스템이 관리
// 시간, 배터리, 신호 강도, 알림 아이콘 등 표시
// WindowManager.LayoutParams.TYPE_STATUS_BAR
// 화면 하단 (제스처 네비게이션 시에도 존재)
// 뒤로가기, 홈, 최근 앱 버튼 또는 제스처 힌트
// WindowManager.LayoutParams.TYPE_NAVIGATION_BAR
// 실제 Activity 콘텐츠
// WindowManager.LayoutParams.TYPE_BASE_APPLICATION
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) // ← 이 Window
}
}
Window 계층 구조:
┌─────────────────────┐ ← Status Bar Window (최상위)
│ 🔋 📶 12:34 │
├─────────────────────┤
│ │ ← Application Window (중간)
│ Activity Content │
│ │
├─────────────────────┤
│ ◀ 🏠 ⬜ │ ← Navigation Bar Window (하단)
└─────────────────────┘
추가 Window (상황에 따라):
// Dialog 표시 시 +1개
AlertDialog.Builder(this).show() // Dialog Window 추가
// Toast 표시 시 +1개
Toast.makeText(this, "메시지", Toast.LENGTH_SHORT).show() // Toast Window 추가
// 시스템 오버레이 권한 시 +1개
// TYPE_APPLICATION_OVERLAY Window
결론: 기본적으로 3개의 Window (Status Bar + Navigation Bar + Application)가 존재하며, 추가 UI 요소에 따라 더 많은 Window가 생성될 수 있음
Q) 48. 웹 페이지는 어떻게 렌더링하는가?
WebView는 앱 내에서 웹 콘텐츠를 표시하는 컴포넌트
웹 페이지 렌더링 과정:
HTML 문서를 파싱하여 DOM(Document Object Model) 트리 생성
브라우저가 HTML 태그들을 해석하여 노드 구조로 변환
CSS 파일들을 파싱하여 CSSOM(CSS Object Model) 생성
스타일 규칙들을 파싱하고 우선순위 계산
DOM과 CSSOM을 결합하여 실제 화면에 표시될 요소들만 포함한 렌더 트리 생성
display: none 등으로 숨겨진 요소는 제외
각 요소의 정확한 위치와 크기 계산
뷰포트 내에서의 기하학적 속성 결정
실제 픽셀로 화면에 그리기
색상, 이미지, 텍스트 등을 래스터화
여러 레이어를 합성하여 최종 화면 생성
GPU 가속을 통한 효율적인 렌더링
Android WebView에서의 특징:
Chromium 엔진 기반으로 동일한 렌더링 파이프라인 사용
하드웨어 가속 지원으로 성능 최적화
네이티브 앱과의 JavaScript 브릿지 제공
Q) 외부 링크를 클릭했을 때 사용자가 앱을 떠나는 것을 방지하기 위해 WebView 내비게이션을 효과적으로 처리하는 방법은 무엇인가?
외부 링크 처리:
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
val url = request?.url.toString()
return if (url.startsWith("http://myapp.com") || url.startsWith("https://myapp.com")) {
// 내부 링크는 WebView에서 처리
false
} else {
// 외부 링크는 차단하거나 커스텀 처리
showExternalLinkDialog(url)
true
}
}
}
보안 고려사항:
JavaScript 비활성화 (필요시에만 활성화)
HTTPS 강제 사용
파일 접근 제한
쿠키 정책 설정
Bitmap이 메모리에 저장되는 방식
// Bitmap은 압축 해제된 픽셀 데이터를 저장
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.image)
println("Bitmap 메모리 사용량: ${bitmap.byteCount} bytes") // 예: 4,000,000 bytes
// 각 픽셀당 메모리 사용량
// ARGB_8888: 픽셀당 4바이트 (Alpha + Red + Green + Blue)
// RGB_565: 픽셀당 2바이트 (Red + Green + Blue)
val memoryUsage = width * height * bytesPerPixel
실제 메모리 계산 예시
// 1000x1000 이미지, ARGB_8888 포맷
val width = 1000
val height = 1000
val bytesPerPixel = 4 // ARGB_8888
val bitmapMemory = width * height * bytesPerPixel
println("Bitmap 메모리: ${bitmapMemory / (1024 * 1024)} MB") // 약 3.8MB
이미지 파일 형태로 압축
// Bitmap을 JPEG/PNG로 압축하여 ByteArray 생성
fun bitmapToByteArray(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int): ByteArray {
val stream = ByteArrayOutputStream()
bitmap.compress(format, quality, stream)
return stream.toByteArray()
}
// 압축 결과 비교
val originalBitmap = BitmapFactory.decodeResource(resources, R.drawable.large_image)
val compressedBytes = bitmapToByteArray(originalBitmap, Bitmap.CompressFormat.JPEG, 80)
println("원본 Bitmap: ${originalBitmap.byteCount} bytes") // 4,000,000 bytes
println("압축된 ByteArray: ${compressedBytes.size} bytes") // 200,000 bytes (약 95% 감소)
ByteArray가 저장하는 데이터
// Bitmap: 압축 해제된 픽셀 데이터
bitmap.getPixel(0, 0) // 0xFFFF0000 (빨간색 픽셀)
// ByteArray: 압축된 파일 데이터 (픽셀 아님!)
val jpegBytes = bitmapToByteArray(bitmap, JPEG, 80)
println(jpegBytes.take(4)) // [FF, D8, FF, E0] - JPEG 파일 헤더
// ByteArray 구성: 파일 헤더 + 메타데이터 + 압축 알고리즘별 인코딩된 데이터
핵심: ByteArray는 픽셀이 아닌 압축된 파일 형태로 저장
JPEG 압축 과정
// JPEG는 **손실** 압축 방식
// 1. 색상 공간 변환 (RGB → YUV)
// 2. DCT (이산 코사인 변환)
// 3. 양자화 (데이터 손실 발생)
// 4. 엔트로피 인코딩 (허프만 코딩)
val jpegBytes = bitmapToByteArray(bitmap, Bitmap.CompressFormat.JPEG, 80)
// 품질 80%로 압축하면 원본 대비 80-95% 용량 감소
PNG 압축 과정
// PNG는 무손실 압축 방식
// 1. 필터링 (예측 알고리즘)
// 2. DEFLATE 압축 (LZ77 + 허프만 코딩)
val pngBytes = bitmapToByteArray(bitmap, Bitmap.CompressFormat.PNG, 100)
// 무손실이므로 JPEG보다 용량 큼, 하지만 여전히 원본 Bitmap보다 작음
동일한 이미지의 다양한 형태별 용량
class ImageSizeComparison {
fun compareImageSizes() {
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.sample_image)
// 1. 메모리상의 Bitmap (압축 해제됨)
val bitmapSize = bitmap.byteCount
println("Bitmap (메모리): ${bitmapSize / 1024} KB") // 예: 3906 KB
// 2. JPEG 압축 (손실 압축)
val jpegBytes = bitmapToByteArray(bitmap, Bitmap.CompressFormat.JPEG, 80)
println("JPEG ByteArray: ${jpegBytes.size / 1024} KB") // 예: 156 KB (96% 감소)
// 3. PNG 압축 (무손실 압축)
val pngBytes = bitmapToByteArray(bitmap, Bitmap.CompressFormat.PNG, 100)
println("PNG ByteArray: ${pngBytes.size / 1024} KB") // 예: 980 KB (75% 감소)
// 4. WebP 압축 (구글 개발, 효율적)
val webpBytes = bitmapToByteArray(bitmap, Bitmap.CompressFormat.WEBP, 80)
println("WebP ByteArray: ${webpBytes.size / 1024} KB") // 예: 124 KB (97% 감소)
}
}
이미지 특성에 따른 압축률
// 단순한 이미지 (그라데이션, 단색)
val simpleImage = createSimpleGradient(1000, 1000)
val simpleCompressed = bitmapToByteArray(simpleImage, Bitmap.CompressFormat.JPEG, 80)
// 압축률: 매우 높음 (98% 이상)
// 복잡한 이미지 (노이즈, 디테일 많음)
val complexImage = loadDetailedPhoto()
val complexCompressed = bitmapToByteArray(complexImage, Bitmap.CompressFormat.JPEG, 80)
// 압축률: 낮음 (80-90%)
품질 설정에 따른 차이
fun compareQualitySettings(bitmap: Bitmap) {
val quality100 = bitmapToByteArray(bitmap, Bitmap.CompressFormat.JPEG, 100)
val quality80 = bitmapToByteArray(bitmap, Bitmap.CompressFormat.JPEG, 80)
val quality50 = bitmapToByteArray(bitmap, Bitmap.CompressFormat.JPEG, 50)
val quality20 = bitmapToByteArray(bitmap, Bitmap.CompressFormat.JPEG, 20)
println("품질 100%: ${quality100.size / 1024} KB") // 300 KB
println("품질 80%: ${quality80.size / 1024} KB") // 156 KB
println("품질 50%: ${quality50.size / 1024} KB") // 89 KB
println("품질 20%: ${quality20.size / 1024} KB") // 45 KB
}
네트워크 전송 최적화
// 서버로 이미지 전송 시
fun uploadImage(bitmap: Bitmap) {
// ❌ 비효율적: Bitmap 직접 전송 (불가능)
// val bitmapBytes = bitmap 자체를 바이트로 변환 불가
// ✅ 효율적: 압축된 ByteArray 전송
val compressedBytes = bitmapToByteArray(bitmap, Bitmap.CompressFormat.JPEG, 75)
uploadToServer(compressedBytes) // 원본 대비 90% 이상 용량 감소
}
로컬 저장 최적화
// 파일 저장 시
fun saveImageToFile(bitmap: Bitmap, file: File) {
// Bitmap을 압축하여 저장
val compressedBytes = bitmapToByteArray(bitmap, Bitmap.CompressFormat.JPEG, 85)
file.writeBytes(compressedBytes)
println("저장 공간 절약: ${(1 - compressedBytes.size.toFloat() / bitmap.byteCount) * 100}%")
}
결론
용량 감소의 핵심 원리:
Bitmap: 모든 픽셀의 색상 정보를 압축 없이 저장 (Raw 데이터)
ByteArray: 압축 알고리즘을 적용하여 중복성과 불필요한 정보 제거
압축률: 이미지 복잡도와 품질 설정에 따라 70-98% 용량 감소 가능
실제 활용:
네트워크 전송: 대역폭 절약
스토리지 저장: 저장 공간 절약
메모리 관리: 캐시 효율성 향상
압축률 비교
val bitmap = createBitmap(1920, 1080)
val jpegSize = compress(bitmap, JPEG, 80).size // 245KB
val webpSize = compress(bitmap, WEBP, 80).size // 164KB (33% 감소)
기술적 우위
JPEG: 8x8 DCT + 허프만 코딩 (2000년대 기술)
WebP: 4x4 DCT + 예측코딩 + 산술코딩 (2010년대 기술)
기능 비교
결론:
더 좋은 압축률 (JPEG 대비 25-35% 작음)
다양한 기능 (투명도, 애니메이션, 손실/무손실 압축, 프로그레시브 로딩)
웹 표준화 (구글 주도, 브라우저 지원 확대)
모바일 최적화 (데이터 절약, 배터리 효율성)