태그 리액트 네이티브 모달앱 멈춤온보딩리액트 네이티브모바일 버그화면 전환
전체 글 보기

리액트 네이티브 모달

리액트 네이티브 모달은 항상 위에 뜹니다 - 신규 사용자 소프트락

안내 카드의 확인 버튼이 안 눌리고 앱이 멈춘 것처럼 보인다는 제보를 받았습니다. 리액트 네이티브 모달은 별도의 창이라, 같은 화면의 다른 요소보다 무조건 위에 뜹니다.

2026년 8월 11일 4분

제보는 이렇게 들어왔습니다.

처음 들어갔는데 “알겠어요” 버튼이 안 눌려요. 앱이 멈춘 것 같아요.

화면에는 첫 사용자를 위한 안내 카드가 떠 있습니다. 버튼도 잘 보입니다. 누르면 아무 일도 안 일어납니다. 뒤로 가기도 소용없습니다.

에러는 없습니다. 앱은 정상적으로 돌고 있습니다. 그냥 아무 터치도 안 먹습니다.

리액트 네이티브 모달은 화면 안에 있지 않습니다

원인은 리액트 네이티브 모달이었습니다. 안내 카드 위에 투명한 모달이 하나 떠 있었습니다.

여기서 중요한 건 “위” 라는 말의 의미입니다. 보통 화면 안에서 무엇이 위에 오는지는 우리가 정합니다. 쌓이는 순서를 바꾸거나 값을 조절하면 원하는 대로 됩니다.

리액트 네이티브 모달은 그 규칙 밖에 있습니다. 모달을 열면 운영체제가 창을 하나 더 만들고, 그 창이 앱 화면 전체를 덮습니다. 같은 화면 안의 요소가 아니라 화면 위에 얹힌 별개의 판입니다.

그래서 이렇게 됩니다.

  • 화면 안에서 아무리 위로 올려도 모달보다 위로 갈 수 없습니다.
  • 모달이 투명하면 아래가 그대로 다 보입니다.
  • 그런데 터치는 위에 있는 모달이 전부 가져갑니다.

보이는 것과 눌리는 것이 어긋나는 상태입니다. 사용자 입장에서는 버튼이 멀쩡히 보이는데 안 눌리니까, 이건 “버튼이 고장났다” 가 아니라 “앱이 멈췄다” 로 읽힙니다.

제 경우 그 투명한 모달은 아침 계획을 묻는 시트였습니다. 같은 화면이 첫 진입에서 자동으로 여는 것이었고, 안내 카드와 정확히 같은 타이밍에 떴습니다.

// 이 시트는 안이 Modal 이다. 즉 별도의 창이다.
<Modal visible={visible} animationType="slide" transparent statusBarTranslucent>

transparent 가 붙어 있으니 화면은 그대로 보입니다. 그게 이 버그를 눈으로 못 잡게 만든 부분입니다.

코드만 봐서는 안 보입니다

이 버그가 오래 살아남은 이유가 있습니다.

두 요소가 서로를 모릅니다. 안내 카드는 화면 JSX 안에 있고, 시트는 다른 파일에 있는 별개의 컴포넌트입니다. 각각을 따로 읽으면 둘 다 정상입니다.

둘이 동시에 뜰 수 있다는 사실은 코드 어디에도 안 적혀 있습니다. “이 화면에 안내 카드가 떠 있을 때 시트를 열면 안 된다” 는 조건은 아무 파일에도 없었습니다. 그런 조건이 필요하다는 것 자체를 몰랐으니까요.

게다가 두 요소 다 실제로 화면에 있습니다. 하나가 다른 하나를 가린 것도 아니고, 렌더링이 실패한 것도 아닙니다. 둘 다 정상적으로 그려졌고, 터치를 한쪽이 독점했을 뿐입니다.

그래서 순서를 강제했습니다

고친 방식은 단순합니다. 안내 카드가 떠 있는 동안에는 자동으로 열리는 것들을 미룹니다.

// 가이드가 떠 있으면 미룬다. 이 시트는 RN Modal (별도 창) 이라
// 가이드(일반 View) 위에 떠서 터치를 전부 가져간다.
// 사용자에겐 "알겠어요가 안 눌리고 앱이 멈춘" 것으로 보인다.
if (showGuide) return;

여기서 놓치기 쉬운 게 하나 있습니다. 이 조건을 어디서 판단하느냐입니다.

처음에는 안내 카드를 띄울지 말지를 화면이 그려진 다음에 결정하고 있었습니다. 저장된 값을 읽어와서 “아직 안 봤네” 싶으면 그때 켜는 식입니다.

문제는 그 사이에 시트를 여는 쪽이 먼저 돈다는 것입니다. 순서가 이렇게 됩니다.

  1. 화면이 그려진다. 이때 안내 카드는 아직 꺼져 있다.
  2. 시트를 여는 코드가 돈다. 안내 카드가 꺼져 있으니 조건을 통과한다. 시트가 열린다.
  3. 그제서야 안내 카드가 켜진다.

가드를 넣었는데도 그 가드가 통과되는 상황입니다. “가이드가 떠 있으면 막는다” 를 검사하는 시점에 아직 안 떠 있었으니까요.

그래서 화면이 그려지기 전에 값을 정하도록 바꿨습니다.

// 값을 처음부터 결정한다. 나중에 켜면 그 사이에 시트가 먼저 열려버린다.
const [showGuide, setShowGuide] = useState(
  () => !(DeviceStore.get<boolean>(CacheKeys.GUIDE_TIMELINE_V1_DONE) ?? false),
);

읽는 값이 기기에 저장된 것이라 기다릴 필요가 없어서 가능한 방법입니다. 서버에서 받아와야 하는 값이었다면 이 방법은 안 되고, “아직 모름” 상태를 하나 더 두고 그동안 아무것도 안 여는 쪽으로 가야 합니다.

그리고 보이지도 않는 화면이 시트를 열고 있었습니다

같은 자리에서 문제가 하나 더 나왔습니다.

이 앱은 아래 탭으로 화면을 오갑니다. 그런데 탭 설정 중에 안 보이는 화면도 메모리에 올려두는 옵션이 켜져 있습니다. 탭을 옮길 때마다 화면을 새로 만들면 느리고 상태도 날아가기 때문에 일부러 그렇게 둔 것입니다.

문제는 “안 보인다” 와 “안 돌아간다” 가 다르다는 점입니다. 안 보이는 화면도 코드는 정상적으로 돕니다.

그래서 앱을 켜자마자 다른 탭을 보고 있는데, 뒤에서 조용히 돌던 이 화면이 시트를 열어버립니다. 사용자에게는 엉뚱한 화면에서 갑자기 시트가 튀어나온 것으로 보입니다.

// 이 탭이 실제로 보이고 있을 때만 연다.
const isFocused = useIsFocused();
...
if (!isFocused) return;

여기서도 판단이 하나 필요했습니다. 안 보일 때는 그냥 건너뛸 것인가, 아니면 나중에 다시 시도할 것인가.

건너뛰면 그날 시트를 아예 못 봅니다. “오늘 한 번만 띄운다” 는 기록을 이미 남겼다면 더 그렇습니다. 그래서 버리지 않고 미루도록 했습니다. 탭이 보이는 시점에 다시 판단하고, 기록도 모든 조건을 통과한 뒤에 남깁니다.

왜 이게 하필 신규 사용자에게만 터졌나

이 버그의 조건은 딱 하나입니다. 안내 카드와 자동 시트가 같은 시점에 뜨는 것.

안내 카드는 처음 한 번만 뜹니다. 그러니까 기존 사용자는 이 조합을 만들 수가 없습니다. 저를 포함해서요.

반대로 신규 사용자는 100% 이 경로를 지납니다. 꿈을 만들면 이 화면으로 자동으로 넘어오게 되어 있고, 넘어오자마자 안내 카드가 뜨고, 같은 순간 아침 계획 시트가 열립니다.

즉 이 버그는 앱을 처음 켠 사람만 겪고, 이미 쓰고 있는 사람은 절대 못 만납니다. 가장 중요한 순간에, 가장 확인하기 어려운 사람들에게만 나타난 것입니다.

첫 할일을 완료하는 비율이 거의 0에 가까웠는데, 여기에 이게 얼마나 기여했는지는 정확히 모릅니다. 다만 앱을 처음 켠 사람이 첫 화면에서 아무것도 못 누른다면, 그 뒤 숫자를 해석하는 건 의미가 없습니다.

남는 규칙

첫째, 리액트 네이티브 모달은 화면 안의 다른 요소와 경쟁하지 않습니다. 항상 위입니다. 그러니 화면 안에 직접 올린 안내나 오버레이가 있고, 같은 화면이 모달을 자동으로 연다면, 그 둘은 반드시 순서를 정해줘야 합니다. 둘 다 “보이는” 상태로 공존할 수 있다는 게 이 문제의 핵심입니다.

둘째, 무언가를 막는 조건은 막으려는 대상보다 먼저 정해져 있어야 합니다. 나중에 켜지는 값으로 가드를 세우면, 그 가드는 정확히 필요한 첫 순간에 없습니다.

셋째, 화면이 안 보인다고 코드가 안 도는 게 아닙니다. 자동으로 무언가를 여는 코드에는 “지금 이 화면이 실제로 보이는가” 를 같이 확인하세요.

넷째, 처음 한 번만 나타나는 상태는 만드는 사람이 제일 못 봅니다. 그 상태를 되돌리는 스위치를 개발자 메뉴에 하나 만들어 두는 게 낫습니다. 저는 이 일 이후로 안내 카드 기록을 초기화하는 항목을 넣었습니다.

같은 앱에서 예약 알림이 계정을 넘어 살아남던 일도 성격이 비슷했습니다. 에러가 안 나는 대신 사람이 겪는 증상만 이상해지는 종류입니다.

2026년 8월 11일 기준이고, React Native 0.85 · Expo SDK 56 · React Navigation 7 · 안드로이드에서 확인했습니다. 모달이 별도 창이라는 점은 iOS 도 같지만, 화면을 안 보일 때도 유지하는 옵션은 탭 설정에 따라 다릅니다.

이전 글 연속 기록을 없앴습니다 - 하루 놓치면 0이 되는 구조