Skip to main content
← 블로그

폰 앱을 아이패드에서 데스크톱처럼 — 폭 하나로 두 레이아웃 살리기

VauDium·

같은 코드베이스에서 폰은 탭바, 아이패드는 상단 탑바와 마스터-디테일로. 가로 잠금이 부른 크래시, 떠 보이던 서식 툴바, 폭 기준 분기 하나로 두 모습을 공존시킨 이야기.

폰 앱을 아이패드에서 데스크톱처럼 — 폭 하나로 두 레이아웃 살리기

fecit은 폰 앱으로 시작했습니다. 하단 탭바, 한 번에 한 화면, 위로 쌓이는 네비게이션. 그런데 데스크톱 웹은 이미 다른 모습이었습니다. 상단 바에 로고와 메뉴, 왼쪽에 목록 오른쪽에 상세를 같이 보는 마스터-디테일.

아이패드는 그 사이에 끼어 있었습니다. 폰 앱을 그대로 키우면 화면이 휑하고, 데스크톱을 그대로 가져오면 터치에 안 맞습니다. 같은 앱이 화면 크기에 따라 다른 몸을 입어야 했습니다.

기준은 기기가 아니라 폭

가장 먼저 정한 건 “언제 데스크톱 모습으로 바꿀까”였습니다.

Platform.isPad로 가르고 싶은 유혹이 있었지만, 안 그러기로 했습니다. 아이패드도 split-view로 화면을 반만 쓰면 폰만큼 좁아지고, 그땐 폰 레이아웃이 맞습니다. 그래서 기기가 아니라 폭으로 갈랐습니다. 데스크톱 웹이 쓰던 768px breakpoint를 그대로 미러했습니다.

export const LARGE_SCREEN_BREAKPOINT = 768;

export function useIsLargeScreen() {
    const {width} = useWindowDimensions();
    return width >= LARGE_SCREEN_BREAKPOINT;
}

useWindowDimensions를 쓰니 회전, split-view, Stage Manager 리사이즈에 다 반응합니다. 좁은 split-view 창은 의도대로 폰 레이아웃으로 떨어지고, 나중에 안드로이드 태블릿을 켜도 같은 코드가 동작합니다. 기준이 폭 하나니까요.

탭바를 상단 바로

폰에서는 하단 탭바가 다섯 화면을 오갑니다. 큰 화면에선 이게 어색합니다. 화면은 넓은데 손은 아래 구석까지 가야 하고, 위쪽 공간은 비어 있죠.

그래서 큰 화면에선 하단 탭바를 걷어내고 상단 바(AppTopBar) 로 대체했습니다. 로고, 서브뷰 스위처, 아이콘 네비게이션, 그리고 그 화면의 액션(검색·생성·추가·메뉴)을 한 줄에 모았습니다. 폰에서 화면 안에 흩어져 있던 버튼들이, 큰 화면에선 상단 바의 플레인 아이콘으로 올라갑니다.

한 번에 한 화면 → 목록과 상세를 나란히

진짜 작업은 레이아웃이었습니다. Tasks·Library·Memo는 큰 화면에서 마스터-디테일로 바꿨습니다. 왼쪽에 목록, 오른쪽에 상세. 왼쪽엔 폴더 트리 사이드바도 붙었습니다.

폰에서는 항목을 누르면 상세 화면이 위로 쌓이고, 뒤로가기로 돌아옵니다. 큰 화면에선 상세가 오른쪽 pane에 그냥 열립니다. 같은 상세 화면 컴포넌트인데, 한쪽에선 풀스크린으로 밀려 올라오고 다른 쪽에선 pane 안에 박힙니다.

여기서 까다로운 게 하나 있었습니다. pane 안에 박힌 상세에는 뒤로가기 버튼이 있으면 안 됩니다. 풀스크린일 땐 필요하지만, 옆에 항상 떠 있는 pane에선 돌아갈 데가 없으니까요. 같은 컴포넌트가 자기가 어디에 들어가 있는지 알아야 했습니다. 그래서 pane 안에 있다는 걸 알려주는 컨텍스트를 두고, 그 안에서는 뒤로가기를 숨겼습니다.

Community와 Me는 결이 조금 다릅니다. 목록-상세가 아니라 메뉴-콘텐츠라, 왼쪽 메뉴 오른쪽 콘텐츠로 나눴습니다.

가로 잠금이 부른 크래시

큰 화면 레이아웃은 가로를 가정합니다. 그래서 앱을 가로 전용으로 잠갔습니다. 그랬더니 크래시가 났습니다.

원인은 RN의 Modal이었습니다. 앱은 가로로 잠겨 있는데, 모달이 세로(portrait)를 기본으로 열려고 하면서 둘이 충돌했습니다. 잠긴 방향과 모달이 원하는 방향이 안 맞으면 그냥 죽는 거였습니다.

해결은 모달마다 supportedOrientations를 명시해서 앱의 잠금과 맞춰주는 것이었습니다. 화면 방향 같은 건 한 번 정하면 끝일 줄 알았는데, 모달이라는 별도 레이어가 자기 방향을 따로 들고 있었던 셈입니다.

떠 보이던 서식 툴바

큰 화면에선 글쓰기에 키보드 단축키(⌘ 조합)도 붙였습니다. 하드웨어 키보드를 쓰니까요. 그리고 서식 툴바(굵게·기울임 등)를 description 입력칸 위에 sticky로 두려고 했습니다.

처음엔 이 툴바가 떠 있는 얇은 바처럼 보였습니다. 입력 컨테이너의 패딩(8px) 안에 들어가 있어서, 라벨과 툴바 사이에 빈 띠가 생기고 좌우로도 끝까지 안 닿았습니다. 작은 차이인데 눈에 거슬렸습니다.

음수 마진으로 툴바를 풀폭으로 펴고 라벨에 딱 붙인 다음, 툴바 아래에만 8px 간격을 둬 입력칸과 분리했습니다. 그 뒤로도 높이를 50에서 38로, 라벨 아래 간격을 8에서 4로 줄여 더 컴팩트하게 다듬었습니다. “툴바를 단다”는 한 줄이 실제로는 여백을 몇 번이고 다시 재는 일이었습니다.

큰 정리 하나

이 작업을 하면서 가장 큰 변화는 눈에 안 보이는 곳에서 일어났습니다. Tasks 탭 파일 하나가 1,100줄 넘게 줄었습니다.

폰과 큰 화면이 같은 화면을 다르게 그려야 하니, 한 파일에 두 레이아웃을 다 욱여넣으면 금방 손댈 수 없게 됩니다. 공통 로직과 레이아웃을 갈라내고, split layout·상단 바·사이드바를 각자의 자리로 빼냈습니다. 결과적으로 분기는 useIsLargeScreen 한 줄로 모이고, 나머지는 각 레이아웃 컴포넌트가 책임집니다.

배운 것

  1. 반응형의 기준은 기기가 아니라 폭이다. isPad로 가르면 split-view에서 어긋난다. 폭으로 가르면 한 코드가 폰·아이패드·태블릿·리사이즈를 다 덮는다.

  2. 같은 상세 컴포넌트라도 자기가 어디 들어갔는지 알아야 한다. 풀스크린이면 뒤로가기, pane이면 없음. 컨텍스트로 알려주면 컴포넌트가 알아서 처신한다.

  3. 방향 잠금은 모달까지 따라가지 않는다. 앱을 가로로 잠가도 모달은 자기 방향을 따로 들고 있다. supportedOrientations를 명시하지 않으면 충돌로 죽는다.

  4. 여백은 한 번에 안 맞는다. “툴바를 단다”는 패딩·마진·높이·간격을 몇 번이고 다시 재는 일이다. 떠 보이면 대개 부모 패딩 안에 갇혀 있는 것이다.

  5. 두 레이아웃을 공존시키려면 먼저 갈라야 한다. 한 파일에 다 넣으면 못 고친다. 분기는 한 곳에 모으고, 레이아웃은 각자 빼낸다.


폰 앱 하나를 만들었다고 생각했는데, 화면이 커지자 같은 앱이 다른 몸을 원했습니다. 결국 둘은 폭 하나로 갈리는 같은 뼈대였습니다.