Post

React Native Android namespace 자동화하기

React Native Android namespace 자동화하기

문제 상황

최근 React Native 프로젝트를 업그레이드하는 과정에서 골치 아픈 문제가 발생했다. 안드로이드 빌드 시 각 라이브러리마다 namespace를 일일이 추가해줘야 하는 상황이었다. 특히 node_modules를 삭제하고 재설치할 때마다 이 작업을 반복해야 했는데, 이는 매우 비효율적이고 시간 낭비였다.

처음에는 Android Studio에서 각 라이브러리의 build.gradle 파일을 열어 수동으로 namespace를 추가했다. 하지만 20개가 넘는 라이브러리에 대해 이 작업을 반복하는 것은 너무 고통스러웠다. 특히 새로운 팀원이 프로젝트를 셋업할 때마다 이런 불편함을 겪어야 한다는 점이 마음에 걸렸다.

해결 과정

이 문제를 자동화하기 위해 세 가지 주요 작업을 진행했다.

1. package.json에 postinstall 스크립트 추가

먼저 npm install 실행 시 자동으로 namespace를 추가하도록 package.json의 scripts에 postinstall을 추가했다.

1
2
3
"scripts": {
  "postinstall": "node scripts/add-namespaces.ts && patch-package"
}

2. gradle.properties에 namespace 정보 추가

안드로이드 빌드 시스템이 참조할 수 있도록 android/gradle.properties 파일에 각 라이브러리의 namespace를 정의했다.

1
2
3
4
android.enableNamespaceCheck=true
react-native-gesture-handler.namespace=com.swmansion.gesturehandler
react-native-webview.namespace=com.reactnativecommunity.webview
# ... 기타 라이브러리들의 namespace

이렇게 하면 프로젝트에서 사용하는 모든 라이브러리의 namespace를 한 곳에서 관리할 수 있다.

3. namespace 자동 추가 스크립트 작성

가장 핵심적인 부분은 scripts/add-namespaces.ts 파일이다. 이 스크립트는 node_modules 내의 각 React Native 라이브러리의 build.gradle 파일을 찾아서 namespace를 자동으로 추가해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
const fs = require('fs');
const path = require('path');

const nodeModulesPath = path.join(__dirname, '..', 'node_modules');

const namespaceMap = {
  'react-native-gesture-handler': 'com.swmansion.gesturehandler',
  'react-native-firebase-messaging': 'io.invertase.firebase.messaging',
  'react-native-kakao-share-link': 'com.reactnativekakaosharelink',
  'react-native-get-random-values': 'org.linusu',
  'react-native-webview': 'com.reactnativecommunity.webview',
  '@react-native-firebase/app': 'io.invertase.firebase',
  '@react-native-firebase/dynamic-links': 'io.invertase.firebase.dynamiclinks',
  'react-native-inappbrowser-reborn': 'com.proyecto26.inappbrowser',
  'react-native-safe-area-context': 'com.th3rdwave.safeareacontext',
  'react-native-channel-plugin': 'com.zoyi.channel.rn',
  'react-native-screens': 'com.swmansion.rnscreens',
  '@react-native-async-storage/async-storage': 'com.reactnativecommunity.asyncstorage',
  '@react-native-community/masked-view': 'org.reactnative.maskedview',
  '@react-native-seoul/kakao-login': 'com.dooboolab.kakaologins',
  '@invertase/react-native-apple-authentication': 'com.RNAppleAuthentication',
  'react-native-reanimated': 'com.swmansion.reanimated',
  'react-native-svg': 'com.horcrux.svg',
  'react-native-device-info': 'com.learnium.RNDeviceInfo',
  'react-native-push-notification': 'com.dieam.reactnativepushnotification',
  'react-native-permissions': 'com.zoontek.rnpermissions',
  'react-native-splash-screen': 'org.devio.rn.splashscreen',
  '@react-native-cookies/cookies': 'com.reactnativecommunity.cookies',
};

function addNamespaceToGradleFile(gradleFilePath, packageName) {
  try {
    let content = fs.readFileSync(gradleFilePath, 'utf8');

    // 이미 namespace가 있는지 확인
    if (!content.includes('namespace')) {
      // android { 블록 찾기
      const androidBlockRegex = /android\s*{/;
      if (androidBlockRegex.test(content)) {
        // namespace 추가
        content = content.replace(
          androidBlockRegex,
          `android {\n    namespace "${packageName}"`,
        );

        fs.writeFileSync(gradleFilePath, content, 'utf8');
        console.log(`✅ Added namespace to ${gradleFilePath}`);
      }
    } else {
      console.log(`ℹ️ Namespace already exists in ${gradleFilePath}`);
    }
  } catch (error) {
    console.error(`❌ Error processing ${gradleFilePath}:`, error);
  }
}

function processNodeModules() {
  console.log('🔍 Starting to process React Native libraries...');

  // namespaceMap의 각 항목에 대해 처리
  Object.entries(namespaceMap).forEach(([lib, namespace]) => {
    let androidBuildGradle;

    if (lib.startsWith('@')) {
      const [org, name] = lib.slice(1).split('/');
      androidBuildGradle = path.join(
        nodeModulesPath,
        '@' + org,
        name,
        'android',
        'build.gradle',
      );
    } else {
      androidBuildGradle = path.join(
        nodeModulesPath,
        lib,
        'android',
        'build.gradle',
      );
    }

    if (fs.existsSync(androidBuildGradle)) {
      addNamespaceToGradleFile(androidBuildGradle, namespace);
    } else {
      console.log(`⚠️ Could not find build.gradle for ${lib}`);
    }
  });

  console.log('✨ Finished processing libraries');
}

// 스크립트 실행
processNodeModules();

스크립트의 주요 기능은 다음과 같다:

  • 라이브러리별 namespace 매핑 정보 관리
  • @org/package 형태의 패키지도 처리 가능
  • 이미 namespace가 있는 경우 건너뛰기
  • 작업 진행 상황을 콘솔에 표시

결과

이제 npm install을 실행하면 자동으로 다음과 같은 작업이 진행된다:

  1. 모든 패키지가 설치됨
  2. postinstall 스크립트가 실행되어 필요한 라이브러리에 namespace가 추가됨
  3. patch-package가 실행되어 수정된 내용이 패치로 저장됨

npm install > 출력 결과

출력 결과를 보면 어떤 라이브러리에 namespace가 추가되었고, 어떤 것은 이미 namespace가 있어서 건너뛰었는지 확인할 수 있다.

마무리

이 자동화 작업으로 개발 환경 설정이 훨씬 수월해졌다. 새로운 팀원이 프로젝트를 셋업할 때도 별도의 수동 작업 없이 npm install 한 번으로 모든 설정이 완료된다.

This post is licensed under CC BY 4.0 by the author.