React Native로 여러 프로필(또는 다양한 서비스 flavor)을 하나의 앱으로 통합해서 운영하다 보면, 가장 먼저 복잡해지고 꼬이는 게 바로 화면 라우팅(Routing)입니다.
어떤 프로필은 지도 화면이 메인이고, 어떤 프로필은 리스트 화면 중심이며, 또 다른 프로필은 간단한 설정이나 안내 화면만 있으면 되는 경우가 있죠. 이 차이를 화면 코드 곳곳에 if (profile === 'A') 같은 조건문으로 처리하다 보면, 나중에 새 프로필을 추가할 때마다 꼭 어디선가 사이드 이펙트(회귀 오류)가 터집니다. 화면 컴포넌트 안에 프로필별 분기 코드가 늘어나면 나중에는 어떤 화면이 어느 앱에서 열려야 하는지 추적하기도 힘들어지더군요.
저의 경우도 이 문제로 고생을 좀 하다가, 라우팅 정책을 한곳으로 모으는 Route Registry 방식을 도입해서 구조를 정리했습니다. 자꾸 까먹기도 하고 나중에 참고하려고 블로그에 기록해 둡니다.
Route Registry가 필요한 이유
이래저래 앱을 운영하다 보면 라우트가 잘못 연결되어 데이터는 정상인데 사용자가 빈 화면을 보거나 엉뚱한 상세 화면으로 튕기는 일이 생깁니다. 특히 여러 프로필을 통합한 앱에서는 '현재 프로필에서 이 라우트가 정말 존재하는가?'를 코드로 확실히 보장해 주어야 합니다.
| 문제 원인 | 기존 방식 (기존 문제) | 해결 방향 (Registry 도입) |
| 없는 화면으로 이동 | Route 이름을 하드코딩 문자열로 직접 입력 | Route Registry를 통해 허용된 목록으로 제한 |
| 타 프로필 화면 노출 | Group Guard(진입 장벽) 없음 | Profile Group별 허용 Route 관리 |
| 첫 화면 오류 | initialRoute 불일치 및 꼬임 | Registry 근처에서 시작 화면을 함께 관리 |
| 테스트 누락 | 대표 프로필 한두 개만 수동 확인 | Group별 Smoke Test 및 단위 테스트 추가 |
Route 이름을 하드코딩된 문자열로 화면 곳곳에 흩어두면, 나중에 라우트 명칭 하나 바꿀 때마다 수정 범위를 찾기 어렵습니다. 그래서 Profile Group, 시작 화면, Fallback(대체) 기준을 한곳(Registry)에서 관리하는 편이 정신 건강에 좋습니다.
Route Registry 구현 예시
아래는 제가 실무에서 프로필 그룹별로 사용할 수 있는 라우트를 정의하고, 특정 그룹에서 열 수 있는 화면인지 검증하도록 만든 구조입니다.
type ProfileGroup = 'tour' | 'filedata' | 'utility';
type RouteName = 'Home' | 'Map' | 'List' | 'Detail' | 'Settings';
type RouteConfig = {
name: RouteName;
componentKey: string;
groups: ProfileGroup[];
};
export const routeRegistry: RouteConfig[] = [
{name: 'Home', componentKey: 'HomeScreen', groups: ['tour', 'utility']},
{name: 'Map', componentKey: 'MapScreen', groups: ['tour', 'filedata']},
{name: 'List', componentKey: 'ListScreen', groups: ['tour', 'filedata']},
{name: 'Detail', componentKey: 'DetailScreen', groups: ['tour', 'filedata']},
{name: 'Settings', componentKey: 'SettingsScreen', groups: ['tour', 'filedata', 'utility']},
];
// 해당 그룹이 사용할 수 있는 라우터 목록 가져오기
export function getRoutesByGroup(group: ProfileGroup) {
return routeRegistry.filter((route) => route.groups.includes(group));
}
// 접근 가능한 라우트인지 검증
export function canOpenRoute(group: ProfileGroup, routeName: RouteName) {
return getRoutesByGroup(group).some((route) => route.name === routeName);
}
이렇게 구조를 짜두면 화면 컴포넌트가 프로필별 조건을 일일이 알 필요가 없어집니다. 라우팅 정책은 Registry가 판단하고, 화면은 렌더링과 사용자 동작 처리에만 집중할 수 있습니다. 나중에 새 프로필이 추가되어도 화면 파일들을 일일이 수정할 필요 없이, 이 Registry 파일에 그룹만 쏙 추가해 주면 되니 흐름이 아주 명확해집니다.
initialRoute(시작 화면)도 함께 관리하기
프로필 그룹마다 앱을 켰을 때 보여줘야 하는 첫 화면(initialRoute)이 다를 수 있습니다. 이것도 Registry와 가까운 곳에서 함께 관리해 주는 것이 안전합니다. 시작 화면이 허용된 라우트 목록에 없으면 앱 실행 직후 빈 화면이 뜨거나 무한 무한 루프 fallback이 생길 수 있거든요.
type ProfileRouteConfig = {
group: ProfileGroup;
initialRoute: RouteName;
};
export const profileRouteConfigs: ProfileRouteConfig[] = [
{group: 'tour', initialRoute: 'Map'},
{group: 'filedata', initialRoute: 'List'},
{group: 'utility', initialRoute: 'Home'},
];
export function getInitialRoute(group: ProfileGroup): RouteName {
return profileRouteConfigs.find((config) => config.group === group)?.initialRoute ?? 'Home';
}
운영 관점에서는 이 시작 화면 처리가 꽤 중요합니다. 앱이 실행되자마자 꼬인 라우트로 진입하면, 나머지 기능이 아무리 완벽해도 사용자는 "앱이 깨졌다"고 느끼고 바로 이탈합니다.
외부 진입을 막기 위한 Group Guard
Registry를 짜두는 것만으로도 좋지만, 딥링크(Deep Link)나 푸시 알림 클릭처럼 앱 외부에서 특정 화면으로 다이렉트 패스로 치고 들어오는 상황이 복잡합니다. 외부에서 던져준 Route 이름을 검증 없이 그대로 열어버리면, 현재 프로필에서는 지원하지 않는 화면이 덜컥 열려버릴 수 있습니다.
export function resolveRouteOrFallback(
group: ProfileGroup,
requestedRoute: RouteName,
): RouteName {
// 허용된 라우트라면 그대로 진행
if (canOpenRoute(group, requestedRoute)) {
return requestedRoute;
}
// 허용되지 않은 라우트라면 안전한 기본 화면으로 토스
return group === 'utility' ? 'Home' : 'List';
}
이렇게 Fallback 처리(안전장치)를 두면 잘못된 외부 요청이 들어와도 앱이 터지는 대신 안전한 기본 화면으로 사용자를 안내할 수 있습니다.
💡 실무 팁: > Fallback이 너무 조용하게 지나가면 개발자가 문제를 놓칠 수 있습니다. 개발 단계(__DEV__)에서는 console.warn이나 에러 로그를 남겨서 기획 오류나 잘못된 딥링크 페이로드를 바로 인지할 수 있도록 장치를 해두는 편이 좋습니다.
UI 메뉴에서만 버튼을 숨긴다고 해결되는 게 아닙니다. 딥링크나 알림 등 외부 인풋까지 고려해서 실제 Route 접근 조건을 이중으로 락(Guard)을 걸어두어야 안전합니다.
최소한의 Smoke Test 기준 세우기
프로필 개수가 늘어날수록 빌드할 때마다 모든 화면을 수동으로 일일이 눌러보기는 불가능에 가깝습니다. 배포 전 최소한의 안전망으로 아래와 같은 Smoke Test 기준을 잡아두는 것이 좋습니다.
| 테스트 항목 | 주요 확인 내용 |
| initialRoute | 프로필별 지정된 시작 화면이 정상적으로 잘 열리는가? |
| route list | 현재 Group에 할당된 허용 화면들만 올바르게 등록되었는가? |
| deep link | 허용되지 않은 라우트 접근 시 지정된 Fallback 화면으로 잘 튕겨내는가? |
| empty state | 데이터가 없는 프로필 환경에서 빈 화면 안내(Empty View)가 잘 노출되는가? |
| representative profile | 그룹별 대표 프로필을 하나씩 지정해 두고 기본 기능이 깨지지 않는가? |
Smoke Test는 모든 세부 기능을 딥하게 검증하는 테스트라기보다는, 앱의 메인 뼈대가 무너지지 않았는지 확인하는 최소한의 방어선입니다. 통합 앱 환경에서는 그룹별 대표 프로필을 하나씩 픽스해 두고 돌려보면 회귀 에러를 훨씬 빨리 잡을 수 있습니다.
Jest로 Registry 함수 유닛 테스트하기
화면 UI 컴포넌트를 일일이 렌더링(Render)하지 않더라도, 우리가 작성한 Registry 함수(비즈니스 로직)만큼은 Jest로 가볍고 빠르게 검증할 수 있습니다. 이렇게 분리해 두면 테스트 짜기가 아주 편합니다.
test('utility 그룹은 Map 라우트를 열 수 없어야 한다', () => {
expect(canOpenRoute('utility', 'Map')).toBe(false);
});
test('tour 그룹은 Detail 라우트를 열 수 있어야 한다', () => {
expect(canOpenRoute('tour', 'Detail')).toBe(true);
});
test('지원하지 않는 라우트로 접근 시 안전한 홈 화면으로 fallback 되어야 한다', () => {
expect(resolveRouteOrFallback('utility', 'Map')).toBe('Home');
});
이런 유닛 테스트들은 코드가 작아 보여도 나중에 라우팅 정책이나 그룹이 추가/변경될 때 엄청나게 큰 힘을 발휘합니다. 컴포넌트 렌더링 테스트보다 수백 배 빠르고, 새 프로필 추가 시 빼먹은 라우트가 없는지 배포 전에 바로 검증해 주니까요.
원본 앱과 달라진 UX 기록 남기기 (중요)
기존에 따로 놀던 개별 앱들을 하나의 통합 앱으로 마이그레이션하다 보면 원본 앱과 UX나 화면 흐름이 조금씩 달라질 수밖에 없습니다. 이때 중요한 것은 달라진 사실을 대충 넘기지 말고 무조건 흔적(근거)을 남겨두는 것입니다.
- 원본 앱에는 있던 화면을 통합 앱에서 제외했다면 제외 사유를 주석이나 문서에 남긴다.
- 화면 진입 순서가 바뀌었다면 변경된 기획 이유를 문서화한다.
- Route 이름을 리팩토링하며 바꿨다면 이전 이름과 새 이름의 매핑 테이블을 적어둔다.
- 아직 기능 싱크(Parity) 확인이 덜 끝난 화면은 작업 완료로 표시하지 않는다.
단순한 문서 작업 같아 보여도 나중에 유지보수할 때나 QA 도중 "이 화면 예전 앱엔 있었는데 왜 지금은 안 나와요?" 혹은 "첫 화면 왜 바뀐 거죠?" 같은 질문이 들어왔을 때, 과거의 히스토리를 추적할 수 있는 유일한 생명줄이 됩니다. 코드만 보고는 당시의 기획적/UX적 판단 근거를 알 수 없기 때문입니다.
⚠️ 자주 하는 실수 리스트 (체크리스트)
- 화면 컴포넌트 안에서 profileId 조건문 계속 늘리기
- 당장 구현할 때는 이 방식이 가장 빨라 보이지만, 프로필이 5개, 10개로 늘어나면 나중에 걷잡을 수 없어집니다. 어떤 화면이 도대체 어느 앱에 쓰이는지 추적 불가 상태가 됩니다. 라우팅 조건은 화면 안이 아니라 독립된 Registry에 던지세요.
- 메뉴(UI)에서만 링크를 숨기고 딥링크 검증 안 하기
- 사용자가 눈으로 보는 메뉴에서 숨겼다고 끝이 아닙니다. 딥링크나 푸시 스키마를 타고 우회 진입할 수 있으므로 앞서 말한 Group Guard 검증은 필수입니다.
- initialRoute가 허용 목록에 있는지 검증 누락
- 앱 시작 라우트가 꼬여버리면 사용자는 앱이 켜지지도 않는 쓰레기 앱이라고 생각합니다. 시작 화면이 해당 그룹의 허용 범위에 있는지 꼭 더블 체크하세요.
- 원본 앱과 달라진 UX 히스토리 미기록
- 나중에 화면 빠진 이유나 화면 시퀀스가 바뀐 이유를 기억해 내지 못해 기획 서류를 다시 다 뒤져야 하는 불상사가 생깁니다. 변경 사유는 짧게라도 주석이나 커밋 로그, 사내 위키에 꼭 남깁시다.
결론
React Native 통합 앱 환경에서 Route Registry는 화면을 연결하는 단단한 기준표가 됩니다. Profile Group별 화면 권한, 시작 라우트, Fallback 예외 처리, 그리고 테스트 코드까지 한곳으로 몰아서 구조화를 해두면 새 프로필을 계속 얹어도 배포할 때 두려움이 훨씬 줄어듭니다.
화면 컴포넌트 내부에 분기 조건문 도배하지 마시고, Registry와 Guard 단계에서 진입 가능 여부를 먼저 걸러내는 구조로 짜보시는 것을 추천합니다. 유지보수 생산성이 정말 많이 올라갑니다.
'개발 노트 > React Native' 카테고리의 다른 글
| React Native 통합 앱 프로필별 데이터 분기 처리 해결 방법 (Normalizer, Controller 분리) (0) | 2026.05.21 |
|---|---|
| React Native 통합 앱에서 프로파일별 딥링크와 푸시 라우팅 안전하게 검증하기 (0) | 2026.05.20 |
| React Native 통합 앱 프로파일별로 단말기 권한 분리해서 요청하기 (0) | 2026.05.20 |
| [React Native] 통합 앱 프로파일별 Feature Flag 설계 기법 (딥링크, 권한 처리 오류 막기) (0) | 2026.05.20 |
| React Native 통합 앱 여러 프로파일 진입점 복잡할 때 정리하는 방법 (Composition Layer) (0) | 2026.05.20 |