콜스택에서 발행한 RN 최적화 문서인 React Native Optimization를 2023년에 공부했던 것 같은데,
2025년 버전도 새로 나와서 이 참에 정리해 봤다.

 

예전에 웹 개발 팀원분이 React Native도 DOM 기반이에요? 라고 질문했을 때 아닌 건 알았지만, 제대로 설명 못했던 경험이 있는데, 리액트 리렌더링 모델 내용을 정리하면서 그때 생각이 났다.
 
아직도 React와 React Native의 차이를 모르는 분이라면 해당 내용을 읽어보면 좋을 것 같다.
react-dom은 별도의 렌더러라는 것을 ...


 

앱이 ‘빠르다’는 것은 무엇을 의미할까?
 
ex.
1940년대에 뉴욕의 한 오피스 빌딩에서 느린 엘리베이터에 대해 한 직원이 불만을 호소했는데,
건물 관리자는 엘리베이터 속도를 실제로 높이는 대신 근처에 거울을 설치했다.
 
어떻게 됐을까? 사람들은 거울을 통해 자신을 바라보면서, 대기 시간을 줄이고 주위를 분산시킬 수 있었다.
 
즉, ⭐️ 실제로 기술적인 개선도 중요하지만, 인지적인 성능 개선도 중요하다.
-> 스플래시 화면, 스켈레톤 UI, 애니메이션과 같은 트릭을 통해 사용자 만족도를 높일 수 있다.
 
인지적 성능이 중요한 만큼, 기술적인 개선도 중요하다.
 


 
 
사용자 경험을 올리는 데 중요한 지표 2가지

  1. TTI (Time To Interactive) : 앱이 실행된 후 앱을 얼마나 빨리 사용할 수 있게 되는지 측정하는 지표
    → 앱 초기 로드 외에도 특정 화면까지의 접근하는 데 걸리는 시간도 포함된다.
  2. FPS (Frames Per Second) : 초당 프레임 수로 앱이 스크롤, 스와이프 등 사용자 상호작용에 얼마나 부드럽게 반응하는지 측정하는 지표

 


PART 1

React Native의 Javascript 및 React를 최적화하여 FPS를 개선하는 기술

- 콜스택의 리액트 네이티브 개발자 100명을 대상으로 한 내부 설문조사에 따르면,
모바일, TV, 데스크톱 리액트 네이티브 앱에서 직면하는 성능 문제의 80%가 자바스크립트 측면에서 발생함.

React Native Thread 구조

 

React re-rendering model

⭐️ 렌더링 시점은 React가 결정하고, 렌더링 방법은 별도의 렌더러가 결정한다.
 
✅ React의 렌더링 및 업데이트 모델

  • React는 상태(state)에 따라 UI를 렌더링하고 업데이트하는 역할을 한다.
  • 핵심 구성 요소
    • Public API 정의
    • 크로스 플랫폼 기능
    • Reconciliation 알고리즘
      : 상태 변화에 따라 UI를 효율적으로 업데이트

✅ "What"과 "How"의 분리

  • React는 "무엇을 렌더링할지(what)"만 신경 쓰고,
  • 실제 "어떻게 렌더링할지(how)"별도의 렌더러가 담당함

    → 각 플랫폼에 따른 렌더러 예시:

    • react-dom: 웹용
    • react-native: 모바일(Android, iOS)
    • react-native-windows: 윈도우용
  • 이 구조 덕분에 하나의 컴포넌트 기반 코드로 다양한 플랫폼에서 동시에 UI를 구성 가능

✅ Virtual DOM과 실제 DOM의 비교 및 업데이트 (react-dom을 사용하는 경우)

  • React는 변경된 Model을 기반으로 새로운 Virtual DOM을 생성
  • 이전 Virtual DOM과 비교(diff) 후,
  • 변경된 부분만 패치(patch) 방식으로 실제 DOM에 반영
    → 최소한의 업데이트만 수행함 (성능 최적화)

✅ 컴포넌트가 다시 렌더링 되는 경우

  1. 부모 컴포넌트가 리렌더링 될 때
  2. 상태(state, hooks 포함)가 변경될 때
  3. props가 변경될 때
  4. context가 변경될 때
  5. 강제로 업데이트할 때 (forceUpdate() 등)
  • React는 공통의 Reconciliation 알고리즘을 통해 어떤 컴포넌트를 업데이트해야 할지 판단한다.
  • Reconciliation은 웹이든, React Native든 공통된 로직

 


 
 
다음은 JS와 React 코드를 프로파일링 하여 FPS 속도를 높이고, JS 메모리 누수를 추적하여 개선하는 방법에 대해 알아보자.

 

Android 앱 개발을 하다보면 보안을 위해서 출시 전에는 꼭 난독화를 해야한다.

 

 

Android 소스 난독화란?

 

앱의 소스 코드를 난독화하는 기술로,

안드로이드 앱의 소스 코드를 분석하기 어렵게 만들기 위한 기술입니다.

이를 통해 소스 코드를 더욱 안전하게 보호할 수 있습니다.

 

해커가 앱의 코드를 이해하고 악성 기능을 추가하는 것을 어렵게 만들 수 있습니다.

그렇기 때문에 앱을 배포하기 전 앱 소스 난독화는 꼭 필요합니다.

 

 

 

Proguard 적용하기

 

 

Proguard는 소스코드를 난독화 및 최적화 해주는 무료 오픈소스 툴입니다.

 

minifyEnabled

축소, 난독화 및 코드 최적화를 활성화 / 비활성화 한다.

 

getDefaultProguardFile("proguard-android.txt")

Android Gradle 플러그인과 함께 패키지된 기본 Proguard 규칙 파일이 포함되어 있다. 

 

proguard-rules.pro 

프로젝트 레벨에 proguard-rules.pro 파일을 만들고 축소, 난독화 제외 클래서, shrink 여부 등에 관한 룰을 작성한다.

 

 

dex2jar 라이브러리를 사용해서 디컴파일하기

 

1. dependency 설치

brew install apktool dex2jar

 

2. apk 빌드

난독화한 앱 apk 빌드 파일을 가져옵니다.

 

3. 디컴파일

cd /path/to/apk/file
apktool d -s -o decompile app-release.apk

 

4. dex 파일을 jar로 변경

d2j-dex2jar classes.dex
d2j-dex2jar classes2.dex
d2j-dex2jar classes3.dex

 

5. jar파일을 zip파일로 변경

mv classes-dex2jar.jar classes-dex2jar.zip
mv classes2-dex2jar.jar classes2-dex2jar.zip
mv classes3-dex2jar.jar classes3-dex2jar.zip

 

6. zip 파일 해제

finder에서 zip 파일을 더블클릭하여 압축을 해제합니다.

 

7. 코드 확인

압축 해제된 디렉터리에서 파일 내용을 확인해봅니다.

아래처럼 사람이 읽을 수 없다면 성공한 것이다 !!!!

 

 

앱 출시를 위해서 난독화해본 건 처음이라, 디컴파일하면서 많이 애먹었다.

누군가 이 글을 보고 많은 도움이 되었으면 좋겠다 ㅎㅎ

xcode 에서 iOS 개발 시 

http 통신할 때 다음과 같은 에러가 나올 경우가 있다.

since it does not conform to ATS policy ...

 

해당 에러가 났을 때 처음 시도해볼 수 있는 방법은

모든 도메인을 허용해주는 방법이다.

Info.plist > ATS > Allow Arbirarty Loads를 YES로 바꾼다.

 

 

하지만, Allow Arbirarty Loads 설정은 모든 웹사이트에 대한 보안성을 자체적으로 떨어뜨리기 때문에 앱 심사에서도 거절당할 수 있다.

 

 

✔️ HTTP인 도메인 일부만 허용

안전하게 특정 도메인만 추가시켜보자.

 

1. nscurl 명령어를 사용하여 ATS 관련정보를 가져온다.

터미널에 아래 명령어를 입력하자.
nscurl https://example.com --verbose --ats-diagnostics

 

 

Starting ATS Diagnostics

Configuring ATS Info.plist keys and displaying the result of HTTPS loads to https://otapi.net/service-json.
A test will "PASS" if URLSession:task:didCompleteWithError: returns a nil error.
================================================================================

Default ATS Secure Connection
---
ATS Default Connection
ATS Dictionary:
{
}
Result : PASS
---

================================================================================

Allowing Arbitrary Loads

---
Allow All Loads
ATS Dictionary:
{
    NSAllowsArbitraryLoads = true;
}
Result : PASS
---

================================================================================

Configuring TLS exceptions for otapi.net

---
TLSv1.3
ATS Dictionary:
{
    NSExceptionDomains =     {
        "example.com" =         {
            NSExceptionMinimumTLSVersion = "TLSv1.3";
        };
    };
}
Result : FAIL
Error : Error Domain=NSURLErrorDomain Code=-1200 "An SSL error has occurred and a secure connection to the server cannot be made." UserInfo={NSErrorFailingURLStringKey=https://otapi.net/service-json, NSLocalizedRecoverySuggestion=Would you like to connect to the server anyway?, _kCFStreamErrorDomainKey=3, _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <1DF7A198-ED54-4AF2-9B93-D89AF25FAA8C>.<1>, _NSURLErrorRelatedURLSessionTaskErrorKey=(
    "LocalDataTask <1DF7A198-ED54-4AF2-9B93-D89AF25FAA8C>.<1>"
), NSLocalizedDescription=An SSL error has occurred and a secure connection to the server cannot be made., NSErrorFailingURLKey=https://otapi.net/service-json, NSUnderlyingError=0x130804220 {Error Domain=kCFErrorDomainCFNetwork Code=-1200 "(null)" UserInfo={_kCFStreamPropertySSLClientCertificateState=0, _kCFNetworkCFStreamSSLErrorOriginalValue=-9836, _kCFStreamErrorDomainKey=3, _kCFStreamErrorCodeKey=-9836, _NSURLErrorNWPathKey=satisfied (Path is satisfied), viable, interface: en0[802.11], ipv4, dns, uses wifi}}, _kCFStreamErrorCodeKey=-9836}
---

---
TLSv1.2
ATS Dictionary:
{
    NSExceptionDomains =     {
        "example.com" =         {
            NSExceptionMinimumTLSVersion = "TLSv1.2";
        };
    };
}
Result : PASS
---

---
TLSv1.1
ATS Dictionary:
{
    NSExceptionDomains =     {
        "example.com" =         {
            NSExceptionMinimumTLSVersion = "TLSv1.1";
        };
    };
}
Result : PASS
---

---
TLSv1.0
ATS Dictionary:
{
    NSExceptionDomains =     {
        "example.com" =         {
            NSExceptionMinimumTLSVersion = "TLSv1.0";
        };
    };
}
Result : PASS
---

================================================================================

Configuring PFS exceptions for otapi.net

---
Disabling Perfect Forward Secrecy
ATS Dictionary:
{
    NSExceptionDomains =     {
        "example.com" =         {
            NSExceptionRequiresForwardSecrecy = false;
        };
    };
}
Result : PASS
---

================================================================================

Configuring PFS exceptions and allowing insecure HTTP for otapi.net

---
Disabling Perfect Forward Secrecy and Allowing Insecure HTTP
ATS Dictionary:
{
    NSExceptionDomains =     {
        "example.com" =         {
            NSExceptionAllowsInsecureHTTPLoads = true;
            NSExceptionRequiresForwardSecrecy = false;
        };
    };
}
Result : PASS
---

================================================================================

Configuring TLS exceptions with PFS disabled for otapi.net

---
TLSv1.3 with PFS disabled
ATS Dictionary:
{
    NSExceptionDomains =     {
        "example.com" =         {
            NSExceptionMinimumTLSVersion = "TLSv1.3";
            NSExceptionRequiresForwardSecrecy = false;
        };
    };
}
Result : FAIL
Error : Error Domain=NSURLErrorDomain Code=-1200 "An SSL error has occurred and a secure connection to the server cannot be made." UserInfo={NSErrorFailingURLStringKey=https://otapi.net/service-json, NSLocalizedRecoverySuggestion=Would you like to connect to the server anyway?, _kCFStreamErrorDomainKey=3, _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <43E82F5E-2733-4BFD-92F7-05553EEFB139>.<1>, _NSURLErrorRelatedURLSessionTaskErrorKey=(
    "LocalDataTask <43E82F5E-2733-4BFD-92F7-05553EEFB139>.<1>"
), NSLocalizedDescription=An SSL error has occurred and a secure connection to the server cannot be made., NSErrorFailingURLKey=https://otapi.net/service-json, NSUnderlyingError=0x130604a50 {Error Domain=kCFErrorDomainCFNetwork Code=-1200 "(null)" UserInfo={_kCFStreamPropertySSLClientCertificateState=0, _kCFNetworkCFStreamSSLErrorOriginalValue=-9836, _kCFStreamErrorDomainKey=3, _kCFStreamErrorCodeKey=-9836, _NSURLErrorNWPathKey=satisfied (Path is satisfied), viable, interface: en0[802.11], ipv4, dns, uses wifi}}, _kCFStreamErrorCodeKey=-9836}
---

---
TLSv1.2 with PFS disabled
ATS Dictionary:
{
    NSExceptionDomains =     {
        "example.com" =         {
            NSExceptionMinimumTLSVersion = "TLSv1.2";
            NSExceptionRequiresForwardSecrecy = false;
        };
    };
}
Result : PASS
---

---
TLSv1.1 with PFS disabled
ATS Dictionary:
{
    NSExceptionDomains =     {
        "example.com" =         {
            NSExceptionMinimumTLSVersion = "TLSv1.1";
            NSExceptionRequiresForwardSecrecy = false;
        };
    };
}
Result : PASS
---

---
TLSv1.0 with PFS disabled
ATS Dictionary:
{
    NSExceptionDomains =     {
        "example.com" =         {
            NSExceptionMinimumTLSVersion = "TLSv1.0";
            NSExceptionRequiresForwardSecrecy = false;
        };
    };
}
Result : PASS
---

================================================================================

Configuring TLS exceptions with PFS disabled and insecure HTTP allowed for otapi.net

---
TLSv1.3 with PFS disabled and insecure HTTP allowed
ATS Dictionary:
{
    NSExceptionDomains =     {
        "example.com" =         {
            NSExceptionAllowsInsecureHTTPLoads = true;
            NSExceptionMinimumTLSVersion = "TLSv1.3";
            NSExceptionRequiresForwardSecrecy = false;
        };
    };
}
Result : FAIL
Error : Error Domain=NSURLErrorDomain Code=-1200 "An SSL error has occurred and a secure connection to the server cannot be made." UserInfo={NSErrorFailingURLStringKey=https://otapi.net/service-json, NSLocalizedRecoverySuggestion=Would you like to connect to the server anyway?, _kCFStreamErrorDomainKey=3, _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <6D229C48-D555-4983-86A8-3923879F6CB2>.<1>, _NSURLErrorRelatedURLSessionTaskErrorKey=(
    "LocalDataTask <6D229C48-D555-4983-86A8-3923879F6CB2>.<1>"
), NSLocalizedDescription=An SSL error has occurred and a secure connection to the server cannot be made., NSErrorFailingURLKey=https://otapi.net/service-json, NSUnderlyingError=0x151a12a00 {Error Domain=kCFErrorDomainCFNetwork Code=-1200 "(null)" UserInfo={_kCFStreamPropertySSLClientCertificateState=0, _kCFNetworkCFStreamSSLErrorOriginalValue=-9836, _kCFStreamErrorDomainKey=3, _kCFStreamErrorCodeKey=-9836, _NSURLErrorNWPathKey=satisfied (Path is satisfied), viable, interface: en0[802.11], ipv4, dns, uses wifi}}, _kCFStreamErrorCodeKey=-9836}
---

---
TLSv1.2 with PFS disabled and insecure HTTP allowed
ATS Dictionary:
{
    NSExceptionDomains =     {
        "example.com" =         {
            NSExceptionAllowsInsecureHTTPLoads = true;
            NSExceptionMinimumTLSVersion = "TLSv1.2";
            NSExceptionRequiresForwardSecrecy = false;
        };
    };
}
Result : PASS
---

---
TLSv1.1 with PFS disabled and insecure HTTP allowed
ATS Dictionary:
{
    NSExceptionDomains =     {
        "example.com" =         {
            NSExceptionAllowsInsecureHTTPLoads = true;
            NSExceptionMinimumTLSVersion = "TLSv1.1";
            NSExceptionRequiresForwardSecrecy = false;
        };
    };
}
Result : PASS
---

---
TLSv1.0 with PFS disabled and insecure HTTP allowed
ATS Dictionary:
{
    NSExceptionDomains =     {
        "example.com" =         {
            NSExceptionAllowsInsecureHTTPLoads = true;
            NSExceptionMinimumTLSVersion = "TLSv1.0";
            NSExceptionRequiresForwardSecrecy = false;
        };
    };
}
Result : PASS
---

================================================================================

 

FAIL이 난 곳을 집중적으로 보면 된다.

 

여기서 볼 수 있듯이, example.com 는 TLSv1.3을 지원하지 않고

Minimum TLS Version을 1.3으로 설정해줘야 에러가 발생하지 않는다.

 

2. Info.plist 에 아래처럼 추가한다.

 

nscurl 명령어를 사용하여 원인을 파악하는데 큰 도움이 되었다 🫢

최근에 웹뷰인 React Native 앱 서비스를 다뤄보는 경험을 했다.

앱 내에서 웹뷰를 띄워보는 희귀한 경험을 해서 꼭 블로그에 작성해야겠다고 다짐했다!

 

 

앱 내에서 webview를 띄우면서 가장 중요하고자 어려웠던 부분을 기록하고자 한다.

 

보통 앱 내에서 사용자들의 히스토리를 기록하고 로깅하고자 로그 이벤트를 기록한다.

그러기 위해서는 웹뷰에서 일어난 이벤트들을 RN에서 감지해야 한다.

 

 

목차

1. React-native와 Webview 통신

2. 내가 처했던 문제 상황


React-native와 Webview 통신

1. Web -> RN

 

Web: postMesageKey 전달

 

RN: postMessageKey event를 받아 동작시킨다.

 

위의 예시에서 test data를 받도록 해놨는데, 만약 data를 넘겨줘야 한다면 위의 방식과 같이 넘겨주면 된다.

 

 

2. RN -> Web

 

RN: WebView 속성 중 onLoadStart를 사용해서 들어가자마자 data 전송

 

RN: data 안에 isPremium 값을 보내준다.

 

Web: Type 지정

 

Web: data 안에 우리가 지정해준 data 타입으로 받는다.

 


내가 처했던 문제 상황

What?

Web에서 RN에 데이터를 전송하는 것은 굉장히 easy 했다. 

위의 코드처럼 RN에서 Web으로 데이터를 전송할 때

웹뷰 로드가 시작되면 데이터가 바로 전송되야 했다.

하지만 RN 측에서는 데이터를 잘보내줌에도 불구하고, 웹뷰에서는 첫 렌더링 때 값을 가져오지 못하는 문제가 발생했다.

 

 

 

How Solve?

값을 보내주지 말고, localStorage에 저장한 후 웹뷰가 로딩될 때마다 가져오자!

 

RN에서 해당 값을 localStorage에 저장한다.

 

Web: didMount 될 때마다 localStorage에서 값을 가져온다.

 

이렇게 문제를 해결할 수 있었다. 

 

 

 

 

이 블로그를 통해 누군가 도움이 되었다면 그것만으로 만족스럽다 ㅎ

Fastlane 배포 자동화 적용 과정

 

항상 효율적으로 반복되는 시스템을 자동화하려고 하는데,

그 중 하나가 앱 배포 시간을 줄이기 위한 CI/CD 구축하는 것이다.

 

안드로이드는 배포할 때 생각보다 간단하지만,

iOS는 아카이브 한 후 앱스토어 커넥트에 업로드 한 뒤 심사 요청까지 거쳐야하는 귀찮고 번거로운 작업들이 발생한다.

 

좋은 코드를 만드는 것도 좋지만, 이러한 반복적인 코드들을 자동화하여 업무의 효율을 높이는 것이 중요하다.

 

예전에 적용했던 fastlane 자동화 일련의 과정을 공유해보려고 한다.

 


 

iOS 배포

  • Fastlane 설치
# ruby가 이미 설치되어 있다면 생략
$ brew install ruby

# Fastlane 설치
$ gem install fastlane
  • iOS 설정
# /ios
$ fastlane init
iOS fastlane init시 나오는 화면 (4가지 중 1개 선택)
  • 일단 테스트 플라이트에 올릴 때 사용하기 위해 2번을 선택했다.
    + 추가) 나중에 또 추가할 수 있으니 신중하게 선택 안해도 된다.

apple ID를 입력하라고 나오는데
애플 아이디 입력 후 숫자 6자리를 입력했더니 이런 오류가 나서, 다시 n을 누르니 이중 로그인 절차를 한 번 더 거치고 완료 됐다.

 
로그인 성공 시 Appfile과 Fastfile이 생성된다.
  • AppFile
app_identifier("your.app.identifier") # The bundle identifier of your app
apple_id("your-apple-id") # Your Apple email address

Appfile에는 app bundle id와 apple id가 포함되어 있는 걸 확인할 수 있다.

우리 팀은 애플아이디를 공유하고 있지 않고, 각자의 계정을 사용하고 있다.
각자가 배포할 때마다 apple id를 바꿔줘야하면 불편함이 생기므로 이것을 .env 를 이용해 환경변수로 관리한다.

 

  • .env
# /ios 루트 안에 해당 파일을 만든다
$ vim ./fastlane/.env
APP_IDENTIFIER="your.app.identifier"
APPLE_ID="my-apple-id@email.com"
...

이렇게 넣어준 뒤 Appfile을 수정하자.

app_identifier(ENV["APP_IDENTIFIER"]) # The bundle identifier of your app
apple_id(ENV["APPLE_ID"]) # Your Apple Developer Portal username
  • Fastfile

실제 커맨드 명령들을 만드는 파일이다.

# Uncomment the line if you want fastlane to automatically update itself
# update_fastlane
default_platform(:ios)
platform :ios do
  desc "Push a new beta build to TestFlight"
  lane :beta do
     # ✅ 자동으로 빌드 넘버를 증가
    increment_build_number(xcodeproj: "xxxx.xcodeproj")
     # ✅ 빌드할 워크스페이스와 빌드 스키마 지정
    build_app(workspace: "xxxx.xcworkspace", scheme: "xxxx")
    # ✅ TestFlight 업로드
    upload_to_testflight(
      # ✅ 업로드 후에 App Store Connect 에 올라가기 전까지 시간이 걸리는데 이걸 기다리고 싶지 않다면 true 로 설정.
        skip_waiting_for_build_processing: true
      )
    slack(
      message: "Testflight 배포 성공",
      channel: "#r_frontend_deploy",
      slack_url:  ENV["SLACK_WEBHOOK_URL"],
    )
  end
  error do |lane, exception, options|
    slack(
      message: "에러 발생 : #{exception}",
      success: false,
      slack_url: ENV["SLACK_WEBHOOK_URL"],
    )
  end
  
end

 

  • SLACK_WEBHOOK_URL 설정

Slack에서 원하는 채널을 선택해서 Webhook을 설정하고, 해당 url을 통해서 요청을 보내면 해당 메세지가 전달되는 방식이다.

https://my.slack.com/services/new/incoming-webhook/ 에 들어가서 설정해보자

슬랙 채널을 하나 생성하고 추가를 하면 이렇게 웹후크 URL이 생성되는데 이를 복사하고, 설정 저장을 누르면 끝!

 

  • TestFlight 배포 설정

원래 실행 명령어는 fastlane ios beta이다. 하지만 우리가 플랫폼을 iOS로 미리 설정해두었기 때문에 이 명령어가 실행 가능하다.

fastlane beta

TIP. 명령어를 실행하기 전에 ios에서는 앱 암호를 발급받아 환경변수로 적용을 해줘야하고, android에서는 서비스 어카운트라는 것을 만들어 접근권한을 부여해주어야 한다.

 

마주했던 에러

error. 앱 암호 생성 에러

FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD variable

https://support.apple.com/ko-kr/HT204397 해당 링크를 들어가 암호를 설정하자.

앱 암호를 만들고 만들어진 암호를 입력해주면 된다.
이것은 매번 fastlane을 사용할 때 자동적으로 앱 암호로 로그인하게 하기 위함이다!

Could not store password in keychain

앱 암호는 ‘xxxx-xxxx-xxxx-xxxx’ 이렇게 만들어진 암호를 붙여넣으면 된다!
이 암호로 처음에 붙이면 에러가 나는데, 두번째 성공하면 upload가 된다. 이유는 모르겠음..!!

추가적으로 릴리즈를 위한 앱의 자동 배포까지 하고 싶다면
참고한 자료 https://gyuios.tistory.com/241 에서 확인해보자


 

Android 배포

$ cd my-project/android
$ fastlane init

작업하기 전 체크 사항

  • 구글 플레이스토어 어드민 계정 필요
  • Google Play Console에 앱 등록
  • Google Credential 발급

https://docs.fastlane.tools/actions/supply/ 에 들어가서 json 파일까지 발급 받아서 Appfile에 넣고 온다.


JSON Secret 설정 및 권한 부여

json 파일을 등록하고,
아래 명령어를 통해 다운받은 private key로 구글플레이스토어와 연결이 잘되는지 확인해보자.

$ fastlane run validate_play_store_json_key json_key:/path/to/your/downloaded/file.json
Google Play Store에 연결 성공

git에 올라가면 안되는 파일 .env나 json 파일은 .gitignore에 저장한다!

 

  • Firebase App Distribution 추가

테스트앱을 내기 위해서 Firebase App Distribution에 올리기 때문에 Firebase App Distribution까지 자동화까지 하기로 한다.

fastlane과 Firebase App Distribution 연결

# firebase_app_distribution을 설치한다.
fastlane add_plugin firebase_app_distribution

firebase 공식 문서를 보면 action으로 firebase_app_distribution_login이 있다는 데 해당 action이 없는 것을 확인했다.

궁금해서 해당 fastlane github issue 올렸더니..

버전 0.5.0에서 부터는 firebase_app_distribution_login이 deprecated가 되고, firebase CLI를 설치 해야한다고 한다.

# Firebase CLI를 전역적으로 설치한다.
$ npm install -g firebase-tools

# 설치 후 Firebase 계정에 로그인한다.
$ firebase login

위 단계를 완료하면 Firebase CLI를 사용하여 Firbase App Distribution과 같은 다른 Firebase 서비스를 사용할 수 있다.

 

  • Fastfile
  ##########################################
  ########## Firebase Distribution #########
  ##########################################
   
  buildDir = "./build"
   
  lane :firebase do
    firebase_app_distribution(
        app: ENV["FIREBASE_APP_ID"],
        # 그룹명은 firebase에 있는 group alias를 사용해야한다.
        groups: "tester_groups",
        debug: true
    )
    slack(
      message: "App Distribution 업로드 성공",
      # 해당 slack 채널명 사용
      channel: "#r_frontend_deploy",
      # iOS에서도 사용했던 웹 훅을 연결해준다. android도 .env 파일을 만들어줄 것
      slack_url:  ENV["SLACK_WEBHOOK_URL"],
    )
  end

  error do |lane, exception, options|
    slack(
      message: "에러 발생 : #{exception}",
      success: false,
      slack_url: ENV["SLACK_WEBHOOK_URL"],
    )
  end

+) 추가적으로 App Store 배포까지 해보자

desc "Deploy a new version to the Google Play"
    lane :playStore do
      releaseFilePath = File.join(Dir.pwd, "my-release-key.keystore")
      gradle(task: "clean")
      gradle(
        task: 'bundle',
        build_type: 'Release',
        properties: {
          "android.injected.signing.store.file" => releaseFilePath,
          "android.injected.signing.store.password" => ENV["STORE_PASSWORD"],
          "android.injected.signing.key.alias" => ENV["KEY_ALIAS"],
          "android.injected.signing.key.password" => ENV["KEY_PASSWORD"]
        }
      )
      upload_to_play_store(
          track: 'production',
          release_status: 'draft',
      )
      slack(
      message: "Google Play Store 업로드 성공",
      channel: "#r_frontend_deploy",
      slack_url:  ENV["SLACK_WEBHOOK_URL"],
    )
    end

releaseFilePath도 ./env 파일 내에 관리해도 된다. 
현재는 /fastlane 파일 안에 my-release-key.keystore를 저장해줬는데 두 번 저장되는 꼴이 되기 때문에 경로를 잘 설정하면 될 것 같다!

 

후기

그동안 하던 스프린트가 마치고, 시간이 남아 자동화 배포까지 적용 해보았다. 
처음에는 낯설었던 fastlane이 에러 코드만 보고도 어떤 부분이 문제인지 알 수 있을 정도로 익숙해졌다.

fastlane 자동화 시스템을 갖추는 데까지는 1~2일 소요되지만, 정말 업무 생산성을 높일 수 있는 시스템인 것 같다.
action으로 버전과 빌드 번호를 증가시키고 슬랙에도 푸시할 수 있으니 얼마나 편리한가 …!

이런 자동화 시스템을 잘 활용하여 업무 생산성을 높이기 위해 노력해야겠다.

프론트 CI/CD를 구축한 나, 제법 멋지군

구글 애드센스 (google adsense)

  • 웹사이트를 위한 것

 

애드몹 (admob)

  • 모바일 앱을 위한 것

 

구글 애드센스 정책 하나하나 살펴보며 시간 버린 나같이,,
그런 사람이 없었으면 하는 바람에서 글을 쓰게 됐다.

 


 

질문: react-native 앱에서 웹뷰를 띄워 모바일 애드센스 광고를 추가할 수 있나?

답변: 간단하게 말하면 '없다'

 

하지만, 본인이 크로스 플랫폼이 아닌 네이티브 플랫폼 개발자라면 가능하다!

  • webview API를 사용하면 구글 애드센스 광고를 추가할 수 있다.
  • 단 android, iOS만 가능하다.

https://support.google.com/adsense/answer/11893859?hl=ko&ref_topic=28893&sjid=5870858233913781273-AP

 

 

 

정책 참조 사이트
https://support.google.com/adsense/thread/194709442/앱-서비스-제공자-애드센스-승인-관련-문의?hl=ko

 

앱 서비스 제공자 애드센스 승인 관련 문의 - Google AdSense 커뮤니티

 

support.google.com

 

?: 앱 크기가 최적화랑 상관이 있나요?
🧚‍♀️: Yessss!

 


앱 크기가 작아야 하는 이유

  • 용량 때문에 18.7% 사람들이 앱을 제거한다. 개발자는 항상 번들 크기와 앱 용량을 줄이는 데 주의를 기울여야 한다.

 

 

WHAT?

코드의 전체 크기에 영향을 미치는 가장 큰 요소 중 하나는 라이브러리이다.

여러 라이브러리를 비효율적으로 사용하고, 원하는 디자인을 위한 커스텀 폰트를 넣다보면 앱의 크기가 커져 앱 속도가 저하됨.

native 앱과 달리 react-native 에서는

1. 메모리에 로드해야 하는 javascript bundle이 포함되어 있음.

2. 로드 후 javascript vm에 의해 구문이 분석되고 실행됨.

번들에 사용되지 않는 코드를 제거해주는 tree shaking 조차 native와 달리 react-native에서는 지원하고 있지 않는다.

tree shaking을 지원하지 않는 건 총 시작 시간에 부정적인 영향을 미칠 수 있음!!

 

 

 

앱이 실행될 때까지 걸리는 시간(로딩시간)은 스마트폰의 속도 성능을 나타내는 주요 지표 중 하나이다.

 

이건 있을 수 없어!!!

 

 

HOW?

앱 번들 크기를 분석해보자.

react-native에서는 react-native-bundle-visualizer 라이브러리를 사용해서 번들에 추가된 라이브러리의 세부 정보들을 파악할 수 있다.

가장 많이 차지하는 것들을 비교해보자!

 

 

라이브러리 설치 & 적용

https://github.com/IjzerenHein/react-native-bundle-visualizer

 

 

1. 라이브러리 설치

yarn add --dev react-native-bundle-visualizer

 

2. 실행

yarn run react-native-bundle-visualizer

 

3. 실행 후 화면

빌드하면 이런 창이 뜨는 걸 확인 할 수 있다!

ex)


아래는 실제 적용 화면 🙌

가장 큰 용량을 차지하는 라이브러리를 확인할 수 있다.

date-time-format-timezone... 

너가 뭔데,, 

 

 

알고보니 이거 현재 제대로 쓰이지도 않는 라이브러리였음..!ㅋ

 


찾아보면서 알게된 사이트 공유 ㅎㅎ
https://bundlephobia.com/ 
들어가서 라이브러리와 버전을 쓰면은 bundle 크기를 알려준다..!!
번들포비아 ㅋㅋ 이름도 웃기다. 잘 지었어 🤔
 

Bundlephobia | Size of npm dependencies

Bundlephobia helps you find the performance impact of npm packages. Find the size of any javascript package and its effect on your frontend bundle.

bundlephobia.com

 

 

 

 

오늘도 이렇게 최적화를 위해 힘써보았다.
유저에게 만족스러운 앱이 되는 그날까지,,
to be continue..

 

요즘 rn 앱 내에서의 ui re-rendering을 막으면서 최적화를 위해 힘쓰고 있다.

여러 방법들을 공부하면서 기록하고자 한다. 📚

 

프로파일링 하며 시도했던 방법들은 추후에 더 기록할 예정이다 🔥

 

 

1. 과도한 커스텀 hook 사용 방지

첫 번째로 우리 앱에는 hook이 정말 많았다.

종속성이 변경되면서 필요하지 않는 화면까지 re-rendering 되는 상황이었다.

 

최적화를 해보자

 

- 과도한 hook 사용을 방지하고 필요한 화면. 즉, 해당 컴포넌트 안에서만 hook을 사용하는 방식이다.

예를 들어 하나의 화면 안에 2가지 컴포넌트로 분리되어 있다고 치자. 

 

 

기존에는 presentational and container 패턴을 사용하면서 1과 2 화면에서 필요한 정보들을 모두 1+2 <전체> 화면에서 갖고 있었다. 
그렇다보니 props drilling이 많이 일어나 사용자 경험이 안좋아졌다.

 

 

필요한 화면에서만 hook을 사용해야겠구나!

 

 

해당 화면에서만 hook을 사용하도록 리팩토링하기 시작했다.

 

 

문제점

1. 1+2 <전체> 화면에서 화면 1에서만 사용되는 hook이 필요하다.

2. 과연 해당 hook이 전체 화면에서도 필요한 걸까?

 

단, 해당 커스텀 hook이 전체 화면에서는 불필요한 정보들이 많을 때를 기준으로 작성되었다.

 

 

 

해결법

필요한 정보들만 빼오자!

각각 화면에서 hook을 사용하되 컴포넌트를 그릴 때 필요한 정보만 전역으로 관리하자! 

전역으로 저장된 정보를 가지고 1+2 <전체> 화면에서 사용하면 되는 것이다 !!!

 

+ Recent posts