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