Demystifying UI Styling in React Native

2023년 4월 4일

이 글은 2023년 4월 4일 퍼블 팀 블로그에 먼저 게시되었습니다. 원문 보기

안녕하세요! 퍼블의 소프트웨어 엔지니어 정주형입니다. 퍼블은 기술 없이 나만의 플랫폼으로 수익을 창출할 수 있는 노코드 플랫폼 빌더입니다. 또한 퍼블은 나만의 브랜디드 앱을 만드는 멋진 서비스도 제공하고 있답니다.

이번 글에서는 React Native에서 흔히들 그렇다고 여기는 몇가지 잘못된 믿음들에 대해 알아 보고, 이를 실제로 테스트 해 본 결과를 공유 드리려 합니다.

Tribalism

몇 억개의 텍스트를 학습한 트랜스포머 모델이 1초마다 몇 천개의 답변을 생성하고 있는 이 시대에도, 여전히 소프트웨어 개발에는 부족적인 믿음(Tribalism)들이 존재하고 있습니다. 과학적인 탐구에서 도출된 결론이 아닌 ‘그렇다더라’ 혹은 ‘그랬었다’ 를 기반으로 한 잘못된 믿음들 말이죠.

React의 메인테이너 Dan Abramov의 트윗

React의 메인테이너 Dan Abramov의 트윗

어쩌면 이러한 부족적 믿음은 Chat GPT가 일상이 된 이 시대에 더 위험할지도 모르겠습니다. 그렇다더라-그랬었다 라는 내용이 웹에 퍼지게 되면, AI가 다시 이걸 학습하고, 유저는 다시 이를 퍼트리겠죠.

유명 SF 작가 테드 창의 말에 따르면 이와 같은 일들이 결국 인터넷을 흐릿한 jpeg로 만들어 버릴 것입니다. 우리가 개발자로써, 혹은 웹을 사용하는 한명의 유저로써 이러한 믿음을 경계해야 하는 이유입니다.

그리고 React Native에도 몇가지 부족적 믿음이 존재하고 있습니다. StyleSheet가 성능에 있어 이득을 가져온다는 믿음과 Inline Styling은 성능상 좋지 않으니 하지 말아야 한다는 믿음이 그것입니다.

먼저 말하자면, 이 믿음들은 틀렸습니다. 이 글을 쓰고 있는 현재 2023년 4월 4일 기준으로 StyleSheet API를 사용하는 것 혹은 Inline Styling은 퍼포먼스에 있어서 별다른 차이를 가져오지 않습니다.

Mythbuster

간단한 테스트를 통해 이를 증명해보도록 합시다. 테스트를 위해 Shopify에서 만든 React-Native-Performance 라이브러리와 React Native가 기본적으로 제공하는 Perf monitor를 사용하였습니다.

React-Native-Performance 라이브러리는 native 영역에서 UI가 그려진 후에 자바스크립트 레이어에 스크린이 렌더 될때까지 걸린 시간과 같은 정보를 전달하므로 실제로 얼마나 걸렸는지 알 수 있습니다.

첫번째 테스트

과연 StyleSheet API가 Plain object를 넣는 것 보다 성능상의 이득이 있는가?

테스트의 구조는 간단합니다. 네비게이션을 통해 StyleSheet API를 사용한 스크린과 Plain Object를 사용한 스크린을 오가면서 렌더링하는데 걸리는 시간을 체크합니다.

실제로 성능상의 이득을 가져온다면 StylsSheet API를 사용한 스크린을 렌더하는데 걸리는 시간이 더 적거나, 혹은 더 적은 메모리를 사용했을 것입니다.

테스트를 위해 사용한 벤치마크용 컴포넌트의 구조는 다음과 같습니다. 부하를 주기 위해서 2000개의 <Text /> 로 이루어져 있습니다.

export default function Component() {
  const renderComponents = () => {
    const components = [];

    for (let i = 0; i < 2000; i++) {
      components.push(
        <View key={i}>
          <Text style={customStyles.text}> Some TEXT</Text>
        </View>,
      );
    }

    return components;
  };

  return (
    <ReactNavigationPerformanceView
      interactive
      screenName={NavigationKeys.PLAIN_OBJECT}>
      <View style={customStyles.container}>
        <Text style={customStyles.text}>Plain Object USED</Text>
        {renderComponents()}
      </View>
    </ReactNavigationPerformanceView>
  );
}

테스트 결과는 어떻게 나왔을까요?

Plain Object / StyleSheet를 사용했을 경우

Plain Object / StyleSheet를 사용했을 경우

Plain Object를 사용한 경우와 StyleSheet를 사용한 경우 모두 RAM 사용에 있어 눈에 띄는 차이는 보이지 않았습니다.

그렇다면 실제 render를 하기까지 걸린 시간은 어떨까요? 이번에는 컴포넌트의 수에 따른 성능에 차이까지 보기 위하여 1000개의 <Text />, 2000개의 <Text /> 로 나누어서 테스트 하였습니다.

왼쪽이 Plain Object, 오른쪽이 StyleSheet API를 사용했을 경우

왼쪽이 Plain Object, 오른쪽이 StyleSheet API를 사용했을 경우

1000개의 <Text /> 를 Render 할 경우 두 경우 모두 실제로 Render하는데 걸린 시간인 timeToRenderMillis가 2.235초 / 2.205초 로 큰 차이를 보이지 않았습니다.

2000개로 컴포넌트 개수를 늘린 경우에도 결과는 3.846초 / 3.871초 로 차이를 보이지 않았습니다.

해당 테스트를 통해 StyleSheet를 사용하는 것이 별다른 퍼포먼스적 이득을 가져오지 않는다는 사실을 확인하였습니다. 어째서 이러한 결과가 나온 것일까요?

소스코드를 보면 허무하리만큼 당연한 결과라는 것을 알 수 있을 것입니다. 글을 쓰는 시점의 가장 최신 버전인 0.71 버전의 StyleSheet 소스 코드를 보도록 합시다.

  /**
   * Creates a StyleSheet style reference from the given object.
   */
  create<+S: ____Styles_Internal>(obj: S): $ReadOnly<S> {
    // TODO: This should return S as the return type. But first,
    // we need to codemod all the callsites that are typing this
    // return value as a number (even though it was opaque).
    if (__DEV__) {
      for (const key in obj) {
        if (obj[key]) {
          Object.freeze(obj[key]);
        }
      }
    }
    return obj;
  },

DEV 환경일 경우에 object를 freezing 해서 재할당을 방지하는 것 말고 특별한 코드가 보이지 않습니다. 즉 StyleSheet로 create 한 객체는 특별한 무언가가 아닌 단순히 인자로 받은 obj를 다시 return한 결과입니다. 따라서 StyleSheet를 사용하는 것과 Plain Object를 사용하는 것이 아무런 차이를 보이지 않는 것입니다.

그렇다면 StyleSheet가 퍼포먼스적 이득을 보여준다는 믿음은 왜 생겨난 것일까요? 답은 간단합니다. 2018년에 Release 된 React Native 0.57 버전까지는 이 말이 어느 정도는* 사실이었기 때문입니다.

0.57 버전 공식 문서 / 0.58 버전의 공식 문서

0.57 버전 공식 문서 / 0.58 버전의 공식 문서

StyleSheet의 공식 문서에서도 그러한 흔적을 확인할 수 있습니다. 0.58 버전부터는 Performance에 대한 언급이 아예 사라지게 되었습니다.

// 0.55 version의 stylesheet create 메서드
  
  /**
   * Creates a StyleSheet style reference from the given object.
   */
  create<+S: ____Styles_Internal>(
    obj: S,
  ): $ObjMap<S, (Object)=> StyleSheetInternalStyleIdentifier> {
    const result = {};
    for (const key in obj) {
      StyleSheetValidation.validateStyle(key, obj);
      result[key] = obj[key] && ReactNativePropRegistry.register(obj[key]);
    }
    return result;
  },

문서상으로는 0.57에서 해당 설명이 사라졌지만, 소스 코드상에서는 0.56버전부터 해당 내용이 사라졌습니다. 0.55 버전의 StyleSheet.create 메서드를 살펴보면 인자로 받은 Obj를 ReactNativePropRegistry 라는 특정 테이블에 등록해 놓는 것을 알 수 있습니다.

//..

  ReactNativePropRegistry.register = function(object) {
    var id = ++uniqueID;
    objects[id] = object;
    return id;
  };

인자로 받은 Object에 id를 부여해 문서에 나온 것처럼 새로운 obj를 생성하는 대신 해당 Registry에서 id를 조회, 필요한 값을 받아오는 방식인 것입니다.

그러나 어떠한 연유인지는 모르겠지만 모종의 이유로 해당 기능은 0.55 버전 이후부터 사라지게 되었습니다.

따라서 현재로써는 StyleSheet는 성능상의 이득을 가져오지 않습니다. 그러나 성능상의 이득이 없다고 해서 StyleSheet를 사용하는 것이 아무런 이득이 없는 것은 아닙니다. 공식 문서에 서술되어 있듯, 해당 방식을 선호한다면 스타일과 로직을 완전히 분리하여 가독성에서의 이점을 가져올 수 있습니다.

Theme과 media query등을 사용할 수 있는 React Native Extended StyleSheet 를 사용할 수 있으며 차후에 실제로 StyleSheet API를 통한 퍼포먼스 최적화가 지원된다면 별다른 수정 없이 그 이점을 누릴 수 있겠죠. 공식적인 API인 만큼 추천되는 방식임은 분명합니다.

두번째 테스트

Inline Styling은 정말 나쁜가?

export default function Component() {
  const renderComponents = () => {
    const components = [];

    for (let i = 0; i < 2000; i++) {
      components.push(
        <View key={i}>
          <Text
            style={{
              fontSize: 24,
              fontWeight: 'bold',
              color: '#333',
            }}>
            Some TEXT
          </Text>
        </View>,
      );
    }

    return components;
  };

  return (
    <ReactNavigationPerformanceView
      interactive
      screenName={NavigationKeys.INLINE_STYLE}>
      <View
        style={{
          flex: 1,
          alignItems: 'center',
          justifyContent: 'center',
          backgroundColor: '#59bcad',
        }}>
        <Text style={{fontSize: 24, fontWeight: 'bold', color: '#333'}}>
          InlineStyle USED
        </Text>
        {renderComponents()}
      </View>
    </ReactNavigationPerformanceView>
  );
}

StyleSheet를 사용했을 경우와 RAM 사용량에 있어서 별다른 차이를 보이지 않습니다.

StyleSheet / Inline

StyleSheet / Inline

Render Time 또한 1000개일 때 2.262초 / 2000개일때 3.855초로 테스트 1에서 진행했던 시간과 그다지 다르지 않은 것을 알 수 있습니다.

inline Styling은 보통 두가지 이유로 나쁜 것으로 여겨지곤 합니다. 퍼포먼스 이슈가 있다는 점과 코드 퀄리티에 좋지 않다는 점 때문입니다.

퍼포먼스 측면에서는 그 이유가 합당해 보입니다. React에서 해당 컴포넌트가 다시 렌더링 된다면 모든 오브젝트들을 새로 생성하기 때문입니다. 즉 React Native에서는 Inline Style로 새로 생성된 obejct들의 값을 native 사이드로 계속해서 보내게 될 것입니다. 만약에 StyleSheet나 Plain Object를 사용한다면 (컴포넌트 외부에 선언한) 레퍼런스 값이 같을 것이므로 새로운 값을 보내지 않겠죠.

그러나 위 결과에서 보이듯 실제 테스트를 해 본다면 이와 같은 차이는 매우 미미하거나 거의 없습니다. 따라서 React Native 앱 개발에 있어 퍼포먼스 관련된 측면을 걱정해서 Inline styling을 하지 않기로 결정했다면 이는 기우에 가깝다고 할 수 있을 것입니다.

따라서 Inline styling을 사용하고자 하는 의도가 확실하다면 사용해도 문제가 없다로 정리할 수 있을 것 같습니다. 실제로 퍼블에서도 Inline styling 방식을 택하고 있습니다. 관심사의 분리는 조금 힘들어지지만, 빠른 UI 수정과 좋은 DX를 얻을 수 있습니다 :)

글을 마치며

로마가 하루 아침에 만들어지지 않은 것처럼 부족적 믿음 또한 오랜 시간 쌓여서 만들어집니다.

개발을 하다가 일정이 촉박해서, 혹은 기능 개발이 우선이라 넘어가는 부분들이 모여서 잘못된 믿음을 만듭니다. 그리고 이러한 믿음들은 단시일 내에는 아니더라도, 언젠간 커다란 버그를 언젠가 낳게 됩니다.

부끄럽지만, 저도 이러한 믿음들을 그냥 받아들이곤 했습니다. useMemo를 쓰는게 성능에 더 좋다고 했으니까! 혹은 StyleSheet를 당연히 써야지. 더 좋은 거 아냐? — 이렇게요.

그러나 이제는 더 클린한 코드를 쓸 수 있도록 가끔씩 시간을 내서라도 이러한 믿음들에 대해 증명해 보고 확인해 보는 시간을 가지려고 합니다. 그러면 여기서 글을 마치도록 하겠습니다. 읽어 주셔서 감사합니다.

틀린 점이나 궁금한 점은 댓글로 남겨주세요!


Demystifying UI Styling in React Native was originally published in 퍼블 팀 블로그 on Medium, where people are continuing the conversation by highlighting and responding to this story.