개발 노트/React Native

[React Native] 통합 앱에서 selectedProfiles로 빌드 대상을 관리하는 방법

pposooj 2026. 5. 19. 15:45

React Native로 여러 앱을 하나의 코드베이스에서 몰아서 관리하다 보면 빌드 시간이 말도 못하게 길어집니다. 한 놈만 수정했는데 엉뚱한 앱까지 같이 빌드되느라 피드백이 늦어지기도 하죠.

저의 경우 이번 작업에 필요한 앱만 선택해서 빌드하려고 selectedProfiles라는 빌드 옵션을 주고 관리하고 있습니다. 처음에는 단순한 빌드 스위치 정도로 생각했는데, 운영하다 보니 화면 접근 권한이나 라우팅 문제와 얽혀서 헷갈리기 쉽더군요. 자꾸 까먹어서 잊어버리지 않게 실무 기준으로 정리해 둡니다.

내가 겪은 문제와 원인

하나의 프로젝트에서 여러 앱을 빌드할 때 기준 없이 관리하면 아래와 같은 상황이 터집니다.

  • release 빌드에서는 딱 하나의 앱만 필요한데, debug에서는 여러 앱을 동시에 확인해야 함.
  • 특정 앱을 빌드에서 제외했는데, 공통 화면 코드에는 메뉴나 링크가 여전히 남아 있어서 누르면 빈 화면이 뜨거나 크래시가 남.
  • 가장 위험한 착각: selectedProfiles로 빌드 대상을 제한한 것을 '보안 조치'로 오해해서, 실제 API 권한 검증이나 관리자 기능 보호를 놓침.

selectedProfiles는 빌드 시점에 어떤 리소스와 플래버(Flavor)를 포함할지 좁혀주는 빌드 스코프 도구일 뿐입니다. 앱 안에서 사용자가 화면을 보고 API를 호출하는 런타임 권한은 별도의 라우트 가드(Route Guard)와 서버 검증으로 처리해야 합니다.

기본 구조: Catalog와 selectedProfiles 분리하기

가장 먼저 할 일은 전체 프로파일 기준표(Catalog)와 이번 빌드 대상 목록(selectedProfiles)을 확실히 찢어두는 것입니다. 안 그러면 나중에 이게 원래 삭제된 앱인지, 잠시 빌드에서 제외한 건지 구분이 안 됩니다.

구분 역할 예시
Profile Catalog 전체 앱 후보 및 메타데이터 관리 (기준표) profileId, applicationId, group, engine
selectedProfiles 이번 빌드에 포함할 프로파일 선택 (필터) sampleTourBusan, sampleFileData
Route Guard 선택된 프로파일에서 접근 가능한 화면 제한 map, search, settings 노출 조건
Feature Flag 기능 사용 여부 정의 지도 활성화, 외부 링크 여부 등

해결 코드: selectedProfiles 파싱하기 (Node.js 24 기준)

쉼표(,)로 전달된 SELECTED_PROFILES 환경 변수를 파싱해서 전체 catalog에서 안전하게 빌드 대상을 고르는 스크립트입니다. 프로젝트의 민감 정보를 제거하고 단순화했습니다.

JavaScript
 
// build-scope.mjs
const profiles = [
  {
    profileId: 'sampleTourBusan',
    applicationId: 'com.example.tour.busan',
    group: 'tour-regional',
    engine: 'tour',
    enabledForIntegration: true,
  },
  {
    profileId: 'sampleFileData',
    applicationId: 'com.example.filedata',
    group: 'file-data',
    engine: 'filedata',
    enabledForIntegration: true,
  },
  {
    profileId: 'legacyDisabledApp',
    applicationId: 'com.example.disabled',
    group: 'legacy',
    engine: 'hybrid',
    enabledForIntegration: false,
  },
];

function parseSelectedProfiles(value) {
  return new Set(
    String(value || '')
      .split(',')
      .map((item) => item.trim())
      .filter(Boolean),
  );
}

function resolveBuildProfiles(catalog, selected) {
  // 1. 통합 빌드가 활성화된 프로파일만 필터링
  const enabledProfiles = catalog.filter((profile) => profile.enabledForIntegration);

  // 지정된 게 없으면 기본값으로 첫 번째 프로파일 선택
  if (selected.size === 0) {
    return enabledProfiles.slice(0, 1);
  }

  // 2. 선택한 프로파일만 추출
  const resolved = enabledProfiles.filter((profile) => selected.has(profile.profileId));
  const resolvedIds = new Set(resolved.map((profile) => profile.profileId));
  
  // 3. 잘못된 ID나 비활성화된 ID를 넣었다면 조용히 넘기지 않고 에러 throw
  const unknownIds = [...selected].filter((profileId) => !resolvedIds.has(profileId));

  if (unknownIds.length > 0) {
    throw new Error(`[빌드 에러] 존재하지 않거나 비활성된 프로파일입니다: ${unknownIds.join(', ')}`);
  }

  return resolved;
}

const selected = parseSelectedProfiles(process.env.SELECTED_PROFILES);
const targets = resolveBuildProfiles(profiles, selected);

// 4. 최종 빌드 대상 로그 출력 (실수 방지용)
console.table(
  targets.map(({profileId, applicationId, group, engine}) => ({
    profileId,
    applicationId,
    group,
    engine,
  })),
);

터미널 실행 예시

Bash
 
SELECTED_PROFILES=sampleTourBusan node build-scope.mjs

빌드 로그에서 내가 타겟팅한 앱이 정확히 잡혔는지 console.table로 눈에 띄게 보여주는 게 좋습니다. 프로파일이 늘어날수록 이 확인 로그가 실수를 엄청나게 줄여줍니다.

Android Gradle 연동하기

안드로이드 빌드 시에는 ./gradlew :app:assembleDebug -PselectedProfiles=sampleTourBusan 형태로 값을 넘깁니다. build.gradle.kts 설정에서 이 프로퍼티를 받아 product flavor 생성에 연결해 줍니다.

Kotlin
 
// build.gradle.kts 예시
val selectedProfilesProp = providers.gradleProperty("selectedProfiles").orNull

val selectedProfiles = selectedProfilesProp
    ?.split(',')
    ?.map(String::trim)
    ?.filter(String::isNotBlank)
    ?.toSet()
    ?: setOf(defaultProfileId) // 디버그 편의를 위한 기본값

// 카탈로그에서 빌드 대상 필터링
val buildTargets = profiles.filter { profile ->
    profile.enabledForIntegration && selectedProfiles.contains(profile.flavorName)
}

if (buildTargets.isEmpty()) {
    throw GradleException("빌드 대상 프로파일이 선택되지 않았습니다.")
}

[운영 팁] 디버그 빌드에서는 편의상 프로파일 미지정 시 기본값을 주더라도, 릴리즈(운영) 빌드 배포 자동화 단계에서는 스크립트에서 값을 명시하지 않으면 빌드가 터지도록 실패 정책을 가져가는 것이 안전합니다. 실수로 엉뚱한 앱이 배포되는 대형 사고를 막을 수 있습니다.

환경별 빌드 대상 설정 기준

운영 환경에 따라 범위를 다르게 가져가면 빌드 시간도 아끼고 검증도 확실해집니다. 저의 경우 아래 기준으로 쪼개서 적용하고 있습니다.

  • 로컬 개발: 수정 중인 단일 프로파일만 빌드 (무조건 피드백 속도 최우선)
  • PR 검증 (CI): 수정 프로파일 + 대표 프로파일 (공통 컴포넌트 건드려서 다른 앱 깨지는지 확인)
  • Nightly 빌드: 그룹별 대표 프로파일들만 묶어서 검증
  • 릴리즈 빌드: 배포 대상 단일 프로파일 딱 하나만 명시

주의할 점 및 자주 하는 실수

  1. 빌드 대상 제한을 보안으로 착각하는 문제
  2. selectedProfiles로 빌드에서 뺐다고 해서 서버 데이터 접근이나 관리자 URL 검증까지 자동으로 처리되는 게 아닙니다. API 접근 권한은 철저하게 서버에서 토큰과 사용자 권한으로 막아야 합니다.
  3. 민감 설정을 Profile Catalog에 섞어두는 실수
  4. profileId나 applicationId 같은 메타데이터 관리용 카탈로그 파일에 서버 세크리트 키, Firebase 개인 키, keystore 비밀번호 같은 걸 같이 적어두면 안 됩니다. 이 카탈로그는 여러 빌드 스크립트나 문서에서 참조하기 때문에, 나중에 블로그 예제 코드로 쓰거나 문서화할 때 유출될 위험이 큽니다. 비밀값은 무조건 .env나 환경 변수로 분리하세요.
  5. 오타 검증 누락
  6. 단순 문자열 파싱만 하고 검증을 안 하면 -PselectedProfiles=sampleTourBusann 처럼 오타가 났을 때 빈 빌드가 되거나 뚱딴지같은 기본 프로파일로 빌드되어 버립니다. 카탈로그에 없는 값이 들어오면 무조건 빌드를 멈추고 에러를 뱉게 만들어야 합니다.

요약 및 결론

처음부터 너무 복잡하게 다 바꾸려고 하면 머리 아픕니다. 우선 아래 딱 3가지만 먼저 적용해 보세요.

  1. 최종 선택된 profileId 빌드 로그에 보기 좋게 출력하기
  2. 카탈로그에 없는 알 수 없는 profileId가 들어오면 빌드 에러 처리하기
  3. 릴리즈 빌드 시 배포 대상을 환경 변수로 명시하도록 강제하기

통합 앱 구조를 운영할 때 이 세 가지만 뼈대로 잡아놔도 손가락 오타나 배포 설정 실수로 생기는 삽질은 상당 부분 방어할 수 있습니다. 결국 이번 빌드에 무엇이 포함되는지 눈으로 명확히 확인하고 검증하는 게 핵심입니다.