React Native

[React Native]URL 배너 생성 시 re-rendering 속도 최적화

Woonys 2022. 2. 18. 20:42
반응형

후.. 또 어찌어찌 문제 하나 해결해냈다. 이번 건 꽤나 끙끙 앓았던 문제여서 그런지 해결하고 나니 엄청 속이 시원해지는 magic..

 

1. Introduction: What is the problem?

 

지난 번 만들었던 라이브 커머스 방송에 접속했을 때 화면을 클릭하면 채팅창과 하트 날리는 버튼이 가려졌다 나타나는 기능에 더해, 실제 라이브 커머스 스트리밍에서 꼭 필요한 기능인 배너 이미지(하단 이미지 참조)를 만들었다. 스트리머가 URL을 입력하면 해당 URL로부터 이미지와 타이틀, description에 대한 메타데이터를 크롤링해 아래와 같이 배너가 만들어진다. 이를 해결하기 위해 react-native-url-preview 오픈 소스를 가져다가 사용했다.

 

 

문제는 오픈 소스로 제공하는 컴포넌트를 뜯어보면, URL에서 image, title, desc를 크롤링하는 기능과 렌더링하는 기능이 합쳐져 있다는 점이었다. 이게 왜 문제냐?

 

아래 코드를 보자. <TouchableWithoutFeedback> 컴포넌트는 화면 전체에 씌워져 있어 스트리밍 중인 방송 화면을 터치하면 특정한 리액션 없이 onPress에 대한 함수를 실행한다. 이 onPress는 visible에 관한 state를 변경해 아래에 있는 onPressLinkButton() 메소드를 실행한다(하단 코드 참조). 여기서 onPressLinkButtion()이 위의 배너를 그려주는 컴포넌트에 해당한다.

 

// index.js

return (
      <SafeAreaView style={styles.container}>
        {this.renderBackgroundColors()}
        {this.renderNodePlayerView()}
        <TouchableOpacity style={styles.btnClose} onPress={this.onPressClose}>
          <Image
            style={styles.icoClose}
            source={require('../../assets/close.png')}
            tintColor="white"
          />
        </TouchableOpacity>
        <TouchableWithoutFeedback style={styles.contentWrapper} onPress={this.onPressVisible}>
          <View style={styles.footerBar}>
            <View style={styles.head}>{this.onPressLinkButton()}</View>
            <View style={styles.body}>
              {this.renderChatGroup()}
              {this.renderListMessages()}
            </View>
          </View>
        </TouchableWithoutFeedback>
        <FloatingHearts count={countHeart} />
      </SafeAreaView>
    );
  }
}
//index.js

  onPressLinkButton = () => {
    const { isVisibleFooter } = this.state;
    if (isVisibleFooter) return <RNUrlPreview text="https://bit.ly/3rKSI7h" />;
  };

 

근데 코드를 보면, 해당 컴포넌트를 가렸다가 다시 보이게 할 때마다, 외부 url을 방문해 이미지와 타이틀, desc를 매번 긁어오는 식으로 구현이 되어 있다. 즉, 매번 리렌더링할 때마다 해당 컴포넌트에 대한 메소드를 통으로 실행하다보니 매우매우 느린 속도를 보여준다는 것!(하단 동영상 참조)

 

도식을 보면 이런 식이다. isVisible이 True가 될 때마다 해당 컴포넌트를 렌더링하는 함수가 매번 실행되는데, 문제는 url에서 크롤링하는 함수도 매번 실행되다보니 속도가 느리다는 것.

그러다 보니 아래처럼 url 배너만 그려오는 컴포넌트가 따로 렌더링된다.

 

 

 

2. Solution 1: Using Cache => 약간 개선했으나 본질적인 해결은 실패함.

이렇게 글을 쓰는 지금은 왜 해당 컴포넌트에 문제가 발생했는지를 알고 있지만, 저 문제를 직면했을 당시는 정확히 어디가 병목인지 파악하지 못했다. 가장 먼저 들었던 생각은 "다른 메타데이터는 크기가 무겁지 않으니 매번 긁어와도 느리지 않겠지만 이미지는 그렇지 않을 것이다. 따라서 캐시를 적용해보자"였다.

 

곧바로 캐시를 적용했다. 리액트 네이티브 오픈 소스인 React-Native-Fast-Image 를 적용해 이미지를 렌더링할 때 매번 긁어오지 않고 캐싱해서 사용함으로써 로딩 속도를 늦췄다.

// RNUrlPreview.js: 기존 오픈 소스 코드에서 이미지 렌더링하는 메소드

renderImage = (imageLink, faviconLink) => {
    if (imageLink) {
      return <Image style={styles.imageStyle} source={{ uri: imageLink }} {...styles.imageProps} />;
    }
    if (faviconLink) {
      return (
        <Image style={styles.faviconStyle} source={{ uri: faviconLink }} {...styles.imageProps} />
      );
    }
    return null;
  };

 

// RNUrlPreview.js: 수정한 코드
import FastImage from 'react-native-fast-image';

(...)

renderFastImage = (imageLink, faviconLink) => {
    if (imageLink) {
      return (
        <FastImage
          style={styles.imageStyle}
          source={{ uri: imageLink, priority: FastImage.priority.normal }}
          resizeMode={FastImage.resizeMode.contain}
        />
      );
    }
    if (faviconLink) {
      return (
        <FastImage
          style={styles.faviconStyle}
          source={{ uri: faviconLink, priority: FastImage.priority.normal }}
          resizeMode={FastImage.resizeMode.contain}
        />
      );
    }
    return null;
  };

 

 

하지만 문제는 여전했다. 이미지 렌더링 속도는 빨라졌지만 버튼 렌더링 자체 속도가 개선되지 않았다. 이유는 Intro에서도 설명했듯, 제공하는 오픈 소스의 컴포넌트가 외부 URL로부터 크롤링하는 메소드와 렌더링하는 파트가 하나로 합쳐져있었기 때문이다. 이미지를 캐싱하더라도 버튼을 렌더링할 때마다 Url 크롤링 메소드가 실행되기 때문. 따라서 오픈 소스 코드를 리팩토링할 필요가 있었다.

 

3. Solution 2: Open Source Refactoring - URL 크롤링 파트와 렌더링 파트를 분리

처음에는 "함수 자체를 캐싱할 수는 없을까?"를 고민했고, 이와 관련해 useCallback 등에 여러 유용한 hook 등에 대해 공부해볼 수 있었다. 하지만 usecallback을 써서 해당 함수를 기억한다고 해도 매번 url 크롤링 메소드는 실행될 것이라 판단했다.(이와 관련해서는 정글 선배님의 글을 확인하면 좋을 듯!)

 

오히려 코드 자체를 뜯어내는 것이 당장은 어려워보여도 코드 가독성뿐만 아니라 여러 측면에서 더 효율적이 될 것이라 생각해 오픈 소스 코드를 수정하기로 했다.

 

 

위 도식은 현재 구조이다. index.js에서 <RNUrlPreview /> 컴포넌트를 불러오면 해당 컴포넌트에 대응하는 .js가 실행되어 2,3을 실행해 4로 전달해주는 것. 이를 아래와 같이 변경했다. 처음에 스트리밍 방송에 접속할 때만 외부 url로부터 이미지 링크와 title, desc를 크롤링하고 이를 state에 저장한다.

 

이후에 화면을 클릭해 <RNUrlPreview /> 컴포넌트를 리렌더링할 때마다 getURL() 메소드를 실행하지 않고 기존에 state로 저장해놓은 값을 props로 RNUrlPreview.js에 넘겨준다. 여기서 Sol 1에서 소개했던 image cache까지 적용했다. 매번 새로 그릴 때마다 getURL이 실행되지 않더라도 이미지의 경우는 title, desc와 달리 이미지 주소 링크만 저장해두는 방식이라 외부 네트워크로의 io가 반드시 필요해진다. 이를 위해 캐싱을 적용했다.

 

이렇게 변경하면 RNUrlPreview.js에서는 index.js에서 넘겨주는 props를 받아 렌더링만 해주는 방식이다. 이렇게 변경하니 아래 영상과 같이 리렌더링 속도를 획기적으로 개선했다.

 

 

 

 

 

 

4. 회고

 

1. 제품을 만드는 경험이란 무엇인가.

 

완성하고 나니 한숨이 푹~나왔다. 이런 거구나. 처음 개발자가 되고 싶었던 이유는 입으로만 떠드는 반쪽짜리 창업가가 되고 싶지 않아서였다. 제품을 만든다는 건 어떤 것인지 경험해보고 싶었는데, 이런 느낌이구나, 하는 게 어렴풋이 다가왔다.

 

사실 이 문제를 푸는 내내 스트레스가 계속 쌓였다. 보기에는 정말 별 거 아닌 것 같은데 왜 이렇게 손댈 일이 많은 건지 답답해 미칠 노릇이었다. 여기에는 적지 않았지만 스타일 조정해가며 UI 맞추는 것도 쉽지 않았다.

 

완성하고 나니 깨달음이 왔다. 개발이 어려운 게 아니었다.

 

입으로 터는 건 정말 쉬운 일이었구나.

 

갑자기 반성이 확 밀려왔다. 이전 창업에서 나는 무엇을 떠들었던 것인가. 제품을 만든다는 게 이렇게 소중한 경험이라는 것을 새삼 느꼈다.

 

2. 그것도, 사용자를 생각하는 제품을 만드는 경험이란 무엇인가.

 

우리가 아무렇지 않게 휙휙 쓰는 앱이 그렇게 휙휙 쓸 수 있기까지 얼마나 많은 노력이 있었을까. 조금이라도 사용자가 불편함을 느끼지 않고 서비스에서 원하는 바를 만족하고 갈 수 있도록 애를 썼을까. 앱에 접속했을 때 보이는 단순한 UI 하나가 만들어지기까지, 그 UI가 사용자가 눈치도 못 채는 사이에 휙 그려질 수 있기까지 얼마나 많은 기술과 집념이 들어갔을까.

 

렌더링 속도 몇 초 빠르게 하는 거, 별 거 아닌 것처럼 보일 수 있다. 하지만 그 별 거 아닌 것들이 쌓이고 쌓여서 별 것이 되는 제품을 만든다. 사용자를 생각하는 제품이란 이런 것이구나. 입으로 떠드는 것과 직접 만들어보는 건 천지차이였다.

 

참 많은 깨달음을 안겨 줬던 문제 해결 경험이다. 한동안 잊기 힘들 듯.

 

3. 측정할 수 없으면 관리할 수 없고, 관리할 수 없으면 개선시킬 수 없다.

 

그래도 기술적인 회고 하나는 들어가야겠지.

 

사실 위의 글은 반쪽짜리 문제 해결 경험이다. 석사까지 했으면서 이런 글을 썼다는 건 어떤 면에서는 빵점에 가까울 수도 있다. 내가 해결한 문제를 정량적으로 측정하지 않았기 때문.

 

기존에는 렌더링에 몇 초가 걸렸고 이를 몇 초로 단축시켰다는 얘기가 없으면 위의 글은 허풍에 가까워진다. 물론, 위 문제 해결은 눈으로만 봐도 확연한 개선이 있기는 하나 수치로 증명할 수 없으면 말짱 도루묵이다. 문제는 어떻게 앱에서 렌더링 속도를 측정할 수 있는지를 모른다는 점..RNdebugger를 쓰면 된다고 얼핏 들었던 것 같아 이참에 적용해볼 생각이다.

 

명심하자. 무언가를 주장할 때는 확실하고 객관적인 근거가 있어야 한다. 다음에 문제 해결 글을 쓸 때는 이 점을 보완할 것!

 

 

4. (추가) React 라이프사이클에 대한 이해 높이기

 

 

위 문제를 해결하면서 가장 많이 봤던 그림 중 하나. 리액트에서 라이프 사이클에 대한 이해도가 높다면 위의 문제를 어떤 시점에 해결해야 할지 쉽게 감 잡을 수 있을 것(이 글이 도움이 되었다).

반응형