개발하는거북이 어플 만들었어요

React Native

[React Native] 새로고침 시 이미지가 깜빡일 때(image flickering)

tedium._.dev 2024. 11. 24. 22:50

React Native를 이용해 서비스를 개발하던 중, UX 측면에서 좋지 않은 현상 하나를 겪었다. 새로고침 시 이미지가 깜빡이는 현상(flickering)이 생기고 있었다.
문제를 찾기 위해 디버깅을 해 보니, 그 이유는 AWS의 Presigned Url에 있었다. 이미지 관련 정보를 S3에서 동적으로 받아오는 형태로 아키텍처를 구성했는데, 동일한 리소스에 대한 요청에도 그때마다 다른 url 값을 반환하는 특성 상 Image 객체의 url이 변경되어, 원치 않는 리렌더링이 발생하는 것이었다.

 

이미지 flickering 현상

이미지 자동 캐싱을 해 주는 react-native-fast-image 라이브러리를 써 봤을 때에도 별다른 효과가 없었고, 이후 조금 더 근본적인 해결책을 찾게 되었다. patch 파일을 통해 라이브러리 코드를 직접 변경하면 된다.

 

잠깐 patch 파일이 무엇이며 왜 필요한지 간단하게 설명하자면

node_modules 폴더 내에서 관리되는 모든 라이브러리는 기본적으로 모두 package.json에서 dependency 형태로 관리된다. 그런데, 에러 등의 이유로 라이브러리를 직접 변경해야 한다면?

node_modules 속 그 라이브러리 코드를 직접 변경하는 것도 물론 가능하겠지만, 이러한 방식은 당연히 dependency 형태로 관리되는 라이브러리에 영구적인 영향을 줄 수 없다. 당장 node_modules를 삭제하고 재설치하는 것만으로도 변경 사항이 모두 날아갈 것이다.

그래서 등장한 것이 patch 파일이다. 특정 라이브러리에 대한 patch 파일을 영구적으로 유지하고, 이 patch 파일을 필요할 때마다 적용시킴으로써 변경이 필요한 사항에 대한 유지보수성을 높일 수 있다.

 

구글링을 하며 관련 이슈에 대한 patch 파일을 찾을 수 있었고, 아래의 순서로 문제를 해결할 수 있었다.


 

1. 먼저 patch-package를 설치한다(셋업 방법은 readme 파일을 통해 어렵지 않게 알 수 있다)

 

2. 아래 코드 내용을 복사하여 프로젝트 루트 디렉토리에 patches 파일을 생성, 파일명은 react-native-fast-image+8.6.3.patch로 한다(react-native-fast-image가 추후 버전 업데이트가 된다면, 해당 버전에 맞추어 명칭 변경 필요)

diff --git a/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java
index c7a7954..ca2b394 100644
--- a/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java
+++ b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java
@@ -68,6 +68,11 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl> imple
                         .getResourceDrawable(view.getContext(), source));
     }
 
+    @ReactProp(name = "useLastImageAsDefaultSource")
+    public void useLastImageAsDefaultSource(FastImageViewWithUrl view, @Nullable Boolean isActivated) {
+        view.useLastImageAsDefaultSource(isActivated);
+    }
+
     @ReactProp(name = "tintColor", customType = "Color")
     public void setTintColor(FastImageViewWithUrl view, @Nullable Integer color) {
         if (color == null) {
diff --git a/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java
index 34fcf89..4e3c633 100644
--- a/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java
+++ b/node_modules/react-native-fast-image/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java
@@ -1,5 +1,6 @@
 package com.dylanvann.fastimage;
 
+import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;
 import static com.dylanvann.fastimage.FastImageRequestListener.REACT_ON_ERROR_EVENT;
 
 import android.annotation.SuppressLint;
@@ -9,10 +10,12 @@ import android.graphics.drawable.Drawable;
 import androidx.annotation.Nullable;
 import androidx.appcompat.widget.AppCompatImageView;
 
+import com.bumptech.glide.GenericTransitionOptions;
 import com.bumptech.glide.RequestBuilder;
 import com.bumptech.glide.RequestManager;
 import com.bumptech.glide.load.model.GlideUrl;
 import com.bumptech.glide.request.Request;
+import com.bumptech.glide.request.transition.DrawableCrossFadeTransition;
 import com.facebook.react.bridge.ReadableMap;
 import com.facebook.react.bridge.WritableMap;
 import com.facebook.react.bridge.WritableNativeMap;
@@ -30,6 +33,7 @@ class FastImageViewWithUrl extends AppCompatImageView {
     private boolean mNeedsReload = false;
     private ReadableMap mSource = null;
     private Drawable mDefaultSource = null;
+    private Boolean mUseLastImageAsDefaultSource = false;
 
     public GlideUrl glideUrl;
 
@@ -47,6 +51,10 @@ class FastImageViewWithUrl extends AppCompatImageView {
         mDefaultSource = source;
     }
 
+    public void useLastImageAsDefaultSource(@Nullable Boolean isActivated) {
+        mUseLastImageAsDefaultSource = isActivated;
+    }
+
     private boolean isNullOrEmpty(final String url) {
         return url == null || url.trim().isEmpty();
     }
@@ -141,12 +149,11 @@ class FastImageViewWithUrl extends AppCompatImageView {
                             .load(imageSource == null ? null : imageSource.getSourceForLoad())
                             .apply(FastImageViewConverter
                                     .getOptions(context, imageSource, mSource)
-                                    .placeholder(mDefaultSource) // show until loaded
+                                    .placeholder(mUseLastImageAsDefaultSource ? this.getDrawable() : mDefaultSource) // show until loaded
                                     .fallback(mDefaultSource)); // null will not be treated as error
 
             if (key != null)
                 builder.listener(new FastImageRequestListener(key));
-
             builder.into(this);
         }
     }
diff --git a/node_modules/react-native-fast-image/dist/index.d.ts b/node_modules/react-native-fast-image/dist/index.d.ts
index 5abb7c9..7173cde 100644
--- a/node_modules/react-native-fast-image/dist/index.d.ts
+++ b/node_modules/react-native-fast-image/dist/index.d.ts
@@ -89,6 +89,7 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
      * Render children within the image.
      */
     children?: React.ReactNode;
+    useLastImageAsDefaultSource?: boolean;
 }
 export interface FastImageStaticProperties {
     resizeMode: typeof resizeMode;
diff --git a/node_modules/react-native-fast-image/ios/FastImage/FFFastImageView.h b/node_modules/react-native-fast-image/ios/FastImage/FFFastImageView.h
index e52fca7..08a0a6d 100644
--- a/node_modules/react-native-fast-image/ios/FastImage/FFFastImageView.h
+++ b/node_modules/react-native-fast-image/ios/FastImage/FFFastImageView.h
@@ -19,6 +19,6 @@
 @property (nonatomic, strong) FFFastImageSource *source;
 @property (nonatomic, strong) UIImage *defaultSource;
 @property (nonatomic, strong) UIColor *imageColor;
-
+@property (nonatomic, assign) BOOL useLastImageAsDefaultSource;
 @end
 
diff --git a/node_modules/react-native-fast-image/ios/FastImage/FFFastImageView.m b/node_modules/react-native-fast-image/ios/FastImage/FFFastImageView.m
index f710081..4a9e486 100644
--- a/node_modules/react-native-fast-image/ios/FastImage/FFFastImageView.m
+++ b/node_modules/react-native-fast-image/ios/FastImage/FFFastImageView.m
@@ -113,6 +113,12 @@ - (void) setDefaultSource: (UIImage*)defaultSource {
     }
 }
 
+- (void) setUseLastImageAsDefaultSource: (BOOL*)useLastImageAsDefaultSource {
+    if (useLastImageAsDefaultSource != _useLastImageAsDefaultSource) {
+        _useLastImageAsDefaultSource = useLastImageAsDefaultSource;
+    }
+}
+
 - (void) didSetProps: (NSArray<NSString*>*)changedProps {
     if (_needsReload) {
         [self reloadImage];
@@ -205,7 +211,7 @@ - (void) reloadImage {
 - (void) downloadImage: (FFFastImageSource*)source options: (SDWebImageOptions)options context: (SDWebImageContext*)context {
     __weak typeof(self) weakSelf = self; // Always use a weak reference to self in blocks
     [self sd_setImageWithURL: _source.url
-            placeholderImage: _defaultSource
+            placeholderImage: _useLastImageAsDefaultSource ? [super image] : _defaultSource
                      options: options
                      context: context
                     progress: ^(NSInteger receivedSize, NSInteger expectedSize, NSURL* _Nullable targetURL) {
diff --git a/node_modules/react-native-fast-image/ios/FastImage/FFFastImageViewManager.m b/node_modules/react-native-fast-image/ios/FastImage/FFFastImageViewManager.m
index 84ca94e..9b8ff8c 100644
--- a/node_modules/react-native-fast-image/ios/FastImage/FFFastImageViewManager.m
+++ b/node_modules/react-native-fast-image/ios/FastImage/FFFastImageViewManager.m
@@ -20,6 +20,7 @@ - (FFFastImageView*)view {
 RCT_EXPORT_VIEW_PROPERTY(onFastImageError, RCTDirectEventBlock)
 RCT_EXPORT_VIEW_PROPERTY(onFastImageLoad, RCTDirectEventBlock)
 RCT_EXPORT_VIEW_PROPERTY(onFastImageLoadEnd, RCTDirectEventBlock)
+RCT_EXPORT_VIEW_PROPERTY(useLastImageAsDefaultSource, BOOL)
 RCT_REMAP_VIEW_PROPERTY(tintColor, imageColor, UIColor)
 
 RCT_EXPORT_METHOD(preload:(nonnull NSArray<FFFastImageSource *> *)sources)

 

 

 

3. 아래 명령어를 통해, patches 폴더의 파일들을 읽어와 변경 사항을 node_modules에 적용시킨다.

npx patch-package

 

4. 정상적으로 실행되었다면, 아래와 같이 useLastImageAsDefaultSource 프로퍼티가 추가될 것이다. 해당 프로퍼티를 추가해 준다.

 

5. 이후 확인해보면, 잘 해결된 것을 볼 수 있다!

 

 

 

 

출처

https://www.cristiangutu.pro/react-native-fast-image-patch-to-fix-the-image-change-flickering/

 

react-native-fast-image - No flickering when changing the image source

What is react-native-fast-image?

www.cristiangutu.pro

 

'React Native' 카테고리의 다른 글

[React Native] Android에서 그림자가 잘릴 때  (2) 2024.11.07