이래저래 React Native로 통합 앱을 운영하다 보면, 하나의 코드베이스에서 여러 프로파일(브랜드별 또는 업체별 앱)을 관리해야 할 때가 있습니다. 프로파일별로 파일은 잘 나눠놨는데, 막상 앱이 시작되는 진입점(App.tsx 등)에서 코드가 다시 복잡해지더군요.
저의 경우도 처음에는 파일만 분리해두면 구조가 다 정리된 줄 알았습니다. 그런데 막상 앱 시작 흐름을 짜다 보니 현재 프로파일 조회하고, 컨트롤러 고르고, 라우트 접근 기준 체크하고, 예외 처리(Fallback)까지 연결하느라 진입점 코드가 금방 지저분해졌습니다.
이 부분을 한 곳에서 정리하지 않으면 화면이나 네비게이션 코드가 다시 복잡해집니다. 자꾸 까먹어서 나중에도 바로 참고하려고 정리해 둡니다.
내가 겪은 문제 (조립 진입점이 없을 때)
시작 지점에서 중심을 잡아주지 않으면 운영하면서 이런 문제들이 꼭 터집니다.
- 앱 시작 파일에서 profileId 조건문(if-else)이 계속 늘어난다.
- 화면마다 컨트롤러를 직접 선택해서 호출하느라 중복 코드가 생긴다.
- 존재하지 않는 profileId가 들어왔을 때 fallback 기준이 제각각이라 앱이 뻗는다.
- 라우트 가드(Route Guard)나 피처 플래그 기준이 화면마다 다르게 적용된다.
- 모델 빌더와 컨트롤러 연결이 사방에 흩어져서 유닛 테스트하기가 힘들다.
처음에는 프로파일이 두세 개라 문제가 없어 보이지만, 개수가 늘어나면 앱 시작 시 무엇을 먼저 결정해야 하는지가 정말 중요해집니다. 이 순서가 흔트러지면 새 프로파일 추가할 때마다 사이드 이펙트 감당이 안 됩니다.
해결 방법: Composition Layer 도입하기
쉽게 말해서 현재 선택된 프로파일을 기준으로 앱에 필요한 설정과 동작을 한곳에서 조립하는 진입점을 만드는 것입니다. 화면 컴포넌트가 직접 레지스트리를 뒤지거나 컨트롤러를 고르게 두지 않고, 시작 지점에서 필요한 구성 요소를 싹 묶어서 하위로 넘겨주는 방식입니다.
1. 기본 조립 순서 잡기
앱이 켜질 때 아래 순서대로 딱 기준을 잡아 실행해야 흐름이 안 꼬입니다.
- 실행 환경에서 profileId를 읽어온다.
- 프로파일 레지스트리에서 해당 프로파일을 찾는다.
- 만약 프로파일이 없거나 비활성 상태라면 안전한 fallback 프로파일을 선택한다.
- 프로파일에 맞는 컨트롤러를 매핑한다.
- 라우트 가드와 피처 플래그 헬퍼를 생성한다.
- AppProvider나 RootNavigator에 조립된 context를 통째로 전달한다.
실제 적용한 조립 코드 예제
저의 경우 아래처럼 타입을 정의하고 createAppContext라는 함수 하나로 조립을 처리했습니다. 이 함수가 바로 Composition Layer 역할을 합니다.
type Engine = 'tour' | 'filedata' | 'utility';
type RouteName = 'Home' | 'List' | 'Map' | 'Settings';
type AppProfile = {
profileId: string;
displayName: string;
engine: Engine;
initialRoute: RouteName;
enabledRoutes: RouteName[];
fallbackRoute: RouteName;
enabled: boolean;
};
type AppController = {
buildInitialModels: () => Promise<unknown[]>;
};
type AppContextValue = {
profile: AppProfile;
controller: AppController;
canAccessRoute: (routeName: RouteName) => boolean;
fallbackRoute: RouteName;
};
// 1. 프로파일 관리 레지스트리
const profileRegistry: Record<string, AppProfile> = {
sampleTour: {
profileId: 'sampleTour',
displayName: '샘플 관광 앱',
engine: 'tour',
initialRoute: 'Home',
enabledRoutes: ['Home', 'List', 'Map', 'Settings'],
fallbackRoute: 'Home',
enabled: true,
},
sampleFileData: {
profileId: 'sampleFileData',
displayName: '샘플 파일 데이터 앱',
engine: 'filedata',
initialRoute: 'List',
enabledRoutes: ['Home', 'List', 'Settings'],
fallbackRoute: 'List',
enabled: true,
},
};
// 2. 엔진별 컨트롤러 팩토리
const controllers: Record<Engine, AppController> = {
tour: { buildInitialModels: async () => [] },
filedata: { buildInitialModels: async () => [] },
utility: { buildInitialModels: async () => [] },
};
// 3. 안전한 프로파일 조회 로직
function resolveProfile(profileId: string): AppProfile | null {
const profile = profileRegistry[profileId];
if (!profile || !profile.enabled) {
return null;
}
return profile;
}
// 4. 핵심 조립 함수 (Composition Layer)
export function createAppContext(profileId: string): AppContextValue {
// 프로파일이 없으면 기본 fallback(여기선 sampleTour) 적용
const profile = resolveProfile(profileId) || profileRegistry.sampleTour;
const controller = controllers[profile.engine];
return {
profile,
controller,
fallbackRoute: profile.fallbackRoute,
canAccessRoute: (routeName) => profile.enabledRoutes.includes(routeName),
};
}
이렇게 짜두면 화면 컴포넌트나 네비게이션 레이어는 레지스트리를 직접 뒤질 필요가 없습니다. 이미 시작 지점에서 조립이 끝난 context만 바라보면 되니까요.
실무 운영 시 주의할 점
1. Fallback 프로파일 처리 (보안 주의)
잘못된 profileId가 들어오거나 프로파일을 불러오지 못할 때 앱이 크래시 나면 안 됩니다. 그렇다고 아무 프로파일이나 보여주거나 관리자용 설정이 노출되면 큰일 납니다.
| 상황 | 권장 처리 방식 |
| profileId가 아예 없음 | 안전한 기본 프로파일 적용 또는 서비스 안내 화면 노출 |
| profileId가 잘못됨 | 무조건 지정된 fallback profile로 이동하고 내부 로그 기록 |
| 비활성화된 프로파일 | 사용자에게는 일반 오류 화면 표시 |
| 컨트롤러 매핑 실패 | 앱 시작을 안전하게 중단하고 점검 안내 화면 노출 |
중요: 사용자 화면에는 "앱 설정을 불러오지 못했습니다" 정도로 일반적인 문구만 보여주셔야 합니다. 내부 profileId, 서버 API 주소, 디버그 스택 같은 정보가 노출되지 않도록 처리하는 것이 안전합니다.
2. RootNavigator에 Context 전달하고 사용하기
조립된 데이터는 React Context를 사용해 하위로 뿌려줍니다.
import React, { createContext, useContext } from 'react';
const AppRuntimeContext = createContext<AppContextValue | null>(null);
export function AppRuntimeProvider({ value, children }: { value: AppContextValue; children: React.ReactNode }) {
return (
<AppRuntimeContext.Provider value={value}>
{children}
</AppRuntimeContext.Provider>
);
}
export function useAppRuntime() {
const context = useContext(AppRuntimeContext);
if (!context) {
throw new Error('AppRuntimeProvider가 누락되었습니다.');
}
return context;
}
딥링크나 푸시 알림으로 외부에서 특정 화면으로 바로 치고 들어올 때도, 이 context에서 제공하는 canAccessRoute 헬퍼를 태워서 라우트 가드를 적용하면 안전합니다.
type NavigationLike = {
navigate: (routeName: string, params?: Record<string, unknown>) => void;
replace?: (routeName: string, params?: Record<string, unknown>) => void;
};
export function navigateSafely(
navigation: NavigationLike,
app: AppContextValue,
routeName: RouteName,
params?: Record<string, unknown>
) {
// 해당 프로파일에서 접근 가능한 라우트인지 체크
if (app.canAccessRoute(routeName)) {
navigation.navigate(routeName, params);
return;
}
// 차단된 라우트라면 지정된 fallback 경로로 리다이렉트
if (navigation.replace) {
navigation.replace(app.fallbackRoute);
return;
}
navigation.navigate(app.fallbackRoute);
}
이 계층에 넣을 것과 빼야 할 것 구분하기
여기는 '조립'만 담당하는 곳이라 로직이 너무 많이 들어가면 또 다른 거대한 스파게티 파일이 됩니다. 딱 기준을 정해서 넣고 빼야 합니다.
- 포함할 것: 프로파일 조회, 엔진별 컨트롤러 선택 매핑, 라우트 가드 조건, 초기 화면에 필요한 필수 모델 빌더 연결
- 제외할 것: 실제 API Secret 키(보안 영역), 서버 권한 검증 로직(서버나 도메인 레이어 역할), 복잡한 UI 스타일링이나 비즈니스 정책 전체
자주 하기 쉬운 실수 정리
- App 진입점에 if-else 도배하기: 프로파일 늘어날 때마다 App.tsx 열어서 조건문 수정하고 있다면 구조에 문제가 있는 겁니다.
- fallback을 그냥 배열의 첫 번째 값으로 두기: 개발할 때는 편한데, 잘못하면 운영 환경에서 엉뚱한 브랜드 앱이 켜지는 대참사가 납니다. 환경별 fallback 정책을 확실히 분리하세요.
- 유닛 테스트 건너뛰기: UI를 띄우지 않아도 createAppContext('잘못된ID')를 넣었을 때 안전하게 fallback 프로파일과 컨트롤러가 잡히는지 테스트 코드로 검증해 두는 것이 좋습니다. 나중에 리팩토링할 때 엄청 든든합니다.
마무리
좋은 진입점은 혼자서 많은 일을 하는 파일이 아니라, 필요한 모듈들을 안전하게 조립해서 넘겨주고 빠르게 빠지는 파일입니다. 통합 앱 구조가 커질수록 "어디서 무엇을 결정하느냐"가 코딩 생산성을 좌우하는 것 같습니다.
프로파일 관리가 복잡해지기 시작했다면 이렇게 중간 조립 계층을 하나 두고 묶어서 넘겨주는 방식을 고민해 보시기 바랍니다.
'개발 노트 > React Native' 카테고리의 다른 글
| React Native 통합 앱 프로파일별로 단말기 권한 분리해서 요청하기 (0) | 2026.05.20 |
|---|---|
| [React Native] 통합 앱 프로파일별 Feature Flag 설계 기법 (딥링크, 권한 처리 오류 막기) (0) | 2026.05.20 |
| React Native 통합 앱 profileId 조건문 지우기 (Controller와 Model 분리) (0) | 2026.05.20 |
| React Native 통합 앱 프로파일별 화면 접근 제어 (Route Guard 구현하기) (0) | 2026.05.20 |
| [React Native] 멀티 프로파일 통합 앱 태블릿 및 반응형 화면 최적화 기준 정리 (0) | 2026.05.19 |