[React Native] 멀티 프로파일 통합 앱 태블릿 및 반응형 화면 최적화 기준 정리
React Native로 앱을 개발하다 보면 처음에는 보통 스마트폰 세로 화면만 보고 레이아웃을 잡기 마련입니다. 저의 경우도 레이아웃을 정리할 때 가장 먼저 떠올리는 기준은 휴대폰 세로 화면이었습니다.
하지만 하나의 코드베이스로 여러 프로파일(플랫폼 내 서비스 계열)을 동시에 운영하는 통합 앱을 만들다 보면 화면 조건이 생각보다 훨씬 복잡해집니다. 일반 휴대폰 세로/가로 모드는 물론이고, 태블릿 세로/가로, Fold 같은 대화면 기기까지 전부 하나의 코드로 대응해야 하기 때문입니다.
처음에는 단순히 width: '100%'만 주면 알아서 늘어날 것 같지만, 실제로 운영해 보면 화면 최적화는 그렇게 만만하지 않습니다. 콘텐츠 최대 너비를 제한할지, 버튼을 한 줄로 둘지 줄바꿈할지, split-pane(리스트+상세)을 적용할지, safe area나 키보드 영역 처리는 어떻게 할지 등 고려할 예외가 쏟아집니다.
나중에 화면이 뒤늦게 깨져서 고생하는 일이 많아 자꾸 까먹지 않으려고 실무 적용 기준을 정리해 둡니다.
반응형 기준이 없을 때 발생하는 문제 상황
통합 앱을 운영하면서 명확한 반응형 기준이 없으면 아래와 같은 문제가 꼭 생깁니다.
- 태블릿 가득 차는 본문: 콘텐츠가 화면 전체로 무작정 늘어나서 시선 이동이 너무 길어지고 가독성이 떨어집니다.
- 가로 모드 컴포넌트 겹침: 좁아진 세로 높이 때문에 버튼과 검색 영역, 혹은 입력창이 서로 겹쳐버립니다.
- 작은 화면 터치 영역 누락: 화면이 작은 기기나 폰트가 큰 기기에서 액션 버튼이 잘리거나 터치하기 어려워집니다.
- Safe Area 미고려: 노치나 하단 제스처 영역을 계산하지 않아 상하단 주요 UI가 가려집니다.
- 글자 크기(Font Scale) 깨짐: 사용자가 시스템 글자 크기를 크게 키우면 카드나 버튼 안의 텍스트가 밖으로 튀어나갑니다.
- 프로파일별 회귀(Regression) 발생: 한쪽 서비스 프로파일만 확인하고 배포했다가 다른 서비스 프로파일의 고유 화면(지도, 광고 영역 등)이 깨지는 현상이 생깁니다.
단일 앱이라면 화면 몇 개만 에뮬레이터로 직접 돌려보면서 버틸 수 있지만, 프로파일이 많아지면 공통 Screen wrapper나 Responsive hook을 구조적으로 잡아두고 대응하는 편이 훨씬 안전합니다.
화면 최적화의 기본 기준
React Native에서 화면을 설계할 때는 너비(width) 하나만 보면 안 됩니다. 방향, 글자 크기, 안전 영역까지 세트로 묶어서 기준을 나눠야 합니다.
| 기준 | 설명 | 예시 |
| 화면 너비 | compact, medium, expanded 크기 분류 | 360, 600, 840dp 기준 |
| 방향 | 세로 / 가로 모드 구별 | portrait, landscape |
| 콘텐츠 최대 너비 | 태블릿에서 본문이 너무 넓어지지 않게 제한 | maxWidth: 720 |
| 레이아웃 방식 | 단일 컬럼 또는 split-pane 결정 | 리스트 + 상세 동시 표시 |
| 액션 배치 | 버튼 한 줄 표시 또는 줄바꿈(wrap) | 작은 화면이나 큰 글씨에서 wrap 처리 |
| Safe Area | 노치, 상태바, 하단 제스처 영역 대응 | padding inset 값 활용 |
| Font Scale | 큰 글자 설정 대응 | 글자 크기에 따른 버튼 높이 및 줄바꿈 허용 |
특히 fontScale과 휴대폰 가로 모드는 개발할 때 정말 놓치기 쉬운 부분입니다. 세로 화면에서 예쁘게 보이던 카드나 버튼도 가로 모드와 큰 글자 설정이 겹치면 여지없이 깨집니다.
공통 Responsive Metrics 구조 잡기
화면 컴포넌트마다 Dimensions.get('window').width 조건문을 직접 넣기 시작하면 기준이 사방으로 흩어집니다. 나중에 뷰포트 breakpoint 기준을 바꾸려고 할 때 찾아서 수정하는 게 지옥이 됩니다.
그래서 중앙에서 관리하는 metrics 연산 함수를 하나 만들어 두고 쓰는 게 깔끔합니다.
export type WindowSizeClass = 'compact' | 'medium' | 'expanded';
export type Orientation = 'portrait' | 'landscape';
export type ResponsiveMetrics = {
width: number;
height: number;
safeWidth: number;
orientation: Orientation;
sizeClass: WindowSizeClass;
contentMaxWidth: number;
shouldUseSplitPane: boolean;
shouldWrapActions: boolean;
};
export function getWindowSizeClass(width: number): WindowSizeClass {
if (width >= 840) return 'expanded';
if (width >= 600) return 'medium';
return 'compact';
}
export function getResponsiveMetrics(params: {
width: number;
height: number;
fontScale: number;
}): ResponsiveMetrics {
const {width, height, fontScale} = params;
const orientation = width > height ? 'landscape' : 'portrait';
const sizeClass = getWindowSizeClass(width);
const contentMaxWidth = sizeClass === 'expanded' ? 960 : 720;
const safeWidth = Math.min(width, contentMaxWidth);
return {
width,
height,
safeWidth,
orientation,
sizeClass,
contentMaxWidth,
shouldUseSplitPane: sizeClass === 'expanded' && orientation === 'landscape',
shouldWrapActions: safeWidth < 360 || fontScale >= 1.3,
};
}
이렇게 metrics를 빼두면 개별 화면 컴포넌트에서는 숫자를 비교할 필요가 없습니다. 대신 shouldUseSplitPane, shouldWrapActions 같은 명확한 네이밍의 boolean 값을 보고 레이아웃을 분기하면 되므로 코드가 훨씬 직관적으로 바뀝니다.
Screen Wrapper로 공통 기준 적용
공통 기준을 만들었다면 개별 화면에서 매번 불러오지 말고, 전체 화면을 감싸는 공통 Wrapper 컴포넌트를 적용하는 편이 안전합니다.
import React from 'react';
import {PixelRatio, useWindowDimensions, View} from 'react-native';
export function ResponsiveScreen({children}: {children: React.ReactNode}) {
const {width, height} = useWindowDimensions();
const fontScale = PixelRatio.getFontScale();
const metrics = getResponsiveMetrics({width, height, fontScale});
return (
<View
testID="responsive-screen"
style={{
flex: 1,
alignItems: 'center',
paddingHorizontal: metrics.sizeClass === 'compact' ? 16 : 24,
}}
>
<View style={{width: '100%', maxWidth: metrics.contentMaxWidth}}>
{children}
</View>
</View>
);
}
여기서 핵심은 태블릿이라고 해서 화면 전체를 무조건 넓게 채우지 않는 것입니다. 텍스트 중심의 본문형 화면은 maxWidth를 걸어 쥐어짜 주어야 눈이 편합니다. (단, 대시보드나 지도가 메인인 화면은 예외 처리를 할 수 있게 별도 옵션을 둡니다.)
핵심: 가로 모드와 태블릿은 완전히 다른 문제입니다
많은 분들이 실수하는 게 width >= 600이면 무조건 태블릿 레이아웃을 띄우려고 하는 점입니다. 가로 모드와 태블릿은 성격이 완전히 다릅니다.
- 일반 스마트폰 가로 모드: 너비는 넓어지지만 세로 높이가 급격히 낮아집니다. 즉, 상하 공간이 부족하므로 상단 헤더 영역을 축소하고 무조건 스크롤이 되도록 보장해야 합니다.
- 태블릿 세로 모드: 너비는 꽤 넓지만 세로로 길기 때문에 split-pane을 억지로 넣으면 화면이 조잡해집니다. 이때는 단일 컬럼을 유지하되 좌우 여백(maxWidth)만 잡아주는 게 자연스럽습니다.
| 화면 상태 | 주요 문제 | 권장 기준 |
| 휴대폰 세로 | 좁은 너비 | 단일 컬럼, 필요시 액션 wrap |
| 휴대폰 가로 | 낮은 세로 높이 | 상단 영역 축소, 콘텐츠 스크롤 필수 보장 |
| 태블릿 세로 | 넓은 너비 | maxWidth 제한, 좌우 균형 여백 확보 |
| 태블릿 가로 | 넓은 너비와 높이 | split-pane(분할 화면) 적극 검토 |
| Fold 펼침 | 중간/확장 경계 모호 | breakpoint 경계면 테스트 및 캡처 확인 |
| 큰 글자 설정 | 텍스트 넘침 현상 | 버튼 높이 고정 해제, 줄바꿈(flexWrap) 허용 |
프로파일별 회귀 테스트가 필요한 이유
통합 앱 구조에서는 공통 Wrapper를 같이 쓰더라도 서비스 프로파일에 따라 첫 화면 구조와 핵심 기능이 완전히 다릅니다. 지도가 중요한 서비스가 있고, 리스트-상세 뷰가 중요한 서비스가 있으며, 단일 기능 유틸리티 화면이 메인인 서비스가 있습니다.
따라서 훅이나 Wrapper 코드를 수정했다면, 귀찮더라도 아래와 같이 계열별 대표 프로파일을 최소 하나씩 띄워서 확인해봐야 회귀 오류를 막을 수 있습니다.
- tour-regional 계열: 홈, 지도 화면, 상세 페이지, 설정 탭
- tour-utility 계열: 검색창, 지도 UI, 필터 팝업, 외부 링크 이동
- filedata 계열: 파일 리스트, 지도 마커, 상세 액션 레이아웃
- climbing 계열: 코스 정보/지도, 날씨 인포그래픽, 상세 레이아웃
- nongsaro 계열: 카테고리 대분류, 목록 리스트, 상세, 통합 검색
- utility 계열: 단일 기능 기능 화면 전체, 환경설정
- hybrid 계열: WebView 영역 전체, 외부 링크 팝업, 하단 Safe area 인셋
특정 서비스 화면 하나만 보고 "반응형 잘 들어가네" 하고 넘어가면, 다른 프로파일 계열에서 하단 하이브리드 웹뷰 영역이 가려지거나 AdMob 광고 영역이 콘텐츠를 밀어내서 깨지는 불상사가 생깁니다.
배포 전 필수 체크리스트 매트릭스
PR을 올리거나 스테이징 빌드를 뽑기 전에 아래 매트릭스를 기준으로 검증을 거치는 습관을 들이는 게 좋습니다. 처음부터 자동화하기 어렵다면 기기별 스크린샷 캡처 스크립트를 짜두거나, 손으로라도 기록을 남겨야 나중에 편합니다.
- [ ] 일반 휴대폰 세로: 기본 레이아웃, 버튼, 카드, 스크롤 작동에 이상이 없는가?
- [ ] 일반 휴대폰 가로: 좁아진 높이 때문에 상단 헤더와 하단 액션 버튼이 겹치지 않는가?
- [ ] 태블릿 세로: 본문 내용이 너무 찢어지지 않게 maxWidth와 좌우 여백이 적절한가?
- [ ] 태블릿 가로: split-pane 분할 뷰가 부드럽게 출력되며 상세 영역이 제대로 보이는가?
- [ ] Fold/대화면 기기: 화면을 접고 펼치며 breakpoint가 바뀌는 경계에서 레이아웃이 튀지 않는가?
- [ ] 큰 글자 설정: 시스템 fontScale을 올렸을 때 버튼, 탭바, 카드 안의 텍스트가 잘리지 않는가?
- [ ] Safe Area: 노치, 상태바, 최신 폰의 하단 인디케이터 바 영역에 주요 UI가 씹히지 않는가?
- [ ] 키보드/검색: 검색창에 포커싱되어 키보드가 올라올 때 입력 영역이나 전송 버튼이 가려지지 않는가?
- [ ] 광고 영역: 광고 placeholder 레이아웃이 로딩 전후로 콘텐츠를 급격하게 밀어내거나 덮지 않는가?
- [ ] 성능 검증: 대화면 리스트나 지도 렌더링 시 불필요한 재렌더링으로 과도하게 버벅이지 않는가?
자주 하는 실수 4가지 요약
- 무조건 flex: 1이나 width: '100%'로 떼우기: 대화면에서 가독성이 완전히 무너집니다. 본문은 제한적인 너비를 주는 버릇을 들여야 합니다.
- 가로 모드와 태블릿을 동일시하기: 스마트폰 가로 모드는 '낮은 높이' 대응이 핵심이고, 태블릿은 '넓은 너비' 여백 활용이 핵심입니다. 접근 방식 자체가 달라야 합니다.
- 시스템 글자 크기(Font Scale) 무시하기: 고정 높이(height: 50)를 준 버튼들은 글자 크기가 커지면 텍스트 하단이 다 잘려 나갑니다. 줄바꿈을 허용하거나 패딩 기반으로 높이가 늘어나게 설계해야 안전합니다.
- 한 프로파일만 테스트하고 배포하기: 통합 앱 특성상 공통 코드 수정이 뚱딴지없는 다른 프로파일의 고유 컴포넌트를 깨뜨릴 수 있습니다. 주요 타깃 프로파일 교차 검증은 필수입니다.
추천 적용 순서
기존에 짜인 화면이 많다면 한 번에 다 고치려고 욕심부리기보다 아래 단계로 조금씩 이관하는 걸 추천합니다.
- 중앙 통제용 공통 responsive metrics 연산 함수 작성
- 내 프로젝트 상황에 맞는 compact, medium, expanded 크기 breakpoint 확정
- 공통 Screen wrapper를 만들어 점진적으로 화면에 적용 (maxWidth, padding 기준 통일)
- 휴대폰 가로 모드 예외 케이스와 태블릿 가로 모드 분리 처리
- 시스템 큰 글자 설정 시 액션 버튼 레이아웃이 깨지지 않게 flexWrap 등 예외 적용
- 운영 중인 프로파일 계열별 대표 화면 매트릭스 문서화
- 여유가 된다면 스크린샷 캡처 스크립트나 connected UI 테스트 세팅으로 회귀 검증 자동화
이 순서대로 뼈대부터 심어두면 화면 컴포넌트 내부가 임시방편 조건문으로 지저분해지는 걸 막을 수 있습니다. 초기 세팅은 귀찮아도 프로파일 개수가 늘어날수록 이 구조가 앱을 훨씬 튼튼하게 만들어 줍니다.