개발 노트/React Native

[React Native] 통합 앱에서 프로파일별 feature flag를 설계하는 방법

pposooj 2026. 5. 20. 14:14

여러 프로파일을 통합앱에서 관리하다 보면 앱마다 켜야 하는 기능과 숨겨야 하는 기능이 달라집니다. 어떤 앱은 지도 기능이 필요하고, 어떤 앱은 알림만 필요할 수 있습니다. 또 어떤 앱은 설정 화면은 있지만 외부 링크 이동은 막아야 할 수도 있습니다.

이때 feature flag를 사용하면 프로파일별 기능 노출 기준을 한 곳에서 관리할 수 있습니다. 처음에는 단순히 버튼을 보이게 하거나 숨기는 값 정도로 생각하기 쉽지만 실제로 정리해보면 기능 하나를 켜고 끄는 일은 UI, route guard, 권한 요청, 딥링크, 푸시 알림, 테스트 범위까지 함께 연결되는 문제였습니다.

이번 글에서는 React Native 통합 앱에서 프로파일별 feature flag를 설계할 때 확인하면 좋은 기준을 정리해보겠습니다. 통합 앱 설정을 다룰 때 놓치기 쉬운 부분을 중심으로 살펴보겠습니다.

이 글에서 다룰 문제

feature flag 기준이 흐트러지면 다음 문제가 생길 수 있습니다.

  • 메뉴에서는 숨겼지만 딥링크로 기능 화면이 열린다.
  • 지도 기능이 꺼져 있는데 위치 권한 요청이 뜬다.
  • 알림 기능이 없는 앱에서 푸시 토큰 등록 코드가 실행된다.
  • 설정 화면은 보이지만 개인정보 처리방침 링크가 비어 있다.
  • feature flag 이름이 화면마다 달라서 추적이 어렵다.
  • feature flag를 서버 권한 검증처럼 오해해 API 보안 처리를 놓친다.

처음에는 showMap, useMap, enableMap처럼 비슷한 이름을 화면마다 만들 수 있습니다. 저도 이런 구조를 보면 처음에는 빨리 처리하기 좋아 보였습니다. 하지만 프로파일이 늘어나면 같은 의미의 flag가 여러 이름으로 흩어져 유지보수가 어려워집니다. 그래서 feature key와 사용 기준을 먼저 정하는 편이 좋습니다.

feature flag의 역할

feature flag는 특정 기능을 사용할 수 있는지 알려주는 설정입니다. 통합 앱에서는 보통 profile registry 안에 넣어 프로파일별 기능 차이를 관리합니다.

기능 flag 예시 함께 확인할 항목
지도 map 지도 탭, 위치 권한, 지도 상세 route
검색 search 검색창, 필터, 빈 결과 화면
알림 push 알림 권한, 토큰 등록, 알림 클릭 route
즐겨찾기 favorite 저장소, 로그인 필요 여부, 리스트 표시
외부 링크 externalLinks 브라우저 이동, 허용 도메인, WebView 정책
설정 settings 개인정보 처리방침, 고객지원 링크

중요한 점은 flag가 화면 노출만 의미하지 않는다는 것입니다. 예를 들어 map: false라면 지도 탭뿐 아니라 위치 권한 요청, 지도 상세 route, 지도 알림 payload까지 함께 막아야 합니다. 이 기준을 놓치면 화면에서는 숨긴 기능이 다른 경로로 다시 열릴 수 있습니다.

기본 구조 예시

아래 코드는 모든 feature key에 기본값을 두고, 프로파일별로 필요한 기능만 덮어쓰는 방식입니다.

type FeatureKey =
  | 'map'
  | 'search'
  | 'push'
  | 'favorite'
  | 'externalLinks'
  | 'settings';

type FeatureFlags = Record<FeatureKey, boolean>;

type AppProfile = {
  profileId: string;
  displayName: string;
  features: FeatureFlags;
  enabledRoutes: string[];
};

const defaultFeatures: FeatureFlags = {
  map: false,
  search: false,
  push: false,
  favorite: false,
  externalLinks: false,
  settings: true,
};

const profileRegistry: Record<string, AppProfile> = {
  sampleTour: {
    profileId: 'sampleTour',
    displayName: '샘플 관광 앱',
    features: {
      ...defaultFeatures,
      map: true,
      search: true,
      favorite: true,
      externalLinks: true,
    },
    enabledRoutes: ['Home', 'Map', 'Detail', 'Settings'],
  },
  sampleNotice: {
    profileId: 'sampleNotice',
    displayName: '샘플 공지 앱',
    features: {
      ...defaultFeatures,
      push: true,
    },
    enabledRoutes: ['Home', 'Notice', 'Settings'],
  },
};

function hasFeature(profile: AppProfile, feature: FeatureKey): boolean {
  return profile.features[feature] === true;
}

이 구조에서는 모든 feature key가 기본값을 갖습니다. 새 프로파일을 추가할 때 특정 flag를 빠뜨려도 undefined가 아니라 false로 처리할 수 있습니다.

처음에는 필요한 flag만 추가하는 방식이 더 편해 보이지만 프로파일이 많아지면 빠진 값과 의도적으로 끈 값을 구분하기 어려워집니다. 기본값을 명확히 두면 이런 혼란을 줄일 수 있을 것 같습니다.

feature key 이름을 먼저 고정하기

feature flag에서 생각보다 중요한 것은 이름입니다. 화면마다 useMap, showMap, mapEnabled처럼 다른 이름을 쓰면 검색하기 어렵고 기준이 흐려집니다.

나쁜 패턴 개선 방향
화면마다 다른 flag 이름 사용 FeatureKey union 타입으로 고정
부정형 이름 사용 disableMap보다 map: true/false 사용
의미가 큰 flag 하나로 묶기 tourMode보다 map, search, favorite 분리
flag 설명 없이 추가 주석 또는 문서에 사용 범위 기록
오래된 flag 방치 deprecated 목록과 제거 계획 관리

기능이 급할 때는 새 flag를 바로 추가하기 쉽습니다. 하지만 나중에 찾기 어려운 이름으로 남으면 회귀 테스트와 리팩터링이 훨씬 힘들어집니다. flag 이름은 짧고, 기능 단위로, 긍정형으로 정하는 것이 좋은 것 같습니다.

UI와 route guard 연결하기

feature flag는 UI 렌더링과 route guard에 함께 연결되어야 합니다. 먼저 메뉴 항목에 필요한 기능을 지정해둘 수 있습니다.

type MenuItem = {
  label: string;
  routeName: string;
  requiredFeature?: FeatureKey;
};

const menuItems: MenuItem[] = [
  {label: '홈', routeName: 'Home'},
  {label: '지도', routeName: 'Map', requiredFeature: 'map'},
  {label: '검색', routeName: 'Search', requiredFeature: 'search'},
  {label: '설정', routeName: 'Settings', requiredFeature: 'settings'},
];

function getVisibleMenuItems(profile: AppProfile): MenuItem[] {
  return menuItems.filter((item) => {
    if (!item.requiredFeature) return true;
    return hasFeature(profile, item.requiredFeature);
  });
}

메뉴에서 숨겼다고 끝내지 않고, route guard도 같은 feature 기준을 확인해야 합니다.

const routeFeatureMap: Record<string, FeatureKey | undefined> = {
  Home: undefined,
  Map: 'map',
  Search: 'search',
  Settings: 'settings',
};

function canAccessRoute(profile: AppProfile, routeName: string): boolean {
  if (!profile.enabledRoutes.includes(routeName)) {
    return false;
  }

  const requiredFeature = routeFeatureMap[routeName];
  if (!requiredFeature) {
    return true;
  }

  return hasFeature(profile, requiredFeature);
}

이렇게 하면 메뉴와 실제 화면 접근 기준이 같은 feature flag를 바라보게 됩니다. 딥링크나 푸시 알림 클릭에서도 canAccessRoute()를 재사용할 수 있습니다.

처음에는 메뉴만 숨기면 충분하다고 생각하기 쉽습니다. 하지만 통합 앱에서는 딥링크나 알림 클릭처럼 다른 진입 경로가 있기 때문에, 실제 route 접근 기준까지 함께 맞춰야 합니다.

권한 요청과 feature flag 연결하기

권한 요청은 feature flag와 꼭 함께 봐야 합니다. 예를 들어 지도 기능이 꺼진 프로파일에서 위치 권한을 요청하면 사용자는 이유를 이해하기 어렵습니다.

type PermissionName = 'location' | 'notification';

function getRequiredPermissions(profile: AppProfile): PermissionName[] {
  const permissions: PermissionName[] = [];

  if (hasFeature(profile, 'map')) {
    permissions.push('location');
  }

  if (hasFeature(profile, 'push')) {
    permissions.push('notification');
  }

  return permissions;
}

다만 권한 요청은 기능 flag가 true라고 해서 앱 시작 즉시 요청하는 방식이 항상 좋은 것은 아닙니다. 위치 권한은 지도 화면에 들어갈 때 요청하는 식으로 사용 맥락에 맞춰 설계하는 것이 좋습니다.

이 부분은 처음 구현할 때 한 번 막히기 쉽습니다. 기능이 켜져 있으니 권한도 바로 요청하면 된다고 생각할 수 있지만, 사용자가 왜 권한을 요청받는지 이해할 수 있는 시점에 요청하는 편이 더 자연스럽습니다.

feature flag와 서버 권한은 다르다

feature flag는 앱 내부 기능 노출 기준입니다. 서버 권한 검증을 대체하지 못합니다. 이 차이를 분리해서 보는 것이 중요합니다.

구분 역할 예시
feature flag 앱에서 기능을 보이게 할지 결정 지도 메뉴 표시 여부
route guard 화면 접근 가능 여부 확인 Map route 차단
permission flow 단말 권한 요청 시점 결정 위치 권한 요청
server authorization 실제 데이터 접근 권한 확인 API 토큰, 사용자 권한 검증

앱에서 map: false로 숨겼다고 해서 지도 API가 안전해지는 것은 아닙니다. 중요한 데이터 접근은 서버에서 반드시 다시 검증해야 합니다. 이 부분을 헷갈리면 클라이언트 설정만 믿고 서버 보안을 놓칠 수 있습니다.

테스트 기준

feature flag를 설계했다면 아래 테스트를 준비하는 것이 좋습니다. 처음부터 모든 UI를 자동화하기 어렵다면 hasFeature(), canAccessRoute(), getRequiredPermissions() 같은 helper 함수부터 단위 테스트로 검증해볼 수 있습니다.

  • 모든 프로파일이 모든 FeatureKey의 기본값을 갖는지 확인한다.
  • flag가 false인 기능의 메뉴가 보이지 않는지 확인한다.
  • flag가 false인 route로 딥링크 접근 시 fallback 되는지 확인한다.
  • flag가 false인 기능의 권한 요청이 발생하지 않는지 확인한다.
  • push flag가 false일 때 알림 클릭 route가 차단되는지 확인한다.
  • flag 이름이 중복되거나 의미가 겹치지 않는지 확인한다.
  • feature flag를 서버 권한 검증처럼 사용하지 않았는지 확인한다.

이 helper 함수들은 작지만 전체 앱 정책에 영향을 줍니다. 작은 함수부터 테스트해두면 나중에 프로파일이 늘어났을 때 회귀를 줄이는 데 도움이 될 것 같습니다.

자주 하는 실수

1. feature flag를 UI 숨김에만 사용하는 경우가 있습니다. 실제로는 route guard, 딥링크, 푸시 payload, 권한 요청까지 같이 연결해야 합니다.

2. flag 기본값을 정하지 않는 경우도 있습니다. 새 프로파일에서 flag가 빠졌을 때 undefined가 섞이면 화면마다 처리 방식이 달라질 수 있습니다. 기본값을 먼저 정해두는 편이 안전합니다.

3. 의미가 큰 flag 하나로 여러 기능을 묶는 경우가 있습니다. tourMode 하나로 지도, 검색, 즐겨찾기를 모두 켜면 나중에 특정 기능만 끄기 어렵습니다. 기능 단위로 나눠두는 편이 관리하기 좋습니다.

4. feature flag를 보안 기능으로 착각하는 경우가 있습니다. 클라이언트 flag는 앱 동작 제어용이며, 서버 권한 검증은 별도로 필요합니다.

결론

React Native 통합 앱에서 feature flag는 프로파일별 기능 차이를 안전하게 관리하기 위한 기준입니다. 단순히 버튼을 숨기는 값이 아니라 UI, route guard, 권한 요청, 딥링크, 푸시 알림, 테스트 범위와 연결되는 설정으로 봐야 합니다.

좋은 구조는 feature key를 먼저 고정하고, profile registry에서 기본값을 채우며, hasFeature(), canAccessRoute(), getRequiredPermissions() 같은 helper로 정책을 재사용하는 것입니다. 이렇게 하면 화면마다 조건문을 반복하지 않고도 프로파일별 기능 차이를 관리할 수 있습니다.

다만 feature flag는 서버 보안을 대신하지 않습니다. 앱에서 기능을 숨기더라도 실제 데이터 접근은 서버 인증과 권한 검증으로 보호해야 합니다. 이 차이를 명확히 구분하는 것이 통합 앱 운영에서 중요한 기준 중 하나인 것 같습니다.

참고 자료

  • React Native navigation과 deep link 관련 공식 문서
  • Android 런타임 권한 요청 관련 공식 문서
  • TypeScript union type과 Record 타입 관련 문서
  • 프로젝트 내부 profile registry, selectedProfiles, route guard 문서