Glide를 이용한 이미지 로딩 상태 처리: 로딩 중, 성공, 실패 콜백 구현

서론

안드로이드 앱에서 이미지 로딩은 사용자 경험(UX)의 핵심 요소입니다. 네트워크 지연이나 서버 오류로 인해 이미지가 제대로 표시되지 않으면 사용자는 불만을 느낄 수 있으며, 이는 곧 앱 평가 하락으로 이어질 수 있습니다. Glide는 이러한 문제를 해결하기 위해 강력한 로딩 상태 관리 기능을 제공합니다. 본 문서에서는 Glide를 활용하여 이미지 로딩의 세 가지 주요 상태 — 로딩 중, 성공, 실패 — 를 효과적으로 처리하는 방법을 설명합니다.

RequestListener를 통한 상태 감지

Glide는 RequestListener 인터페이스를 통해 로딩 과정 전반에 걸쳐 이벤트를 캐치할 수 있도록 지원합니다. 이 인터페이스는 다음과 같은 두 가지 메서드를 포함하고 있습니다:

public interface RequestListener<R> {
    boolean onResourceReady(R resource, Object model, Target<R> target,
                           DataSource dataSource, boolean isFirstResource);

    boolean onLoadFailed(@Nullable GlideException e, Object model, Target<R> target,
                        boolean isFirstResource);
}

각 메서드는 부울 값을 반환하며, 이 값은 이벤트 전파 여부를 결정합니다. false를 반환하면 다음 리스너에게 전달되며, true일 경우 더 이상 전파하지 않습니다.

기본 구현 단계

1. UI 구성 요소 준비

각 상태에 맞는 UI 요소를 레이아웃에 정의해야 합니다. 아래 예제는 이미지, 프로그레스바, 오류 메시지를 포함한 기본 항목 레이아웃입니다.

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <ImageView
        android:id="@+id/image_view"
        android:layout_width="match_parent"
        android:layout_height="200dp" />

    <ProgressBar
        android:id="@+id/progress_bar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:visibility="visible" />

    <TextView
        android:id="@+id/error_message"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:gravity="center"
        android:text="이미지 로딩 실패"
        android:textColor="#FF0000"
        android:visibility="gone" />
        
</FrameLayout>

2. 리스너 로직 작성

뷰 홀더나 액티비티 내에서 RequestListener를 구현하여 각 상태에 따라 UI를 업데이트합니다.

private final RequestListener<Drawable> requestListener = new RequestListener<Drawable>() {
    @Override
    public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target,
                                  DataSource source, boolean isFirstResource) {
        progressBar.setVisibility(View.GONE);
        errorMessage.setVisibility(View.GONE);
        return false; // 기본 동작 유지 (이미지 설정 허용)
    }

    @Override
    public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target,
                               boolean isFirstResource) {
        progressBar.setVisibility(View.GONE);
        errorMessage.setVisibility(View.VISIBLE);
        Log.w("GlideLoad", "로딩 실패: " + e.getMessage());
        return true; // 기본 에러 플레이스홀더 방지
    }
};

3. Glide 요청에 리스너 연결

로딩 요청 시 listener() 메서드를 사용해 앞서 정의한 리스너를 첨부합니다.

Glide.with(context)
     .load(imageUrl)
     .placeholder(R.drawable.placeholder_loading)
     .error(R.drawable.image_error_fallback)
     .listener(requestListener)
     .into(imageView);

고급 활용 패턴

RecyclerView 최적화

리사이클러뷰에서 사용할 경우, 리스너를 뷰 홀더 내부 클래스로 선언하여 외부 참조 누수를 방지하는 것이 좋습니다.

public class ImageViewHolder extends RecyclerView.ViewHolder {
    private final ImageView imageView;
    private final ProgressBar progressBar;
    private final TextView errorMessage;

    private final RequestListener<Drawable> listener = new RequestListener<Drawable>() {
        @Override
        public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target,
                                      DataSource dataSource, boolean isFirst) {
            progressBar.setVisibility(View.GONE);
            errorMessage.setVisibility(View.GONE);
            return false;
        }

        @Override
        public boolean onLoadFailed(GlideException e, Object model, Target<Drawable> target,
                                   boolean isFirst) {
            progressBar.setVisibility(View.GONE);
            errorMessage.setVisibility(View.VISIBLE);
            return true;
        }
    };

    public void bind(String url) {
        Glide.with(itemView.getContext())
             .load(url)
             .listener(listener)
             .into(imageView);
    }
}

자동 재시도 로직

임시 네트워크 오류 대응을 위해 일정 횟수만큼 재시도하는 전략을 추가할 수 있습니다.

private int attemptCount = 0;
private static final int MAX_RETRY = 3;

private void loadWithRetry(String url) {
    Glide.with(context)
         .load(url)
         .listener(new RequestListener<Drawable>() {
             @Override
             public boolean onLoadFailed(GlideException e, Object model, Target<Drawable> target,
                                        boolean isFirstResource) {
                 if (attemptCount < MAX_RETRY) {
                     attemptCount++;
                     new Handler(Looper.getMainLooper()).postDelayed(() -> {
                         loadWithRetry(url);
                     }, (long) Math.pow(2, attemptCount) * 500); // 지수 백오프
                 } else {
                     progressBar.setVisibility(View.GONE);
                     errorMessage.setVisibility(View.VISIBLE);
                 }
                 return true;
             }

             @Override
             public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target,
                                           DataSource dataSource, boolean isFirstResource) {
                 attemptCount = 0;
                 progressBar.setVisibility(View.GONE);
                 errorMessage.setVisibility(View.GONE);
                 return false;
             }
         })
         .into(imageView);
}

주의사항 및 최적화 팁

메모리 누수 방지

액티비티 생명주기에 맞춰 Glide 요청을 적절히 정리해야 합니다.

@Override
protected void onDestroy() {
    super.onDestroy();
    Glide.with(this).clear(imageView); // 미완료 요청 취소
}

프로가드 설정

코드 난독화 환경에서 리스너가 제대로 작동하도록 보존 규칙을 추가하세요.

-keep class com.bumptech.glide.request.RequestListener { *; }
-keepclassmembers class * implements com.bumptech.glide.request.RequestListener {
    <init>(...);
}

결론

Glide의 RequestListener는 이미지 로딩 상태를 정교하게 제어할 수 있는 강력한 도구입니다. 로딩 인디케이터 표시, 실패 시 사용자 친화적인 피드백 제공, 자동 재시도 등 다양한 UX 개선 기법을 적용할 수 있습니다. 특히 리스트 환경에서는 성능과 안정성을 고려한 구조 설계가 중요하며, ViewHolder 내부에서 리스너를 관리하는 것이 바람직합니다.

태그: Glide Android Image Loading RequestListener RecyclerView

6월 7일 00:39에 게시됨