본문 바로가기
개발 노트/React Native

[React Native] 통합 앱에서 프로파일, controller, model을 조립하는 진입점 만들기

by pposooj 2026. 5. 20.

React Native 통합 앱에서 여러 프로파일을 하나의 코드베이스로 관리하다 보면 파일은 나눴는데, 실제 앱이 시작되는 지점에서 다시 복잡해지는 경우가 있습니다. profile registry, selectedProfiles, controller, model builder, route guard를 각각 만들었더라도 어디에서 어떤 순서로 조립할지 정하지 않으면 앱 진입점이 금방 지저분해지더라구요.

처음에는 각 파일을 분리해두면 구조가 정리된 것처럼 보였는데 막상 앱 시작 흐름을 생각해보면 현재 프로파일을 찾고, controller를 고르고, route 접근 기준을 만들고, fallback 기준까지 연결해야 했습니다. 이 부분을 한 곳에서 정리하지 않으면 화면이나 네비게이션 코드가 다시 복잡해지는 것 같습니다.

 

이럴 때 필요한 것이 composition layer입니다. 쉽게 말하면 현재 선택된 프로파일을 기준으로 앱에 필요한 설정과 동작을 조립하는 진입점입니다. 화면 컴포넌트가 직접 registry를 뒤지고, controller를 고르고, fallback route를 결정하게 두지 않고 시작 지점에서 필요한 구성요소를 묶어 넘기는 방식입니다.

이 글에서는 React Native 통합 앱에서 프로파일, controller, model을 조립하는 진입점 설계 기준을 정리해보겠습니다. 통합 앱 구조를 정리할 때 놓치기 쉬운 부분을 중심으로 살펴보겠습니다.

이 글에서 다룰 문제

조립 진입점이 없으면 다음 문제가 생길 수 있습니다.

  • 앱 시작 파일에서 profileId 조건문이 계속 늘어난다.
  • 화면마다 controller를 직접 선택해 중복 코드가 생긴다.
  • 존재하지 않는 profileId가 들어왔을 때 fallback 기준이 제각각이다.
  • route guardfeature flag 기준이 화면마다 다르게 적용된다.
  • model builder와 controller 연결이 흩어져 테스트하기 어렵다.
  • fallback 화면에서 내부 profileId, 서버 정보, 디버그 메시지가 노출될 수 있다.

처음에는 각 파일이 작아서 문제가 없어 보일 수 있지만 통합 앱에서 프로파일이 늘어나면 앱 시작 시 무엇을 먼저 결정해야 하는지가 중요해집니다. 이 순서가 흐트러지면 새 프로파일을 추가할 때 수정 범위를 예측하기 어려워집니다.

composition layer란 무엇인가

composition layer는 여러 모듈을 실제 앱 실행에 맞게 연결하는 계층입니다. React Native 통합 앱에서는 보통 아래 정보를 한 번에 조립합니다.

구성 요소 역할
selected profile 현재 실행할 프로파일 결정
profile registry 프로파일 설정 조회
controller factory 프로파일에 맞는 controller 선택
model builder 원본 데이터를 화면용 model로 변환
route guard 접근 가능한 화면과 fallback 결정
feature flag 메뉴와 기능 노출 기준 제공
error boundary 잘못된 설정이나 런타임 오류를 안전하게 처리

이 계층이 있으면 화면 컴포넌트는 현재 앱이 어떤 프로파일인지 매번 직접 판단하지 않아도 됩니다. 대신 이미 조립된 app context나 props를 받아서 화면을 렌더링할 수 있습니다.

처음에는 단순히 파일을 하나 더 만드는 것처럼 느껴질 수 있습니다. 하지만 앱 시작 흐름에서 결정해야 하는 기준을 모아두면, 화면 컴포넌트가 훨씬 가벼워지는 것 같습니다.

기본 조립 순서

통합 앱의 시작 흐름은 아래처럼 정리해볼 수 있습니다.

  1. 실행 환경에서 profileId를 읽는다.
  2. profile registry에서 profile을 찾는다.
  3. profile이 없거나 비활성 상태라면 안전한 fallback을 선택한다.
  4. profile.engine 또는 controllerKey로 controller를 선택한다.
  5. route guardfeature flag helper를 만든다.
  6. AppProvider 또는 RootNavigator에 조립된 context를 전달한다.
  7. 화면은 context를 통해 필요한 model과 action만 사용한다.

이 순서에서 중요한 점은 실패 처리입니다. profileId가 잘못됐을 때 앱이 바로 크래시하거나 내부 정보를 보여주면 안 됩니다. 사용자에게는 안전한 안내 화면을 보여주고, 개발 로그에는 필요한 최소 정보만 남기는 방식이 좋습니다.

예제 코드: app context 조립하기

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;
};

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,
  },
};

const controllers: Record<Engine, AppController> = {
  tour: {buildInitialModels: async () => []},
  filedata: {buildInitialModels: async () => []},
  utility: {buildInitialModels: async () => []},
};

function resolveProfile(profileId: string): AppProfile | null {
  const profile = profileRegistry[profileId];

  if (!profile || !profile.enabled) {
    return null;
  }

  return profile;
}

export function createAppContext(profileId: string): AppContextValue {
  const profile = resolveProfile(profileId) || profileRegistry.sampleTour;
  const controller = controllers[profile.engine];

  return {
    profile,
    controller,
    fallbackRoute: profile.fallbackRoute,
    canAccessRoute: (routeName) => profile.enabledRoutes.includes(routeName),
  };
}

이 예제에서 createAppContext()composition layer 역할을 합니다. 화면은 registry와 controller를 직접 고르지 않고, 이미 만들어진 context를 사용합니다.

처음에는 화면에서 직접 getProfile()을 호출하고 controller를 고르는 방식이 더 빠르게 느껴질 수 있지만 이런 코드가 여러 화면에 흩어지면 나중에 fallback 정책이나 route 기준을 바꿀 때 수정 지점이 많아집니다. 시작 지점에서 한 번 조립해 넘기는 방식이 더 안정적인 것 같습니다.

fallback profile을 정할 때 주의할 점

fallback profile은 조심해서 정해야 합니다. 잘못된 profileId가 들어왔을 때 내부 정보를 그대로 보여주거나, 관리용 프로파일로 이동하면 안 됩니다.

상황 권장 처리
profileId가 없음 안전한 기본 프로파일 또는 안내 화면
profileId가 잘못됨 fallback profile로 이동하고 로그 기록
비활성 프로파일 사용자에게 일반 오류 화면 표시
controller가 없음 앱 시작 중단 또는 안전한 fallback 화면
enabledRoutes가 비어 있음 설정 오류로 보고 개발 로그 기록

사용자 화면에서는 앱 설정을 불러오지 못했다는 정도의 일반적인 문구만 보여주는 편이 안전합니다. 내부 profileId, 서버 URL, 관리자 경로, 디버그 스택은 공개 화면에 표시하지 않아야 합니다.

RootNavigator에 조립된 context 전달하기

조립된 context는 Provider나 props로 RootNavigator에 전달할 수 있습니다.

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 is missing.');
  }

  return context;
}

이렇게 하면 하위 화면에서는 useAppRuntime()으로 현재 프로파일과 route guard를 사용할 수 있습니다. 다만 모든 화면에서 context를 직접 많이 참조하면 다시 결합도가 높아질 수 있습니다. 자주 쓰는 판단은 helper로 감싸는 편이 좋습니다.

route guard와 연결하기

composition layer에서 만든 canAccessRoute는 navigation helper에서 사용할 수 있습니다.

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;
  }

  if (navigation.replace) {
    navigation.replace(app.fallbackRoute);
    return;
  }

  navigation.navigate(app.fallbackRoute);
}

핵심은 routeName이 허용되는지 판단하는 기준이 한 곳에서 온다는 점입니다. 메뉴, 딥링크, 푸시 알림 클릭도 같은 helper를 쓰면 접근 기준이 흔들릴 가능성이 줄어듭니다.

처음에는 메뉴에서만 조건을 맞추면 충분해 보일 수 있습니다. 하지만 딥링크나 알림처럼 외부에서 들어오는 경로까지 생각하면, 공통 navigation helper를 두는 쪽이 더 안전한 것 같습니다.

composition layer에 넣을 것과 빼야 할 것

composition layer는 조립 계층이기 때문에 너무 많은 책임을 넣으면 금방 복잡해집니다. 시작 지점에서 꼭 결정해야 하는 것만 모으는 계층으로 이해하는 편이 좋습니다.

항목 포함 여부 이유
profile 조회 포함 앱 실행 기준 결정
controller 선택 포함 프로파일별 동작 연결
route guard helper 포함 화면 접근 기준 제공
model builder 연결 조건부 포함 초기 화면 model 준비에 유용
실제 API 키 제외 보안 설정 영역
서버 권한 검증 제외 서버에서 처리해야 함
복잡한 UI 스타일 제외 component 또는 responsive hook 역할
비즈니스 정책 전체 주의 controller나 domain layer로 분리 필요

모든 로직을 composition layer에 몰아넣으면 새로운 거대한 파일이 될 수 있습니다. profile 조회, controller 선택, route guard 연결처럼 앱 시작 시 반드시 필요한 조립 기준만 담는 편이 좋습니다.

테스트 기준

composition layer는 단위 테스트로 검증하기 좋은 부분입니다. UI를 띄우지 않아도 profile 조립, controller 선택, fallback 적용 여부를 확인할 수 있기 때문입니다.

  • 정상 profileId를 넣으면 올바른 profile과 controller가 조립되는지 확인한다.
  • 잘못된 profileId를 넣으면 안전한 fallback이 적용되는지 확인한다.
  • 비활성 profile은 실행 대상에서 제외되는지 확인한다.
  • initialRoutefallbackRouteenabledRoutes에 포함되는지 확인한다.
  • controller가 없는 engine은 명확한 오류로 처리되는지 확인한다.
  • 사용자에게 내부 profileId나 서버 정보가 노출되지 않는지 확인한다.
  • route guard helper가 허용 route와 차단 route를 정확히 판단하는지 확인한다.

이 테스트는 UI를 띄우지 않아도 검증할 수 있습니다. 그래서 앱이 커지기 전에 먼저 붙여두면 회귀를 줄이는 데 도움이 됩니다. 처음에는 작은 함수 테스트처럼 보이지만, 실제로는 앱 시작 흐름을 지켜주는 기준이 될 수 있습니다.

자주 하는 실수

1. App 진입점에서 너무 많은 조건문을 직접 처리하는 경우가 있습니다. 시작 파일이 커지면 나중에 새 프로파일을 추가할 때 어떤 조건을 건드려야 하는지 알기 어려워집니다.

2. fallback을 그냥 첫 번째 프로파일로 처리하는 경우도 있습니다. 개발 환경에서는 편할 수 있지만, 운영 환경에서는 잘못된 앱이 열리는 문제로 이어질 수 있습니다. 환경별 fallback 정책을 분리하는 편이 좋습니다.

3. composition layer에 secret을 넣는 경우가 있습니다. 앱 시작에 필요한 값이 많아 보인다고 해서 API 키, 토큰, 관리자 URL을 한 곳에 모으면 보안 위험이 커집니다.

4. controller와 model builder 선택 기준을 테스트하지 않는 경우도 있습니다. 빌드는 되지만 실제 프로파일에 맞지 않는 controller가 연결되면 화면 데이터가 이상하게 보일 수 있습니다.

결론

React Native 통합 앱에서 프로파일, controller, model을 각각 분리했다면 마지막으로 필요한 것은 이들을 조립하는 진입점입니다. composition layer를 두면 앱 시작 시 profile registry, controller factory, model builder, route guard를 한 기준으로 연결할 수 있습니다.

 

좋은 진입점은 많은 일을 하는 파일이 아니라, 필요한 구성요소를 안전하게 조립하고 하위 화면에 넘겨주는 파일입니다. profileId가 잘못됐을 때 fallback을 어떻게 처리할지, controller가 없을 때 어떻게 실패할지, route guard가 어떤 기준을 볼지 명확해야 합니다.

 

통합 앱은 구조가 커질수록 어디서 무엇을 결정하는지가 중요해집니다. profile은 registry에서 찾고, controller는 factory에서 고르고, 화면은 model을 렌더링하게 만들면 새 프로파일을 추가할 때 확인해야 할 지점이 훨씬 선명해지는 것 같습니다.

참고 자료

  • React Context와 Provider 관련 React 공식 문서
  • React Navigation deep link와 navigation 구조 관련 문서
  • TypeScript factory pattern, type guard 관련 문서
  • 프로젝트 내부 profile registry, selectedProfiles, controller/model, route guard 문서